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
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, 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
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:
- 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 all four items:
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 registered by AIR.
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:- 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 AIR-resolved partner user ID at the issuer boundary for eligibility and data retrieval.
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:
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 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:- 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 AIR has whitelisted the backend API key and both issuer endpoint URLs.
- 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 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 expectedx-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
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.
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, andschemaUrlmatch the published Dashboard schema.- Claim names and types match the schema.
expirationuses Unix seconds.- Your data does not include
credentialSubject.id.