> ## Documentation Index
> Fetch the complete documentation index at: https://docs.air3.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Issue Iden3 credentials

> Set up the Iden3 issuer service and issue BJJ credentials that holders can prove with zero-knowledge proofs.

<Note>
  **Contact the AIR team to enable them** for your partner account before you start: [Partner with us](https://air3.com/partner-with-us).
</Note>

Iden3 credentials use a BJJ\_SIG\_2021 signature and the W3C Verifiable Credentials Data Model v1.1 in JSON-LD form. Holders prove claims with Groth16 zero-knowledge proofs. To verify them, see [Verify Iden3 credentials](/products/identity/iden3-credentials).

## When to use Iden3

Choose Iden3 over SD-JWT when you need one of these:

* **Predicate proofs.** Prove a condition such as `age > 18` without revealing the value.
* **Unlinkable presentations.** Each proof is randomized, so verifiers cannot link a holder's presentations.
* **On-chain verification.** Check a proof through the Universal Verifier and Groth16 verifier contracts.

The cost is heavier proof generation on the holder's device and a verifier that implements the Iden3 proof profile. Generic W3C VC support is not enough. See [Credential formats](/products/identity/credential-formats) for the full comparison.

| Proof type | What it is |
| - | - |
| `BJJ_SIG_2021` | The issuer signs the credential with its BJJ key. Supports off-chain and on-chain verification. |

## Set up the issuer service

Iden3 issuers use the Iden3 branch of the [AIR issuer service](https://github.com/MocaNetwork/air-issuer-service/tree/main_iden3). It exposes the same `POST /available-vc` and `POST /issue-vc` contract as the SD-JWT service, with `proofType: "BJJ_SIG_2021"`.

```bash theme={null}
git clone -b main_iden3 https://github.com/MocaNetwork/air-issuer-service.git
cd air-issuer-service
npm i
```

### Generate the issuer seed

Your BJJ issuer DID is derived from a 32-byte seed:

```bash theme={null}
echo "SEED=0x$(openssl rand -hex 32)"
```

<Warning>
  `SEED` is your issuer identity. Store it in a secret manager and back it up. Losing it means you can no longer act as that issuer, and changing it creates a new issuer DID.
</Warning>

### Configure the environment

| Variable | Purpose |
| - | - |
| `NODE_ENV` | `sandbox` or your production value |
| `DATABASE_URL` | PostgreSQL connection URL |
| `ISSUER_ORIGIN` | Public HTTPS origin of the service, without a trailing slash. Used in credential status URLs |
| `SEED` | 32-byte hex seed for the BJJ issuer identity |
| `PARTNER_ID` | Your AIR Partner ID |
| `PARTNER_PRIVATE_KEY_KID`, `PARTNER_PRIVATE_KEY_ALG`, `PARTNER_PRIVATE_KEY_DER` | Partner JWT signing key, as in the [issuance quickstart](/get-started/quickstarts/issue-credentials#step-2-configure-and-start-the-issuer-backend) |
| `PARTNER_JWKS` | Public JWKS matching the partner key |
| `API_KEY` | Value AIR sends in `x-api-key` |
| `ADMIN_API_KEY` | Value your servers send in `x-admin-api-key` |

### Extract the issuer DID

Start the service. It logs the DID at startup as `Issuer DID: did:air:…`. You can also print it with the REPL:

```bash theme={null}
echo 'console.log(get(CredentialIssuingService).issuerDID.string());' | npm run repl -
```

Register the issuer in the Developer Dashboard under **Issuer → Settings**, filling in these fields:

| Field | Value |
| - | - |
| DID | The `did:air:…` issuer DID from the log or REPL |
| API key | The service's `API_KEY` |
| Available Credentials API | `{ISSUER_ORIGIN}/available-vc` |
| Issue Credential API | `{ISSUER_ORIGIN}/issue-vc` |

### Define a schema class

Create the schema in the Dashboard's [Schema Builder](/products/identity/iden3/schema-builder) and record its schema ID, type, schema JSON URL, and JSON-LD context URL. Then add a class under `src/issuer/schemas/`:

```ts src/issuer/schemas/schema-<SCHEMA_ID>.ts theme={null}
import { BaseSchema } from "./base-schema";

export default class Schema extends BaseSchema {
  public readonly schemaId = "<SCHEMA_ID>";
  public readonly schemaType = "<TYPE>";
  public readonly schemaUrl = "https://.../schema";
  public readonly schemaContextUrl = "https://.../ctx";

  async generateCredentialData(userId: string) {
    return {
      credentialSubject: {
        // keys must match the schema; do not set `id`
        someField: "...",
      },
      expiration: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
    };
  }
}
```

Register it in `src/issuer/schemas/index.ts`:

```ts src/issuer/schemas/index.ts theme={null}
import Schema1 from "./schema-<SCHEMA_ID>";

const schemas: BaseSchema[] = [new Schema1()];
export default schemas;
```

Claim keys and types must match the Dashboard schema exactly, or merklization and verification fail. The service sets `credentialSubject.id` to the holder DID.

### Issue in bulk without a user session

The Iden3 service includes a CSV runner. It resolves each email through AIR `initialize-user`, issues, encrypts, and uploads:

```csv theme={null}
email,expiration,credentialSubject.field1,credentialSubject.field2
member@example.com,2030-01-01T00:00:00+08:00,"""Hello World""",4
```

Every `credentialSubject.[field]` cell must be a JSON-stringified value, so strings carry quotes.

```bash theme={null}
npm run batch-issue-vc-csv -- \
  <SCHEMA_ID> \
  <SCHEMA_URL> \
  <SCHEMA_TYPE> \
  ./credentials-batch-1.csv
```

The runner writes a result log named `[unix_timestamp_ms].csv`.

### Revocation and status

| Method | Path | Called by | Purpose |
| - | - | - | - |
| `POST` | `/admin/revoke` | Your servers (`x-admin-api-key`) | Revoke by `revocationNonce` |
| `GET` | `/admin/issuance-history` | Your servers (`x-admin-api-key`) | Find issued credentials and their nonces |
| `GET` | `/revocation-status/:nonce` | Verifiers | `{ "isRevoked": boolean }` |
| `GET` | `/credential-status/:nonce` | Verifiers | Non-revocation Merkle proof and issuer tree state (`{ issuer }`) |

The Iden3 service embeds a `/credential-status` URL under `ISSUER_ORIGIN` in each credential, so keep the origin stable. It does not support the token status list.

### Issue both formats

A partner that issues SD-JWT and Iden3 credentials runs both services, each at its own `ISSUER_ORIGIN`. Set `IDEN3_ISSUER_DID` on the SD-JWT service to your BJJ issuer DID; it is then listed under `alsoKnownAs` in your `did:web` document.

## Related

* [Iden3 Schema Builder](/products/identity/iden3/schema-builder)
* [Iden3 schema structure](/products/identity/iden3/schema-structure)
* [Managing your Iden3 schemas](/products/identity/iden3/schema-management)
* [Verify Iden3 credentials](/products/identity/iden3-credentials)
* [Credential formats](/products/identity/credential-formats)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.