issueCredential() to open the AIR flow. The SDK handles login, credential
discovery, and user confirmation; AIR forwards issuance to your backend, which
authorizes the request, retrieves the data, creates and signs the credential,
encrypts it, and stores it in dStorage.
New issuance programs use the SD_JWT_VC proof type. This setup is self-serve
in both sandbox and production. To issue Iden3 (BJJ_SIG_2021) credentials
instead, see Iden3 credentials.
This guide covers user-initiated SDK issuance through a self-hosted issuer
backend. To issue from a backend event with no user session, see
Direct issuance.
Before you start
You need:- A supported Node.js version
- An issuer backend implementing
available-vcandissue-vc, reachable by AIR over HTTPS. You can host it or use an HTTPS tunnel during development. - An AIR Developer Dashboard account and Partner ID
- A server endpoint that signs Partner JWTs, and a public HTTPS JWKS endpoint (you set both up in Step 6)
- A web app whose origin is registered in the Dashboard
What you will build
By the end of this guide, you will have:- A public issuer backend with
POST /available-vc,POST /issue-vc, a revocation-status endpoint, and adid:webdocument - A Dashboard schema and issuance program
- A schema class that produces the claims your backend signs
- A public JWKS endpoint and a server-only Partner JWT endpoint
- A web flow that logs in with AIR Kit and calls
issueCredential
AIR issuer service (SD-JWT)
Issuer endpoints, schema logic, SD-JWT signing, encryption, revocation, and dStorage integration.
How SDK issuance works
- Your app starts
issueCredential()with the issuer and issuance program. - The user logs in to AIR if needed.
- The SDK requests available credentials through AIR. AIR calls the registered
issuer backend’s
POST /available-vcand returns encrypted previews. - The SDK decrypts and displays the previews. The user selects a credential and confirms issuance.
- The SDK requests issuance through AIR, which calls the issuer’s
POST /issue-vcwith the resolved holder identity, encryption key, holder signing key, and schema. - The issuer authorizes the request, loads authoritative claims, creates and signs the SD-JWT VC, encrypts it, and stores it in dStorage.
- The SDK reports completion to your app. If the user declines, issuance stops.
schemaId, not the program ID.
Step 1: Generate partner secrets
Clone the SD-JWT branch of the AIR issuer service, or your fork of it, then install its dependencies:.env.example when you upgrade.
Generate separate API keys for AIR-to-issuer calls and admin calls:
- The issuer backend signs the SD-JWT credentials and its requests to dStorage with the private key.
- Your web server signs short-lived AIR Kit Partner JWTs with the private key.
- The public key is published through your JWKS and the issuer’s
did:webdocument, so AIR and verifiers can check both.
NEXT_PUBLIC_* environment variable.
Step 2: Configure and start the issuer backend
Create the issuer backend environment file:PARTNER_PRIVATE_KEY_DER is the base64 body between the BEGIN PRIVATE KEY
and END PRIVATE KEY lines in partner-private.pem. It must be PKCS#8.
PARTNER_JWKS contains public keys only, matching your Partner JWT signing key
and kid. Generate and publish the document using JWKS endpoint setup.
The service serves these keys in its did:web document and issuer metadata.
If you set DATABASE_URL (this issuer service uses PostgreSQL), apply migrations before the first start:
ISSUER_ORIGIN is reachable over HTTPS, confirm the public identity routes:
did.json returns the issuer DID (did:web:issuer.example.com) with your
public keys as verification methods. Credentials are signed with header
kid set to <issuer DID>#<PARTNER_PRIVATE_KEY_KID>.
The credential
iss is the did:web DID, while /.well-known/jwt-vc-issuer
identifies the HTTPS origin. AIR verification resolves the DID document.
Some external verifiers expect these identifiers to match exactly; test with
the verifier you plan to support.Step 3: Register the issuer with AIR
Expose the issuer backend over public HTTPS. Register the issuer under Dashboard → Issuer → Settings in the sandbox or production Dashboard, then configure its endpoint URLs:
After registration:
- Confirm the Issuer DID in the Dashboard matches the
idin your/.well-known/did.json. - Confirm AIR can reach both registered issuer endpoints over HTTPS.
- Keep
API_KEYidentical to the value you registered with AIR.
x-api-key. Keep the key server-side;
your browser calls AIR Kit with a short-lived Partner JWT. A missing or incorrect
issuer API key causes the backend to return 403.
If you enable CORS on the issuer or a proxy in front of it, allow *.air3.com.
Step 4: Create schema for credential issuance
After registering your issuer, create the schema and issuance program:- Open the Sandbox Developer Dashboard.
- Go to Issuer → Schemas.
- Create a schema with a string attribute named
historical_amount. - Publish the schema and record its schema ID.
- Go to Issuer → Programs and create an issuance program using the published schema.
- Select
SD_JWT_VCas the signature type. - Record the issuance program ID.
vct) unless
your schema class sets its own. For schema design rules and supported field
types, see Schema creation.
Step 5: Implement the credential data
Create one schema class for each Dashboard schema. The class maps a Dashboard schema to the claims your backend will issue and marks which claims the holder can disclose selectively.src/issuer/sd-jwt-vc-schemas/schema_<SCHEMA_ID>.ts
/available-vc and /issue-vc can resolve its
schemaId:
src/issuer/sd-jwt-vc-schemas/index.ts
- Claim keys and JavaScript types must match the published Dashboard schema.
- Do not return reserved claims from
generateCredentialData:id,nonce,vct,sub,exp,cnf,iss,iat, orstatus. The service sets them. expirySecsets the credential’sexp. Theexpirationyou return is only used for the preview.- List a claim in
disclosureFrame._sdonly if verifiers may request it on its own. Claims outside_sdare always visible in a presentation. - Keep the backend authoritative. Do not sign claim values supplied by the browser without validating them against your own data.
- When you replace the static example with a database or API lookup, use the partner user ID at the issuer boundary for eligibility and data retrieval.
Issuer endpoint contract
AIR calls both issuer endpoints during on-demand issuance:POST /available-vc
provides encrypted previews, and POST /issue-vc performs issuance after the user
confirms. The issuer validates eligibility using the AIR-resolved partner user
identifier and retrieves claims from its own data source.
Issuer endpoint requests and responses
Issuer endpoint requests and responses
POST /available-vc accepts the holder identity and optional filters:POST /issue-vc accepts the selected schema and the holder’s signing key:- Generates the credential data.
- Sets
subtoholderDIDand, whensigningKeyis present,cnfto the holder’s public key. This binds the credential to the holder. - Signs the SD-JWT VC with your partner key under the
did:webissuer DID. - Encrypts the credential to
pubKey. - Persists the issuance record when database persistence is enabled.
- Uploads the encrypted credential to dStorage.
- Returns HTTP
201with an empty response body.
x-api-key: <API_KEY> to both endpoints. This issuer HTTP response is
distinct from the SDK result returned to your app.Step 6: Install AIR Kit and set up Partner authentication
Install AIR Kit in your web application:Set up the JWKS endpoint
Issuance fails until AIR can fetch your JWKS. Follow JWKS endpoint, then check that the registered URL returns thekid your Partner JWT will use.
Sign the Partner JWT
Implement the Next.js Partner JWT endpoint. The endpoint must generate a short-lived token on the server withscope: "issue" and a kid that appears in your registered JWKS.
Step 7: Initialize AIR Kit and issue the credential
The frontend examples use Next.js. Create a singleton AIR service and a helper for fetching the Partner JWT. Call these helpers from a browser event handler.lib/air.ts
issueCredential:
{} as credentialSubject: the AIR credential UI fetches encrypted
previews through AIR and the issuer backend supplies the authoritative claims.
The SDK promise resolves on success and rejects on failure. It does not return the VC or a dStorage path. Handle the SDK result separately from backend storage records.
Step 8: Test end to end
Add your web app’s origin under Account → Domains. Start the issuer backend and web app, then:- Open the web app.
- Log in through AIR Kit.
- Start credential issuance.
- Confirm the AIR Credential UI shows the available credential.
- Approve issuance.
- Confirm the app receives a successful
issueCredentialresult.
Troubleshooting
Issuer backend returns 403
The caller is not sending the expectedx-api-key, or the value differs from the
backend API_KEY. Confirm the Partner ID, Issuer DID, and API key match what you
registered with AIR.
AIR cannot validate the Partner JWT
Confirm the token containsscope: "issue" and has not expired. For JWKS and
kid checks, see JWKS endpoint troubleshooting.
No credential preview appears
Confirm AIR can call the registeredPOST /available-vc endpoint and that the
backend returns an encrypted preview for the logged-in holder. Check the
program’s issuer DID, schema, proof type, and the user’s eligibility.
Schema validation fails
Confirm that:schemaIdmatches the published Dashboard schema.- Claim names and types match the schema.
- Your data does not include reserved claims such as
id,sub,vct, orcnf.
Issuer backend is unreachable
Check that the service is running, both issuer endpoint URLs are registered, and your HTTPS domain or tunnel is reachable by AIR. If a development tunnel URL changes, updateISSUER_ORIGIN and the registered URLs. This also changes the
issuer DID, so only do it before you issue credentials you want to keep.