Skip to main content
This guide implements interactive credential issuance from end to end. Your web app starts the flow with AIR Kit, AIR resolves the holder, and your issuer backend creates, signs, encrypts, and stores the credential. New issuance programs should use the BJJ_SIG_2021 proof type.
This guide covers user-initiated issuance through a hosted issuer backend. To issue from a script or backend job without a user session, use Direct Issuance.

Before you start

You need:
  • Node.js 18 or later
  • A 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 and backend whitelisting

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, Partner ID, and issuer endpoint URLs before the Credential API will call your backend.
  • 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 hosted issuance works

The browser never calls /available-vc or /issue-vc directly. AIR calls these endpoints with x-api-key and supplies the holder DID, encryption public key, and partner user ID. The issuance program ID is an AIR Kit and Dashboard identifier. Your issuer backend receives 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 all four items: 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 registered by AIR.
Until whitelisting is complete, AIR may not route requests to your backend, or the backend will return 403 because x-api-key is missing or incorrect.

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 AIR-resolved 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

AIR calls the issuer endpoints. Your frontend does not call them. POST /available-vc accepts the AIR-resolved holder 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 hosted flow, credentialSubject is part of the SDK request, but it is not the signed source of truth. AIR calls your issuer backend, and the registered backend schema class supplies the claims that are signed into the credential.

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 AIR has whitelisted the backend API key and both issuer endpoint URLs.
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:
The backend’s /issue-vc response is intentionally empty. A successful issuance is delivered through AIR and DStorage rather than returned as a credential in that HTTP response.

Troubleshooting

Issuer backend returns 403

AIR is not sending the expected x-api-key, or the value differs from the backend API_KEY. Ask the Moca Network / AIR team to confirm the Partner ID, Issuer DID, API key, and endpoint whitelist.

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.

AIR does not call the issuer backend

Confirm the issuance program points to the activated Issuer DID and schema. Your web app shoudl not call your backend directly, so you will not see issuer requests until AIR routing and whitelisting are complete.

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