Skip to main content
This guide implements on-demand issuance with the user present. Your app calls 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-vc and issue-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
A database is optional in sandbox. For development and testing, credential data can come from an existing API or service, or you can use static sample data. For production, however, if you need to support features such as credential revocation, issuance history, and token status lists, especially at scale with many users, you will need to persist the relevant data in your own database. Regardless of whether you use a database, encrypted credentials are stored in dStorage. The backend and JWKS endpoint can share a host. See Issuer backend hosting.
Choose a stable public origin for the issuer service before you issue. The issuer DID is did:web:<host of ISSUER_ORIGIN>, so changing the host later changes your issuer identity.

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 a did:web document
  • 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

  1. Your app starts issueCredential() with the issuer and issuance program.
  2. The user logs in to AIR if needed.
  3. The SDK requests available credentials through AIR. AIR calls the registered issuer backend’s POST /available-vc and returns encrypted previews.
  4. The SDK decrypts and displays the previews. The user selects a credential and confirms issuance.
  5. The SDK requests issuance through AIR, which calls the issuer’s POST /issue-vc with the resolved holder identity, encryption key, holder signing key, and schema.
  6. The issuer authorizes the request, loads authoritative claims, creates and signs the SD-JWT VC, encrypts it, and stores it in dStorage.
  7. The SDK reports completion to your app. If the user declines, issuance stops.
The browser does not need the issuer API key or a separate request to fetch claims. See Architecture and data flow for the sequence diagram. The issuance program ID is an AIR Kit and Dashboard identifier. Your issuer backend works with a 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:
Check the branch README and .env.example when you upgrade. Generate separate API keys for AIR-to-issuer calls and admin calls:
Generate an RSA key pair for Partner JWT signing:
You will use the same key pair in three places:
  • 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:web document, so AIR and verifiers can check both.
Never expose the private key through a 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:
Start the service:
Once 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:
  1. Confirm the Issuer DID in the Dashboard matches the id in your /.well-known/did.json.
  2. Confirm AIR can reach both registered issuer endpoints over HTTPS.
  3. Keep API_KEY identical to the value you registered with AIR.
AIR calls these issuer endpoints with 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:
  1. Open the Sandbox Developer Dashboard.
  2. Go to Issuer → Schemas.
  3. Create a schema with a string attribute named historical_amount.
  4. Publish the schema and record its schema ID.
  5. Go to Issuer → Programs and create an issuance program using the published schema.
  6. Select SD_JWT_VC as the signature type.
  7. Record the issuance program ID.
For SD-JWT credentials, the schema ID is the credential type (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
Register the instance so /available-vc and /issue-vc can resolve its schemaId:
src/issuer/sd-jwt-vc-schemas/index.ts
Follow these rules:
  • 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, or status. The service sets them.
  • expirySec sets the credential’s exp. The expiration you return is only used for the preview.
  • List a claim in disclosureFrame._sd only if verifiers may request it on its own. Claims outside _sd are 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.
POST /available-vc accepts the holder identity and optional filters:
It returns encrypted previews:
POST /issue-vc accepts the selected schema and the holder’s signing key:
On success, the issuer backend:
  1. Generates the credential data.
  2. Sets sub to holderDID and, when signingKey is present, cnf to the holder’s public key. This binds the credential to the holder.
  3. Signs the SD-JWT VC with your partner key under the did:web issuer DID.
  4. Encrypts the credential to pubKey.
  5. Persists the issuance record when database persistence is enabled.
  6. Uploads the encrypted credential to dStorage.
  7. Returns HTTP 201 with an empty response body.
AIR sends 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:
Add the browser-safe and server-only values to your web environment:

Set up the JWKS endpoint

Issuance fails until AIR can fetch your JWKS. Follow JWKS endpoint, then check that the registered URL returns the kid 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 with scope: "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
Log in before starting issuance, then call issueCredential:
Pass {} 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:
  1. Open the web app.
  2. Log in through AIR Kit.
  3. Start credential issuance.
  4. Confirm the AIR Credential UI shows the available credential.
  5. Approve issuance.
  6. Confirm the app receives a successful issueCredential result.
When database persistence is enabled, you can also check the backend issuance history:
Without database persistence, an empty history is not a failed issuance. Confirm that the SDK completes successfully and that the holder’s credential can be discovered and used in the verification flow.

Troubleshooting

Issuer backend returns 403

The caller is not sending the expected x-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 contains scope: "issue" and has not expired. For JWKS and kid checks, see JWKS endpoint troubleshooting.

No credential preview appears

Confirm AIR can call the registered POST /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:
  • schemaId matches the published Dashboard schema.
  • Claim names and types match the schema.
  • Your data does not include reserved claims such as id, sub, vct, or cnf.

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, update ISSUER_ORIGIN and the registered URLs. This also changes the issuer DID, so only do it before you issue credentials you want to keep.

Next steps