Skip to main content
This guide implements interactive credential issuance from end to end. Your web app asks your issuer backend for a claimable credentialSubject, then starts the flow with AIR Kit via issueCredential. Your issuer backend remains authoritative over the claims it produces. New issuance programs should use the BJJ_SIG_2021 proof type.
This guide covers user-initiated SDK issuance through a self-hosted issuer backend. To issue from a backend event with no user session, see On-demand issuance.

Before you start

You need:
  • Node.js 18 or later
  • A database and a hosted issuer backend (or local Postgres plus an HTTPS tunnel). For the fastest free sandbox path, see Host an issuer backend and database.
  • An AIR Developer Dashboard account and Partner ID
  • The hosted AIR issuer service starter supplied during issuer onboarding
  • Two public HTTPS origins, or HTTPS tunnels, for your issuer backend and web app
  • Contact to the AIR team for issuer activation

Critical setup checkpoints

These requirements block successful issuance if skipped:
  • Set the issuer seed first. Replace the SEED placeholder before generating the issuer DID. Changing the seed later produces a different DID.
  • Complete AIR activation. AIR must register your Issuer DID, API key, and Partner ID before credential services are enabled for your account.
  • Publish and register your JWKS. Host it at a public HTTPS URL, register the exact URL in the Developer Dashboard, and sign Partner JWTs with a matching kid. See JWKS endpoint setup.

What you will build

By the end of this guide, you will have:
  • A public issuer backend with POST /available-vc, POST /issue-vc, and credential-status endpoints
  • An issuer DID derived from a secret seed
  • 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
The public examples below are useful implementation companions, but the steps on this page contain the required integration contract:

Credential issuance service

A production-oriented AIR issuer backend.

AIR E2E Example

A monorepo that includes both the backend and frontend implementations, showcasing the complete end-to-end integration as a single, cohesive example.

How SDK issuance works

Your frontend calls your issuer backend to obtain the credentialSubject. The backend uses POST /available-vc (or equivalent logic) to derive the claims. The frontend then passes that subject into air.issueCredential. Your backend remains authoritative over what it produces; do not let the browser invent claim values. 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 issuer and partner secrets

Generate a 32-byte issuer seed and separate API keys:
SEED deterministically controls your issuer DID. Store it in a secret manager, back it up, and do not rotate it for an existing issuer. Generate an RSA key pair for Partner JWT signing:
You will use the same key pair in two places:
  • The issuer backend signs requests to DStorage with the private key.
  • Your web server signs short-lived AIR Kit Partner JWTs with the private key and exposes the public key through JWKS.
Never expose the private key or issuer seed 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. Install dependencies, apply the database migrations, and start the service: Check that the database, migrations, and issuer identity are ready:
A ready service returns HTTP 200:
Retrieve the issuer DID:
The DID is derived from SEED and the three IDEN3_* values:
ISSUER_ORIGIN must be the public HTTPS origin that serves /credential-status/:nonce. This URL is embedded in every issued credential.

Step 3: Register the issuer with AIR

Expose the issuer backend over public HTTPS, then provide the Moca Network / AIR team with: For example:
After AIR activates the issuer:
  1. Confirm the Issuer DID in the Dashboard matches GET /.well-known/issuer-did.
  2. Confirm GET /ready returns "status": "ready" through the public URL.
  3. Keep API_KEY identical to the value you registered with AIR.
Your frontend and internal jobs call your issuer endpoints with x-api-key. If the key is missing or incorrect, the backend returns 403.

Step 4: Create schema for credential issuance

After AIR activates 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.
  5. Record its schema ID, type, schema JSON URL, and JSON-LD context URL.
  6. Go to Issuer → Programs and create an issuance program using the published schema.
  7. Select BJJ_SIG_2021 as the signature type.
  8. Record the issuance program ID.
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 and expiration your backend will issue.
src/issuer/schemas/schema-<SCHEMA_ID>.ts
Register the class so /available-vc and /issue-vc can resolve its schemaId:
src/issuer/schemas/index.ts
Follow these rules:
  • Claim keys and JavaScript types must match the published Dashboard schema.
  • Return expiration as Unix time in seconds.
  • Do not add credentialSubject.id. The issuer service sets it to the holder DID after adding your claims.
  • 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.
Setting credentialSubject.id to an email address or partner user ID causes schema validation to fail with must match format "uri". The holder DID must remain the credential subject ID.

Issuer endpoint contract

Your frontend asks your issuer backend for a claimable credential. The backend uses POST /available-vc (or equivalent logic) to derive the credentialSubject that the frontend passes into air.issueCredential. POST /issue-vc is used for On-demand issuance when the end user does not interact with AIR. POST /available-vc accepts the holder identity and optional filters:
It returns encrypted previews:
POST /issue-vc accepts the selected schema:
On success, the issuer backend:
  1. Generates the credential data.
  2. Sets credentialSubject.id to holderDID.
  3. Signs the W3C credential with the issuer identity derived from SEED.
  4. Encrypts the complete credential to pubKey.
  5. Persists the issuance record.
  6. Uploads the encrypted credential to DStorage.
  7. Returns HTTP 200 with an empty response body.
Both endpoints require x-api-key: <API_KEY>.

Step 6: Configure 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

Complete JWKS endpoint setup, including deploying the endpoint at a public HTTPS URL and registering that exact URL in the Developer Dashboard. Before continuing, verify that the endpoint returns a keys array containing 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

Create a singleton AIR service and a helper for fetching the Partner JWT:
lib/air.ts
Log in before starting issuance, then call issueCredential:
For this SDK flow, fetch credentialSubject from your issuer backend before calling issueCredential. Your backend remains authoritative over the claims — do not invent claim values in the browser without validating them against your own data.

Step 8: Complete Dashboard setup and test end to end

Complete the remaining Dashboard setup:
  1. Add the web origin under Dashboard → Domains.
  2. Confirm the Dashboard Issuer DID matches the backend DID.
  3. Confirm the issuance program uses the same schema registered in the issuer backend.
  4. Confirm the issuer backend API_KEY is configured and reachable from your web app’s backend.
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.
Check the backend issuance history:
For on-demand issuance, the backend’s /issue-vc response is intentionally empty — a successful issuance is recorded in the issuer’s history and DStorage rather than returned as a credential in that HTTP response.

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 JWKS URL is public HTTPS and returns { "keys": [...] }.
  • JWT kid exactly matches a JWKS key.
  • JWT alg matches the JWKS alg.
  • The private and public keys belong to the same key pair.
  • The token contains scope: "issue" and has not expired.
See JWKS endpoint setup for a complete diagnostic checklist.

Frontend cannot obtain credentialSubject

Confirm your web app calls your issuer backend (not AIR) to fetch the claimable credentialSubject, and that POST /available-vc returns data for the logged-in holder. Check that the issuance program points to the activated Issuer DID and schema.

Schema validation fails

Confirm that:
  • schemaId, schemaType, and schemaUrl match the published Dashboard schema.
  • Claim names and types match the schema.
  • expiration uses Unix seconds.
  • Your data does not include credentialSubject.id.

Issuer backend is unreachable

Next steps