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
SEEDplaceholder 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
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 thecredentialSubject. 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:
- 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.
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:
200:
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:
- Confirm the Issuer DID in the Dashboard matches
GET /.well-known/issuer-did. - Confirm
GET /readyreturns"status": "ready"through the public URL. - Keep
API_KEYidentical to the value you registered with AIR.
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:- Open the Sandbox Developer Dashboard.
- Go to Issuer → Schemas.
- Create a schema with a string attribute named
historical_amount. - Publish the schema.
- Record its schema ID, type, schema JSON URL, and JSON-LD context URL.
- Go to Issuer → Programs and create an issuance program using the published schema.
- Select
BJJ_SIG_2021as the signature type. - Record the issuance program ID.
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
/available-vc and /issue-vc can resolve its
schemaId:
src/issuer/schemas/index.ts
- 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.
Issuer endpoint contract
Your frontend asks your issuer backend for a claimable credential. The backend usesPOST /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:
POST /issue-vc accepts the selected schema:
- Generates the credential data.
- Sets
credentialSubject.idtoholderDID. - Signs the W3C credential with the issuer identity derived from
SEED. - Encrypts the complete credential to
pubKey. - Persists the issuance record.
- Uploads the encrypted credential to DStorage.
- Returns HTTP
200with an empty response body.
x-api-key: <API_KEY>.
Step 6: Configure Partner authentication
Install AIR Kit in your web application: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 akeys 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 withscope: "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
issueCredential:
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:- Add the web origin under Dashboard → Domains.
- Confirm the Dashboard Issuer DID matches the backend DID.
- Confirm the issuance program uses the same schema registered in the issuer backend.
- Confirm the issuer backend
API_KEYis configured and reachable from your web app’s backend.
- 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.
/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 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 JWKS URL is public HTTPS and returns
{ "keys": [...] }. - JWT
kidexactly matches a JWKS key. - JWT
algmatches the JWKSalg. - The private and public keys belong to the same key pair.
- The token contains
scope: "issue"and has not expired.
Frontend cannot obtain credentialSubject
Confirm your web app calls your issuer backend (not AIR) to fetch the claimablecredentialSubject, 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, andschemaUrlmatch the published Dashboard schema.- Claim names and types match the schema.
expirationuses Unix seconds.- Your data does not include
credentialSubject.id.