Skip to main content

Getting Started with Credentials

This guide walks you through a simple issuance, presentation, and verification using the APIs. It picks up where the Quick Starts for the Core and the Full Stack guides stop, and assumes you have completed one of those guides (in other words, you have Procivis One running somehow).

Notes​

The guide is written for the Core API with static token authentication (default), and works whether the API is served locally or on some domain. If you are using:

  • Desk UI — You can follow the step headlines as your path through the UI.
  • Desk API/BFF service — You will not need to include the organisationId in your calls, as this context is stored in your session. Otherwise, all API calls in this guide work equally for the Desk API.
  • Other HTTP clients — Following the Quick Starts linked above, calls are shown as they would be made from Swagger UI, where the base URL and auth header are already applied once you hit Authorize. If you are using Postman, Bruno, or a similar client instead, you will need to add the base URL and the Content-Type, Accept, and Authorization: Bearer <token> headers yourself — see Making Requests for full request details.

Placeholders​

Throughout this guide, values you get back from one call and need in a later call are shown as <PLACEHOLDER_NAME>. As you go, save each one somewhere — this guide will tell you which step needs it next.

Lost track of a UUID?

Every resource in this guide can be looked up again with a list call:

  • Organizations — GET /api/organisation/v1
  • Keys — GET /api/key/v1
  • Identifiers — GET /api/identifier/v1
  • Credential schemas — GET /api/credential-schema/v2
  • Proof schemas — GET /api/proof-schema/v1
  • Credentials — GET /api/credential/v1
  • Proof requests — GET /api/proof-request/v1

1. Set up organizations and identifiers​

Everything in this section is prep work and none of it is time-limited, so feel free to stop and pick this back up later. You will create two organizations: one to act as issuer and verifier, and one to act as your wallet/holder.

1.1 Create organizations​

POST /api/organisation/v1

{}

response:

{
"id": "<ISSUER_ORG_ID>"
}

Do this twice — once for your issuer/verifier organization, once for your wallet organization.

response:

{
"id": "<WALLET_ORG_ID>"
}

You need two organizations because, in this guide, one plays the issuer and verifier roles while the other plays the holder. In practice, a single organization can do all three, but we keep them separate here so it's always clear which role is acting.

1.2 Create keys​

POST /api/key/v1

{
"keyParams": {},
"keyType": "ECDSA",
"name": "IssuingKey",
"organisationId": "<ISSUER_ORG_ID>",
"storageParams": {},
"storageType": "INTERNAL"
}

response:

{
"id": "<ISSUER_KEY_ID>"
}

Repeat for the wallet organization:

POST /api/key/v1

{
"keyParams": {},
"keyType": "ECDSA",
"name": "WalletKey",
"organisationId": "<WALLET_ORG_ID>",
"storageParams": {},
"storageType": "INTERNAL"
}

response:

{
"id": "<WALLET_KEY_ID>"
}

1.3 Issuer/Verifier: Create a certificate identifier​

You need an identifier to sign the credentials you issue and the requests you make as a verifier. There are several supported identifier types — here we create an X.509 certificate, generated from a self-signed root CA.

Create the root CA

POST /api/identifier/v1

{
"name": "Root CA",
"organisationId": "<ISSUER_ORG_ID>",
"certificateAuthorities": [
{
"keyId": "<ISSUER_KEY_ID>",
"name": "Optional internal reference",
"selfSigned": {
"content": {
"subject": {
"commonName": "Acme Root CA",
"countryName": "CH",
"organisationName": "Acme Co"
}
},
"signer": "X509_CERTIFICATE",
"validityStart": "2026-07-22T00:00:00Z",
"validityEnd": "2027-07-21T00:00:00Z"
}
}
]
}
note

validityStart cannot be in the past. validityEnd can be at most 1825 days after validityStart by default, per your instance's configuration.

response:

{
"id": "<ROOT_CA_ID>"
}

Create the certificate, signed by the CA you just created:

POST /api/identifier/v1

{
"certificates": [
{
"content": {
"certificateAuthority": {
"identifierId": "<ROOT_CA_ID>"
},
"profile": "GENERIC",
"signer": "X509_CERTIFICATE",
"subject": {
"commonName": "Acme Co Certificate",
"countryName": "CH",
"organisationName": "Acme Co"
},
"validityStart": "2026-07-22T00:00:00Z",
"validityEnd": "2027-07-21T00:00:00Z"
},
"keyId": "<ISSUER_KEY_ID>",
"name": "Certificate display name",
"roles": [
"AUTHENTICATION",
"ASSERTION_METHOD"
]
}
],
"name": "Certificate identifier",
"organisationId": "<ISSUER_ORG_ID>"
}

Reusing the same key for the CA and the certificate is fine for local testing like this — it's not a production pattern, but there's no functional downside here.

response:

{
"id": "<CERT_IDENTIFIER_ID>"
}

Save this, as it is the identifier you will use for both issuing (§3) and verifying (§4).

1.4 Wallet: Create a key identifier​

POST /api/identifier/v1

{
"key": {
"keyId": "<WALLET_KEY_ID>"
},
"name": "MyWallet",
"organisationId": "<WALLET_ORG_ID>"
}

response:

{
"id": "<WALLET_IDENTIFIER_ID>"
}

Save this — you will use it to accept the credential in §3.3.


2. Define what you will issue and request​

Also not time-limited. Here you define the shape of the credential (a reusable schema) and the shape of the request you will later use to verify it (a reusable proof schema).

2.1 Create a credential schema​

POST /api/credential-schema/v2

{
"claims": [
{ "datatype": "STRING", "key": "Name", "required": true },
{ "datatype": "BIRTH_DATE", "key": "Birthdate", "required": true },
{ "datatype": "EMAIL", "key": "Email", "required": true }
],
"formats": [
{ "format": "SD_JWT_VC" }
],
"allowSuspension": true,
"allowRevocation": true,
"name": "Simple SD-JWT VC",
"organisationId": "<ISSUER_ORG_ID>"
}

response:

{
"id": "<CREDENTIAL_SCHEMA_ID>"
}

You can reuse this same schema any time you want to issue a "Simple SD-JWT VC" credential.

2.2 Retrieve claim IDs​

GET /api/credential-schema/v2/<CREDENTIAL_SCHEMA_ID>

response (trimmed to the relevant fields):

{
"id": "<CREDENTIAL_SCHEMA_ID>",
"claims": [
{ "id": "<NAME_CLAIM_ID>", "key": "Name" },
{ "id": "<BIRTHDATE_CLAIM_ID>", "key": "Birthdate" },
{ "id": "<EMAIL_CLAIM_ID>", "key": "Email" }
]
}

Save these three claim IDs, as you will need them both to create the credential (§3.1) and to create the proof schema below.

2.3 Create a proof schema​

A proof schema is a template of the claims you want to request. You will use it any time you want to request this data from a wallet.

POST /api/proof-schema/v1

{
"name": "Simple Request",
"organisationId": "<ISSUER_ORG_ID>",
"proofInputSchemas": [
{
"claimSchemas": [
{ "id": "<NAME_CLAIM_ID>", "required": true },
{ "id": "<BIRTHDATE_CLAIM_ID>", "required": true },
{ "id": "<EMAIL_CLAIM_ID>", "required": true }
],
"credentialSchemaId": "<CREDENTIAL_SCHEMA_ID>"
}
]
}

response:

{
"id": "<PROOF_SCHEMA_ID>"
}

Save this — you will use it in §4.1.


3. Issue a credential​

tip

Time-sensitive from here. Once you share the offer in §3.2, it expires (expiresAt in the response). Read through §3.2 - §3.3 fully before you start, then run them back-to-back.

3.1 Issuer: Prepare the credential​

POST /api/credential/v1

{
"claimValues": [
{ "claimId": "<NAME_CLAIM_ID>", "path": "Name", "value": "Jane A. Smith" },
{ "claimId": "<BIRTHDATE_CLAIM_ID>", "path": "Birthdate", "value": "1985-01-14T23:00:00.000Z" },
{ "claimId": "<EMAIL_CLAIM_ID>", "path": "Email", "value": "jsmith85@example.com" }
],
"credentialSchemaId": "<CREDENTIAL_SCHEMA_ID>",
"protocol": "OPENID4VCI_FINAL1",
"issuer": "<CERT_IDENTIFIER_ID>"
}

No organisationId needed here — it's inferred from the schema.

response:

{
"id": "<CREDENTIAL_ID>"
}

The credential data is prepared and ready to issue whenever you are. This step itself is not time-limited — the clock starts at the next step.

3.2 Issuer: Share the offer​

POST /api/credential/v1/<CREDENTIAL_ID>/share

response:

{
"url": "openid-credential-offer://?credential_offer_uri=...",
"expiresAt": "2026-07-22T04:21:10.325Z"
}

If you were issuing to a mobile wallet, this is the URL you would encode as a QR code. Here, we hand it directly to our API-based wallet in the next step.

3.3 Wallet: Accept the offer​

POST /api/interaction/v1/handle-invitation

{
"organisationId": "<WALLET_ORG_ID>",
"transport": ["HTTP"],
"url": "<OFFER_URL from §3.2>"
}

response (trimmed):

{
"interactionId": "<ISSUANCE_INTERACTION_ID>",
"interactionType": "ISSUANCE"
}

POST /api/interaction/issuance-accept

{
"identifierId": "<WALLET_IDENTIFIER_ID>",
"interactionId": "<ISSUANCE_INTERACTION_ID>"
}

response:

{
"id": "<HELD_CREDENTIAL_ID>"
}

This is the credential's ID as held in the wallet — distinct from <CREDENTIAL_ID> from §3.1, which is the issuer's record of it. You will use <HELD_CREDENTIAL_ID> in §4.3.


4. Verify a credential​

tip

Time-sensitive again. The proof request you share in §4.2 also expires. Read §4.2 - §4.3 fully before starting, then run them back-to-back.

4.1 Verifier: Create a proof request​

POST /api/proof-request/v1

{
"proofSchemaId": "<PROOF_SCHEMA_ID>",
"protocol": "OPENID4VP_FINAL1",
"transport": ["HTTP"],
"verifier": "<CERT_IDENTIFIER_ID>"
}

response:

{
"id": "<PROOF_REQUEST_ID>"
}

The request is prepared — again, not time-limited until you share it.

4.2 Verifier: Share the request​

POST /api/proof-request/v1/<PROOF_REQUEST_ID>/share

response:

{
"url": "openid4vp://?client_id=...&request_uri=...",
"expiresAt": "2026-07-22T10:12:40.451Z"
}

4.3 Wallet: Respond to the request​

POST /api/interaction/v1/handle-invitation

{
"organisationId": "<WALLET_ORG_ID>",
"transport": ["HTTP"],
"url": "<REQUEST_URL from §4.2>"
}

response (trimmed):

{
"interactionId": "<VERIFICATION_INTERACTION_ID>",
"interactionType": "VERIFICATION",
"proofId": "<PROOF_ID>"
}

Check what's being requested against credentials you hold:

GET /api/proof-request/v2/<PROOF_ID>/presentation-definition

response (trimmed):

{
"credentialQueries": {
"<CREDENTIAL_SCHEMA_ID>": {
"applicableCredentials": [
{ "id": "<HELD_CREDENTIAL_ID>", "state": "ACCEPTED" }
]
}
}
}

Submit the matching credential:

POST /api/interaction/v2/presentation-submit

{
"interactionId": "<VERIFICATION_INTERACTION_ID>",
"submission": {
"<CREDENTIAL_SCHEMA_ID>": [
{ "credentialId": "<HELD_CREDENTIAL_ID>" }
]
}
}

4.4 Verifier: Check the result​

GET /api/proof-request/v1/<PROOF_REQUEST_ID>

response (trimmed):

{
"id": "<PROOF_REQUEST_ID>",
"state": "ACCEPTED",
"proofInputs": [
{
"claims": [
{ "path": "Name", "value": "Jane A. Smith" },
{ "path": "Birthdate", "value": "1985-01-14T23:00:00.000Z" },
{ "path": "Email", "value": "jsmith85@example.com" }
]
}
]
}

A state of ACCEPTED with the claim values you attested to in §3.1 confirms the full loop worked — a credential issued, held, presented, and verified.

Next Steps​