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
organisationIdin 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, andAuthorization: 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"
}
}
]
}
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
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
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
- Explore other identifier types → Identifier Management
- Learn more about credential schema options → Credential Schemas
- Learn about other supported issuance flows → Receiving a Credential