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'll 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