Direct Credential Signing
Direct signing produces a signed credential in a single request, without any wallet involvement. Instead of creating a credential offer and waiting for a holder to claim it, you submit the claims and the issuer immediately returns the signed credential in compact serialized form. This is useful when the credential is bound to a passive subject (a product, a device, an asset) rather than to a wallet that can prove key possession, or when your application already has its own delivery channel for the resulting credential.
All direct-signing requests require a valid Bearer token, sent as Authorization: Bearer <token> (see Authentication).
1. Direct Signing vs an OpenID4VCI Offer
The platform supports two ways to issue a credential. Choose based on who the subject is and how the credential reaches its holder.
OpenID4VCI offer (see Issuing Credentials & Interacting with Wallets): the issuer creates a credential offer, the recipient scans a QR code or follows an
openid-credential-offer://deep link, and a wallet claims the credential. The wallet proves possession of its key, so the issued credential is cryptographically bound to the holder (acnf/ holder-binding claim). Use this for credentials issued to people who hold a wallet.Direct signing (this page): the issuer signs and returns the credential synchronously, with no wallet flow and no proof of possession. The subject is identified only by an optional
subjectDid. Use this when the subject cannot run a wallet or perform a key-binding proof — for example a Digital Product Passport bound to a product DID — or when you need the raw credential string to store or transmit yourself.
Both paths sign with the same issuer DID and produce the same credential formats. Direct signing simply skips the offer-and-claim handshake.
2. Sign a Credential
POST /issuers/:issuerDid/credentials/sign
POST /issuers/:issuerDid/credentials/sign
Authorization: Bearer <token>
Content-Type: application/jsonissuerDid(path, required): The DID of the issuer that signs the credential. Signing material is resolved from this DID, so it must be a DID the service controls (adid:webordid:keycreated via the Agent / DID endpoints). If the issuer's signing material cannot be resolved, the request fails with HTTP 400 Bad Request.
Request Body
format(string, required): The credential format to produce. One of:"sd-jwt-vc"— SD-JWT VC, supporting selective disclosure."jwt_vc_json"— JWT-VC-JSON, a W3C Verifiable Credential serialized as a JWT.
payload(object, required): The credential claims. Forsd-jwt-vc, this object must include avct(Verifiable Credential Type) string. Forjwt_vc_json, an optionaltypefield (string or array of strings) sets the credentialtypevalues.subjectDid(string, optional): The DID of the credential subject. Forsd-jwt-vcit is written as thesubclaim; forjwt_vc_jsonit becomescredentialSubject.id. This identifies the subject only — it does not create holder binding, since direct signing involves no proof of key possession.disclosureFrame(object, optional): Selective-disclosure configuration for SD-JWT. An object with a single key:_sd(array of strings, required, at least one entry): the claim keys that should be made selectively disclosable.
disclosureFrameis only valid whenformatis"sd-jwt-vc". Supplying it withjwt_vc_jsonis rejected with HTTP 400 Bad Request.
Response Body
credential(string): The signed credential in compact serialized form. For SD-JWT this is the issuer-signed JWT followed by~-separated disclosures; for JWT-VC-JSON it is a compact JWT.format(string): The claim format of the returned credential —"dc+sd-jwt"for an SD-JWT VC, or"jwt_vc"for a JWT-VC-JSON. These are the DIF claim-format identifiers, which differ from theformatvalue you send in the request ("sd-jwt-vc"/"jwt_vc_json").
3. SD-JWT with Selective Disclosure
For sd-jwt-vc, the payload carries the claims and disclosureFrame._sd lists which of those claims the holder can later disclose individually. Claims not listed in _sd are always present in the credential; listed claims are hashed into the SD-JWT so that, at presentation time, the subject can reveal them one by one without exposing the rest.
The service requires a vct in the payload and stamps an iat (issued-at) timestamp for you; you normally do not include iat in the payload (a value you supply there would override the default).
Example Request
Example Response
In this example, vct is fixed and present in every presentation, while employee_id, full_name, department, and access_level become selectively disclosable: a verifier asking only for department receives just that claim.
If payload.vct is missing or empty, the request is rejected with HTTP 400 Bad Request and the message payload.vct is required for sd-jwt-vc format.
4. JWT-VC-JSON
For jwt_vc_json, the credential is built as a W3C Verifiable Credential and serialized as a JWT, signed with the issuer DID using EdDSA. The type field in payload sets the credential type array; VerifiableCredential is always included automatically. The remaining payload fields become credentialSubject claims, and subjectDid (if supplied) becomes credentialSubject.id. A reserved id key inside payload is not copied into the subject claims.
disclosureFrame is not applicable to this format and must be omitted.
Example Request
Example Response
5. Errors
The endpoint validates the request body and the signing context, returning HTTP 400 Bad Request in these cases:
The body fails validation (for example an unknown
format, a missingpayload, or adisclosureFrame._sdthat is not a non-empty array of strings).formatis"sd-jwt-vc"butpayload.vctis missing or empty.formatis"jwt_vc_json"butdisclosureFramewas supplied (disclosureFrame is only supported for sd-jwt-vc format.).The signing material for the issuer DID cannot be resolved (the DID is unknown to the service or has no usable key).
Errors follow the global error shape: { statusCode, message, path, timestamp }.
Security
Direct signing produces a credential with no holder-binding proof, so the issuer is fully responsible for the correctness of the claims and the subject it names. Protect this endpoint accordingly: it requires a valid OIDC JWT Bearer token, and it should only be called by trusted backend systems that have already validated the data they are signing into a credential.
Last updated