Issue a credential
The credential issuer credential endpoint is a required endpoint defined in the OID4VCI specification. It’s where GOV.UK Wallet, acting on behalf of the holder, requests and receives verifiable credentials from the credential issuer. As a credential issuer, you must implement this endpoint according to this specification to correctly integrate with GOV.UK Wallet.
Technical details
Endpoint location
The credential endpoint’s URI path is implementation-specific.
As the credential issuer, you must publish the location of your credential endpoint in your issuer metadata API using the credential_endpoint parameter.
Request format
Your credential endpoint must accept HTTP POST requests.
GOV.UK Wallet will send a request to your credential endpoint to get a verifiable credential. The credential request must include the:
Authorizationheader: a bearer access token, which must be a JSON Web Token (JWT), issued by GOV.UK One Login- JSON request body: a proof of possession of the cryptographic key material, in the form of a JWT, issued by GOV.UK Wallet — this proves the wallet controls the private key to which the credential will be bound
Request validation
Authorization header
The access token in the credential request authorises the issuance of a verifiable credential to GOV.UK Wallet. It’s different from the access token used when the user initially logs in to GOV.UK One Login, which is for authentication purposes. Because they have different roles, the access tokens are signed and verified using different keys.
You must validate the access token to make sure it was issued by GOV.UK One Login, and that the request originates from the expected user.
To validate the access token, you must complete the following steps:
Verify the signature
- Retrieve the GOV.UK One Login JSON Web Key Set (JWKS) from its published JWKS endpoint.
- Copy the
kid(key ID) parameter from the header. - Find the matching public key in the JWKS by comparing
kidvalues. - Confirm the
alg(algorithm) parameter in the token header matches the algorithm of the identified public key. - Use the matching public key to cryptographically verify the token signature using the specified algorithm.
Validate the header parameters
Make sure the value of the typ (type) parameter is at+jwt.
This is an example of an access token header:
{
"alg": "ES256",
"typ": "at+jwt",
"kid": "8f9ec544-f5df-4d37-a32d-a5defd78ab0f"
}
Validate the payload claims
Make sure that:
- the value of the
iss(issuer) claim matches the GOV.UK One Login URL:https://token.account.gov.ukfor production orhttps://token.integration.account.gov.ukfor integration - the value of the
aud(audience) claim is the credential issuer URL - the value of the
sub(subject - this is the wallet subject identifier) claim matches the value stored in your cache for this specific credential issuance flow - the value of the
exp(expiration time) claim is in the future - the value of the
credential_identifiersclaim matches the value stored in your cache for this specific credential issuance flow - the value of the
c_nonceclaim matches the value of thenonceclaim in the proof of possession - the value of the
jti(JWT ID) claim is unique among all non-expired access tokens received by this API
This is an example of an access token payload:
{
"sub": "urn:fdc:wallet.account.gov.uk:2024:DtPT8x-dp_73tnlY3KNTiCitziN9GEherD16bqxNt9i",
"iss": "https://token.account.gov.uk",
"aud": "https://example-credential-issuer.gov.uk",
"exp": 1756115975,
"credential_identifiers": [
"daa01d3e-b17c-4c8a-8adf-ef808b456c9c"
],
"c_nonce": "657a09cd-7165-486d-a858-065eb23f7a8d",
"jti": "62b45850-4c5c-4696-983a-af66450301d4"
}
The sub claim is a pairwise identifier generated by GOV.UK One Login. Your implementation must compare this sub claim against the wallet subject identifier you stored when the user authenticated with GOV.UK One Login. This comparison makes sure that the wallet requesting the credential belongs to the same user which authenticated with your service. This identifier starts with urn:fdc:wallet.account.gov.uk:.
Request body
The request body sent by GOV.UK Wallet to your credential endpoint must be a JSON object containing proof of possession that demonstrates the wallet’s control of the private key to which the credential will be bound.
It must contain the following parameters:
| Parameter | Description | Value(s) |
|---|---|---|
proof |
A JSON containing the proof of possession of the cryptographic key material. | |
proof.proof_type |
A string indicating the type of proof being presented. | Must be jwt. |
proof.jwt |
The JSON Web Token (JWT) that serves as the proof. |
Below is an example of a request body:
{
"proof":{
"proof_type":"jwt",
"jwt":"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ekRuYWVTR2ZTUU1Zdm5MYkxXRXViaGhHRFBvcTdwQTlNTU52dW12YnNtTUNab3ZVUiJ9.eyJpc3MiOiJ1cm46ZmRjOmdvdjp1azp3YWxsZXQiLCJhdWQiOiJodHRwczovL2V4YW1wbGUtY3JlZGVudGlhbC1pc3N1ZXIuZ292LnVrIiwiaWF0IjoxNzQ1MjMzNjIzODE2LCJub25jZSI6IjY1N2EwOWNkLTcxNjUtNDg2ZC1hODU4LTA2NWViMjNmN2E4ZCJ9.UNszjzQeT6Vv8-n5kEAoHIr84Tf2gCJxMzhiUBBPPHdv6l0JK3WzNVaV0V6wgkTccLozOa7y3_3lp2iM4KjOOw"
}
}
The proof of possession is a cryptographic mechanism that verifies the wallet controls the private key that matches the public key (did:key) that will be associated with the credential. This ensures credentials are issued to their rightful holder.
GOV.UK Wallet generates this token and includes a cryptographic client nonce (from the access token issued by GOV.UK One Login) that has been signed with the wallet’s private signing key. The did:key (the wallet’s public key) is included in the token’s header kid parameter.
When you receive the credential request, you can verify the proof of possession signature with the did:key. Successful verification shows the wallet’s ownership of the private key corresponding to that public did:key.
There is more information about the did:key method.
To validate the proof, you must complete the following steps:
Verify the signature
- Extract the
kid(key ID) parameter from the header, which contains the wallet’sdid:key. - Extract the public key from the
did:key. - Confirm the
alg(algorithm) parameter in the proof header isES256and is compatible with the key type derived from thedid:key. - Use the public key to cryptographically verify the proof signature using the specified algorithm.
Validate the header parameters
Make sure the value of the typ (type) parameter is openid4vci-proof+jwt.
This is an example of a proof header:
{
"alg": "ES256",
"typ": "openid4vci-proof+jwt",
"kid": "did:key:zDnaeSGfSQMYvnLbLWEubhhGDPoq7pA9MMNvumvbsmMCZovUR"
}
Validate the payload claims
Make sure the:
- value of the
iss(issuer) claim matches the GOV.UK Wallet identifier -urn:fdc:gov:uk:wallet - value of the
aud(audience) claim is the credential issuer URL - value of the
iat(issued at) is a numeric date formatted as seconds since the epoch - this must be a date in the past that is after the pre-authorised code was generated - value of the
nonceclaim matches the value of thec_nonceclaim in the access token
This is an example of a proof payload:
{
"iss": "urn:fdc:gov:uk:wallet",
"aud": "https://example-credential-issuer.gov.uk",
"iat": 1745233623816,
"nonce": "bd423745-7705-45c2-9f51-6ae8dcac5589"
}
You can find more information about the credential request in the OID4VCI specification.
Response format
After validating the request successfully, your credential endpoint must return a 200 OK HTTP status code and a JSON response following the OID4VCI specification.
Below is an example of a validated credential response:
{
"credentials": [
{
"credential": "eyJraWQiOiJkaWQ6d2ViOmV4YW1wbGUtY3JlZGVudGlhbC1pc3N1ZXIuZ292LnVrIzVkY2JlZTg2M2I1ZDdjYzMwYzliYTFmNzM5M2RhY2M2YzE2NjEwNzgyZTRiNmExOTFmOTRhN2U4YjFlMTUxMGYiLCJjdHkiOiJ2YyIsInR5cCI6InZjK2p3dCIsImFsZyI6IkVTMjU2In0.eyJpc3MiOiJodHRwczovL2V4YW1wbGUtY3JlZGVudGlhbC1pc3N1ZXIuZ292LnVrIiwic3ViIjoiZGlkOmtleTp6RG5hZVNHZlNRTVl2bkxiTFdFdWJoaEdEUG9xN3BBOU1NTnZ1bXZic21NQ1pvdlVSIiwiaWF0IjoiMTcxMjY2NDczMSIsIm5iZiI6IjE3MTI2NjQ3MzEiLCJleHAiOiIxNzQ0MjIxNjU3IiwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnL25zL2NyZWRlbnRpYWxzL3YyIiwiPEpTT04tTEQgQ09OVEVYVCBVUkkgRk9SIElTU1VFUj4iXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIkZpc2hpbmdMaWNlbmNlIl0sImlzc3VlciI6Imh0dHBzOi8vZXhhbXBsZS1jcmVkZW50aWFsLWlzc3Vlci5nb3YudWsiLCJuYW1lIjoiRmlzaGluZyBsaWNlbmNlIiwiZGVzY3JpcHRpb24iOiI8T1BUSU9OQUwgQ1JFREVOVElBTCBERVNDUklQVElPTj4iLCJ2YWxpZEZyb20iOiIyMDI0LTA0LTA5VDEyOjEyOjExWiIsInZhbGlkVW50aWwiOiIyMDI4LTEyLTEwVDIyOjU5OjU5WiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6RG5hZVNHZlNRTVl2bkxiTFdFdWJoaEdEUG9xN3BBOU1NTnZ1bXZic21NQ1pvdlVSIiwibmFtZSI6W3sibmFtZVBhcnRzIjpbeyJ2YWx1ZSI6IlNhcmFoIiwidHlwZSI6IkdpdmVuTmFtZSJ9LHsidmFsdWUiOiJFZHdhcmRzIiwidHlwZSI6IkZhbWlseU5hbWUifV19XSwiZmlzaGluZ0xpY2VuY2UiOlt7ImxpY2VuY2VOdW1iZXIiOiIwMDk4Nzg4NjMiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTEyLTEwIiwiZXhwaXJhdGlvbkRhdGUiOiIyMDI4LTEyLTEwIn1dfX0.UXGyl9ihcMNn-RlqBe_8rTxvtPDqdaGLKMJs3pk9QL4zgEZbF4LZmeZt-k2p04MSlW8IX8uWKgtxFDOGu9j2rQ"
}
],
"notification_id": "776aefd4-26c6-4a5f-aa7c-b5e294cd87cd"
}
You can find more information about the credential response in the OID4VCI specification. You should only include the notification_id in the response if you implement the notification endpoint for the callback success/failure notification.
To construct and issue the verifiable credential:
- Retrieve the underlying data that will go into the credential from your database using the information in your issuance cache.
- Build the verifiable credential according to the W3C Verifiable Credentials Data Model v2.0.
- Cryptographically bind the credential to the wallet by using the wallet’s
did:keyas the credential’s subject identifier (thesubclaim). - Sign the credential with your issuer’s private key.
The header and payload example below shows the structure of a verifiable credential using a JSON Web Token (JWT) format for a fishing licence.
Header
{
"alg": "ES256",
"typ": "vc+jwt",
"cty": "vc",
"kid": "did:web:example-credential-issuer.gov.uk#5dcbee863b5d7cc30c9ba1f7393dacc6c16610782e4b6a191f94a7e8b1e1510f"
}
| Parameter | Required or optional | Definition |
|---|---|---|
alg |
Required | algorithm. The cryptographic algorithm used to sign the JWT - must be ES256 for ECDSA using the P-256 curve. |
cty |
Required | content type. The media type of the secured content (the payload) - must be vc. |
typ |
Required | type. The media type of the signed content - must be vc+jwt. |
kid |
Required | key ID. The Decentralised Identifier (DID) URL of the issuer’s public key used for signature verification - this must match the id of the corresponding key in the credential issuer’s DID Document. |
Payload
{
"iss": "https://example-credential-issuer.gov.uk",
"sub": "did:key:zDnaeSGfSQMYvnLbLWEubhhGDPoq7pA9MMNvumvbsmMCZovUR",
"nbf": "1712664731",
"iat": "1712664731",
"exp": "1744221657",
"@context": [
"https://www.w3.org/ns/credentials/v2",
"<JSON_LD_CONTEXT_URI_FOR_ISSUER>"
],
"type": [
"VerifiableCredential",
"FishingLicenceCredential"
],
"issuer": "https://example-credential-issuer.gov.uk",
"name": "Fishing licence",
"description": "Permit for fishing activities",
"validFrom": "2024-04-09T12:12:11Z",
"validUntil": "2026-04-09T22:59:59Z",
"credentialSubject": {
"id": "did:key:zDnaeSGfSQMYvnLbLWEubhhGDPoq7pA9MMNvumvbsmMCZovUR",
"name": [
{
"nameParts": [
{
"value": "Sarah",
"type": "GivenName"
},
{
"value": "Edwards",
"type": "FamilyName"
}
]
}
],
"fishingLicenceRecord": [
{
"licenceNumber": "009878863",
"issuanceDate": "2023-12-10",
"expiryDate": "2028-12-10"
}
]
}
}
| Parameter | Required or optional | Definition |
|---|---|---|
@context |
Required | The context of the data exchange. This must be a set of URIs that point to documents that describe the context. The first item in the set must be the URI https://www.w3.org/ns/credentials/v2. |
credentialSubject |
Required | An object containing claims about the holder of the verifiable credential. |
iss |
Required | Issuer. The URL of the credential issuer service operated by the organisation sharing the credential. |
issuer |
Required | The URL of the credential issuer service operated by the organisation sharing the credential. Must be the same as the value of the iss claim. |
sub |
Required | Subject. The identifier of the holder of the information in the credential. The subject identifier is a decentralised identifier did:key generated by the wallet. In the credential issuance flow, the wallet shares its did:key with the issuer, and the issuer makes this the value of the credential’s sub claim. This cryptographically binds the credential to the wallet. |
type |
Required | A set of values indicating the verifiable credential type. The first value in the set must be VerifiableCredential. |
validUntil |
Required | The date and time the credential stops being valid. This value specifies the date until which the information within the credentialSubject property remains valid. In the example above, the values of expiryDate and validUntil are the same. Must be expressed in ISO 8601 format with seconds (YYYY-MM-DDTHH:mm:ssZ) as per the VC data model v2.0. |
description |
Optional | Issuer-specified credential description. |
iat |
Optional | Issued at. The time the JWT was issued. Must be expressed in epoch time as per the IETF RFC 7519. |
name |
Optional | Issuer-specified credential name. |
validFrom |
Optional | The date and time the credential becomes valid. Must be expressed in ISO 8601 format with seconds (YYYY-MM-DDTHH:mm:ssZ) as per the VC data model v2.0. |
Guidance on credential expiration
Verifiable credentials issued to GOV.UK Wallet have two expiry dates:
- the technical expiry date, set using the
validUntilclaim, is when the digital version of the document is no longer valid. - the document expiry date, specified using the
expiryDateclaim, is when the physical document expires and must be renewed.
In the example above, a user has a valid fishing licence. This physical document owned by the user expires on 2028-12-10, as set by its credentialSubject.fishingLicenceRecord.expiryDate. The verifiable credential version of this credential issued to GOV.UK Wallet is only valid until 2026-04-09T22:59:59Z, as set by its validUntil claim. On 09/04/2026, this user must refresh the digital version of their fishing licence in the app. On 10/12/2028, the user must renew their fishing licence with the department who issued it.
Credential consumers must consider any verifiable credential as invalid (expired) if the validUntil date has passed, regardless of the credentialSubject.fishingLicenceRecord.expiryDate. Any expired credentials must be rejected by the consumer.
Guidance on including photos
If a photo is required in the credential, you must include it within the credentialSubject claim as a Base64-encoded string.
GOV.UK Wallet will validate the image to make sure that:
its format is JPG or PNG conforming to one of the following file specifications:
FF D8 FF E0 JPGFF D8 FF EE JPGFF D8 FF DB JPG89 50 4E 47 0D 0A 1A 0A PNGFF D8 FF E0 00 10 4A 46 49 46 00 01 JFIF
it does not exceed 1 MiB (1 Mebibyte = 1,048,576 bytes) in size before encoding to Base64
its EXIF metadata has been stripped
If GOV.UK Wallet fails to process an image in a credential, a credential_failure notification will
be sent, following the notification specification.
This is an example of a claim representing a PNG image, encoded in Base64 format:
"photo": "iVBORw0KGgoAAAANSUhIAAAAE2BViAAAA[...]" // PNG file as source
This is an example of a claim representing a JPEG image, encoded in Base64 format:
"photo": "/9j/4AAQSkZJRgABAQpDYXQwMy5qcGf/[...]" // JPG file as source
Signature
The credential must be signed with your credential issuer’s private signing key using the ECDSA (Elliptic Curve Digital Signature Algorithm) cryptographic algorithm with P-256 (also known as Secp256r1) elliptic curve.
Error response format
If the credential request could not be processed successfully, the credential issuer must return an appropriate HTTP error status code.
When the credential request does not include an access token or the access token is invalid, your credential endpoint must return a 401 Unauthorized HTTP status code and include the WWW-Authenticate response header field as defined in RFC6750, specifying the Bearer authentication scheme.
This is an example error response to a request with no access token:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Cache-Control: no-store
This is an example error response to an authentication attempt using an invalid access token:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"
Cache-Control: no-store
When there is an issue with the request body, your credential endpoint must return a 400 Bad Request HTTP status code and a response body in JSON format containing the following parameter:
error: a case-sensitive string indicating the error -invalid_proof(the proof of possession is invalid), orinvalid_nonce(thenonceis invalid or does not match the access token’sc_nonce)
Below is an example of a credential error response when the credential request included an invalid proof of possession:
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
{
"error": "invalid_proof"
}
Further guidance on credential binding
Binding credentials to users
Because each GOV.UK Wallet instance can be uniquely identified, credentials are bound to a specific instance. GOV.UK Wallet uses a specific type of decentralised identifier (DID) called a did:key to cryptographically bind credentials.
A did:key is a DID method. The DID represents the public key of an asymmetric key pair generated when GOV.UK Wallet is installed on a device. The private key never leaves the device, whereas the did:key is shared with credential issuers and verifiers. This allows credentials to be cryptographically bound to a specific GOV.UK Wallet instance.
GOV.UK Wallet creates a did:key from a P-256 (also known as Secp256r1) elliptic curve public key.
Using the did:key format
You can use the did:key method to transfer public keys. The format of a did:key is:
did:key:<MULTIBASE_VALUE>
The <MULTIBASE_VALUE> is a multicodec identifier for the public key type followed by the compressed public key, encoded as a base58-btc multibase string. To generate this value:
- Encode the compressed public key as bytes.
- Prefix the key bytes with the curve multicodec value encoded as unsigned varint (variable length integers). The multicodec hexadecimal value of a P-256 elliptic curve public key is
0x1200; in varint-encoded bytes that is[0x80, 0x24]. - Encode the above with base58-btc and then prefix it with
zto indicate the base58-btc encoding.
This is an example of a valid P-256 did:key:
did:key:zDnaeSGfSQMYvnLbLWEubhhGDPoq7pA9MMNvumvbsmMCZovUR
DIDs generated from a P-256 public key always start with zDn.