Credentials API
Credentials are the core output of the Reference Implementation. Everything else in the system — DIDs, services, data models, identifiers, and master data — exists to support one goal: issuing trusted, verifiable digital documents about products, facilities, organisations, and supply chain events.
A credential is a digitally signed statement. A company issues a credential that says "this product was made sustainably" or "this facility passed a conformity assessment". Because the credential is cryptographically signed, anyone who receives it can verify that the statement hasn't been tampered with and that it really came from the company that claims to have issued it, without needing to contact the issuer directly.
The Credentials API has two sides:
- Issuance (authenticated) — a tenant creates a credential, the system validates it, signs it, stores it, and optionally publishes it so it can be discovered by resolving an identifier.
- Verification (public, no login required) — anyone with a link to a stored credential can check whether it's genuine, untampered, and still valid.
The Reference Implementation includes a Swagger UI at /api-docs with full request/response schemas you can try directly from the browser. The endpoint descriptions below focus on behaviour and internal logic. Refer to Swagger for exact payload shapes. All endpoints except Verify require authentication. See Authentication for how to obtain a Bearer token.
Concepts
What's Inside a Credential?
Every credential issued by the Reference Implementation follows the W3C Verifiable Credentials Data Model and the UNTP Verifiable Credential profile. In plain terms, a credential contains:
- Who issued it — the issuer's DID (a cryptographic identity)
- What it says — the credential subject (e.g., product sustainability data, conformity assessment results)
- When it was issued — a timestamp
- A digital signature — proof that the issuer really signed it and that nobody changed it afterwards
- A credential status — a mechanism for the issuer to revoke or suspend the credential later if needed (managed via BitstringStatusList by the VC service)
How Credentials Are Packaged
UNTP credentials use the enveloped form: the credential payload is signed as a JWT (JSON Web Token), and the JWT is wrapped inside a JSON-LD envelope. This means you get the best of both worlds — compact, efficient JWT signatures with the semantic richness of linked data.
When the Reference Implementation issues a credential, the result is an EnvelopedVerifiableCredential that looks like this:
{
"@context": ["https://www.w3.org/ns/credentials/v2"],
"type": "EnvelopedVerifiableCredential",
"id": "data:application/vc+jwt,eyJhbGciOiJFZDI1NTE5..."
}
The id field contains the actual JWT. The verification endpoint knows how to unwrap this and decode the original credential payload.
Credential Types
The type of credential determines what kind of data it contains and which schema is used to validate it. Credential types are defined by data models — see the Data Models API for the full list of core UNTP types and how extension data models work.
Encryption and Privacy
When a credential is issued, two things are created: the signed credential (stored externally by the storage service) and a credential record (stored in the Reference Implementation's database, tracking metadata like the storage URI, hash, and published status).
By default, the storage service encrypts the signed credential before storing it. When encrypted, the file at the storage URI is unreadable without the decryption key. The decryption key is returned by the storage service and saved in the credential record so it can be provided later during verification.
This matters for privacy: a credential about a product's supply chain might contain commercially sensitive information. Encryption ensures that only someone with the key can read it, even if they have the storage URL.
| Setting | What Happens |
|---|---|
encrypt: true (default) | Credential encrypted before storage. A decryptionKey is returned. |
encrypt: false | Credential stored in plaintext. Anyone with the URL can read it. |
Integrity Hashing
Every stored credential has a content hash — a fingerprint computed from the credential's contents. If even one character changes, the hash changes. This allows anyone to detect that the credential at a storage URI hasn't been swapped or modified after being stored. During verification, the computed hash is compared against the expected hash.
Discoverability via the Identity Resolver
A credential on its own is just a file at a URL. To make it useful, it needs to be discoverable — someone who knows a product's identifier should be able to find the credential. This is the role of the UNTP Identity Resolver and Decentralised Access Control specifications.
This is where the Identity Resolver comes in. When a credential is published, the Reference Implementation registers a link with the Identity Resolver that connects the entity's identifier (e.g., a GS1 GTIN) to the credential's storage URL. Now anyone who resolves that identifier can find the credential.
Publishing is optional and requires the credential's primary entity (product, facility, or organisation) to have a configured identifier scheme with an IDR service.
CVC Compliance (Conformity Credentials Only)
For Digital Conformity Credentials, the issuance pipeline performs an extra advisory check: it compares the conformity criteria, standards, and regulations referenced in the credential against the tenant's imported CVC (Conformity Vocabulary Catalogue) data. This helps catch mistakes like referencing a non-existent standard or missing a required criterion.
CVC validation is advisory only — it never blocks issuance. If issues are found, the credential is still issued but the response includes warnings.
The Issuance Pipeline
Issuing a credential involves eight stages. Each stage can fail independently, and failures at different stages produce different HTTP status codes and warning codes. See the Issue a Credential endpoint for the full request and response reference.
Stage 1: Request Validation
The three required fields are validated:
| Field | Type | Required | Description |
|---|---|---|---|
credentialPayload | object | Yes | The full credential payload conforming to the UNTP schema for the specified type and version |
credentialType | string | Yes | Must match a registered data model (e.g., DigitalProductPassport) |
version | string | Yes | Must match a registered data model version (e.g., 0.6.1) |
Stage 2: Data Model Resolution
The credentialType and version are used to look up a registered data model. The data model provides the JSON Schema URL(s) for validation, the JSON-LD context URL, and the bridge that extracts entity references from the payload.
For extension data models, both the parent schema and the extension schema are validated.
Stage 3: Payload Validation
The credential payload is validated in two passes:
- JSON Schema validation against the data model's schema URL(s). This catches structural issues such as missing required fields, incorrect types, or invalid enum values.
- JSON-LD expansion to verify the payload is valid linked data with a resolvable
@context.
If either check fails, the request is rejected with HTTP 400.
Stage 3.5: CVC Compliance Validation (Advisory)
For Digital Conformity Credentials (DCC), the issuance pipeline performs an advisory check against the tenant's imported CVC (Conformity Vocabulary Catalogue) data. This check verifies that the conformity criteria, standards, and regulations referenced in the credential payload correspond to entries in the tenant's CVC catalogues.
CVC validation is advisory only. It never blocks issuance. If the check fails or no CVC data is available, the credential is issued with warnings in the response. Warning codes include:
| Code | Meaning |
|---|---|
CVC_NO_CONFORMITY | DCC credential payload has no conformity data to validate |
CVC_NO_SCOPE | No conformity scope (standard, regulation, or criterion) found in the payload |
CVC_SCOPE_NOT_FOUND | Referenced scope URL not found in imported CVC catalogues |
CVC_UNKNOWN_CRITERION | Referenced criterion URL not found in imported CVC catalogues |
CVC_MISSING_CRITERION | A required criterion from the scheme profile is not present in the credential |
CVC_NO_CRITERIA | No criteria found in the payload |
CVC_VALIDATION_ERROR | CVC validation could not be performed (infrastructure or extraction failure) |
Stage 4: Issuer DID Ownership Validation
The issuer.id field in the credential payload must contain a DID that the authenticated tenant is authorised to use. The Reference Implementation looks up the DID and verifies that it either:
- belongs to the authenticated tenant, or
- is a system default DID — available to all tenants as part of the incremental adoption ramp
If the DID is not registered to the tenant and is not a system default DID, the request is rejected with HTTP 400. A tenant cannot issue credentials using a DID that belongs to another tenant.
Stage 5: DID Service Association Check
The issuer DID must have an associated VC service instance — this is the service that holds the DID's key material and will perform signing. If the DID has no association (e.g., the service instance was force-deleted), the request is rejected with HTTP 400. The DID must be re-imported or re-created to restore the association.
Stage 6: Service Resolution
The VC service is resolved from the issuer DID's associated service instance. This ensures signing always happens on the VC service that holds the DID's key material, regardless of whether the DID is a tenant-owned DID or a system default DID. The caller does not need to specify which VC service to use; the DID determines it.
The storage service and IDR service follow the standard resolution chain:
| Service | Purpose | How Resolved |
|---|---|---|
| VC Service | Signs the credential payload | From the issuer DID's associated service instance |
| Storage Service | Stores the signed credential | storageOptions.serviceInstanceId, or tenant primary, or system default |
| IDR Service | Publishes links (only when publish: true) | From the entity's scheme configuration |
Stage 7: Sign, Store, and Record
The credential payload is signed by the VC service, producing an Enveloped Verifiable Credential. The signed credential is then stored by the storage service.
Encryption: By default, the stored credential is encrypted with AES-GCM. The decryption key is returned in the credential record and must be provided when verifying encrypted credentials. Set storageOptions.encrypt to false to store the credential unencrypted.
Entity linking: The data model bridge extracts entity references (organisations, facilities, products) from the credential payload. The primary entity (priority: product > facility > organisation) is linked to the credential record in the database. This link enables the optional publishing step.
Stage 8: IDR Publishing (Optional)
When publishingOptions.publish is true, the Reference Implementation publishes a link to the stored credential on the Identity Resolver for the primary entity's identifier. This makes the credential discoverable via the entity's identifier scheme (e.g., resolving a GS1 GTIN leads to the credential).
Publishing requires the primary entity to have:
- A primary identifier with a configured identifier scheme
- The scheme must have a registrar with a namespace
- An IDR service instance (configured on the scheme, or the tenant/system default)
If any of these are missing, publishing is silently skipped and a PUBLISH_SKIPPED warning is included in the response.
| Publishing Option | Type | Description |
|---|---|---|
publish | boolean | Whether to publish to the identity resolver |
linkType | string | Link relation type (defaults to gs1:sustainabilityInfo) |
linkTitle | string | Human-readable title for the link (defaults to the data model name) |
qualifierPath | string | Qualifier path for sub-identifiers, e.g., /10/LOT123/21/SER456 (defaults to /) |
machineVerificationUrl | string | URL for machine-readable verification of the credential |
humanVerificationUrl | string | URL for human-readable verification of the credential |
Issuance Endpoints
Issue a Credential
POST /api/v1/credentials
Validates, signs, stores, and optionally publishes a verifiable credential. Returns the credential's database ID and any advisory warnings.
Request body fields:
| Field | Type | Required | Description |
|---|---|---|---|
credentialPayload | object | Yes | Full credential payload conforming to the UNTP schema for the specified type and version |
credentialType | string | Yes | Registered data model type (e.g., DigitalProductPassport) |
version | string | Yes | Registered data model version (e.g., 0.6.1) |
storageOptions.serviceInstanceId | string | No | Explicit storage service instance |
storageOptions.encrypt | boolean | No | Whether to encrypt (default: true) |
publishingOptions.publish | boolean | No | Whether to publish to IDR |
publishingOptions.linkType | string | No | Link relation type |
publishingOptions.linkTitle | string | No | Link title (defaults to data model name) |
publishingOptions.qualifierPath | string | No | Qualifier path (default: /) |
publishingOptions.machineVerificationUrl | string | No | Machine verification URL |
publishingOptions.humanVerificationUrl | string | No | Human verification URL |
List Credentials
GET /api/v1/credentials
Returns a paginated list of credentials for the authenticated tenant.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
credentialType | string | Filter by credential type (case-sensitive exact match) |
isPublished | "true" or "false" | Filter by published status |
limit | integer | Maximum results (default: 20, max: 100) |
offset | integer | Number of results to skip (default: 0) |
Get a Credential
GET /api/v1/credentials/{id}
Retrieves a specific credential record by its database ID. The response includes the storage URI, hash, decryption key (if encrypted), credential type, published status, and linked entity IDs.
Verification Endpoint
Verify a Credential
POST /api/v1/credentials/verify
This endpoint does not require authentication. It is designed for third-party verification of credentials using parameters typically encoded in a QR code or verification link.
Request body fields:
| Field | Type | Required | Description |
|---|---|---|---|
uri | string (URL) | Yes | Storage URI where the credential is stored. Must be HTTP(S). |
hash | string | No | Expected SHA-256 hash (64-character hex string). If provided, the fetched credential's hash is verified against it. |
decryptionKey | string | No | AES-GCM decryption key (64-character hex string). Required for encrypted credentials. |
The endpoint always returns HTTP 200 for a completed verification attempt, even if the credential fails verification. Check the verified field for the outcome. Processing errors (decryption failure, hash mismatch, unsupported type) that prevent a verification attempt return non-200 status codes with a code field.
Environment Variables
| Variable | Default | Description |
|---|---|---|
VERIFY_ALLOW_PRIVATE_URLS | false | Set to true to bypass SSRF checks (development only) |
VERIFY_MAX_CREDENTIAL_SIZE | 10485760 (10 MB) | Maximum credential response size in bytes |