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

# Direct Issuance

> Issue AIR credentials directly from a script or backend job — resolve the user, sign the VC, encrypt to the holder, and store in DStorage without a live issuer.

**Direct Issuance** lets your **backend job or a controlled script** issue a credential straight to a user's AIR Account without hosting a live issuer backend and without any action from the user at issuance time. The user doesn't need to be in a session, open a wallet, or interact with any UI. The issuer holds its own keys and data throughout, signing and encrypting each credential before it reaches AIR.

This enables **zero-friction, invisible credential issuance** for use cases like:

* Issuing a KYC credential the moment a user passes identity verification
* Issuing an attendance credential when a venue scan detects a fan's check-in
* Issuing a subscriber tier credential at the end of each billing cycle
* Issuing a loyalty credential triggered by a purchase event

The user simply benefits from the credential next time they interact with a verifier.

<Info>
  **Direct issuance vs hosted Issuer Backend**

  Use direct issuance when you don't want to run a live backend. If you need queryable available credentials and on-demand issuance triggered through the SDK, use the [hosted Issuer Backend](/airkit/usage/credential/issuing-credentials) instead. Both paths sign with issuer-controlled keys and store an encrypted envelope in DStorage.
</Info>

<Note>
  Direct issuance is not a single turnkey API call. You run it through the **`air-issuer-service`** tool — either as a job that reads a CSV of recipients, or as your own script replicating the same logic. Under the hood it calls the AIR issuance endpoints (`initialize-user`, then `dstorage/vcs`); see the [Issuance API Reference](/airkit/usage/credential/issuance-api).
</Note>

<Warning>
  The bundled CSV runner uses `initialize-user` compatibility contract (`x-partner-id`, `{ partnerJwt }`, and the recipient email inside the JWT). Do not copy that bootstrap request into a new integration. Custom jobs should use the current v1 contract documented below: send a Partner JWT with `scope: "issue"` in `x-partner-auth`, and put the recipient email in the request body.
</Warning>

## How it works

Custom jobs using the current v1 API follow this flow. The bundled CSV runner performs the same issuance steps but uses the legacy v2 bootstrap request described above.

1. A backend event fires — KYC passed, purchase confirmed, check-in scanned — or your custom batch begins.
2. Your job loads its environment: issuer signing keys and DID, schema and program configuration, Partner auth, DStorage config, and source data access.
3. Your job calls `initialize-user` with the recipient's email to resolve or create the user's AIR Account, receiving the user DID and public key. The email is passed here in the request body — not in the Partner JWT.
4. Your job retrieves the user's source data.
5. Your job builds the VC, signs it with issuer-controlled keys (`BJJ_SIG_2021`), and encrypts it to the returned user public key.
6. Your job stores the encrypted VC envelope via `dstorage/vcs` and records the returned `storagePath`.
7. The user presents the credential later at any verifier; no action needed at issuance time.

The issuer holds the data and keys throughout; AIR stores only the encrypted envelope and metadata.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Script as Custom issuer job
    participant AIR as AIR API
    participant DS as Issuer Data Source
    participant Chain as Moca Chain API (DStorage)
    Script->>Script: Load env vars (issuer keys, DID, schema, Partner auth, DStorage config)
    Script->>AIR: POST /auth/initialize-user { email }
    alt Existing AIR account
        AIR-->>Script: { userId, did, publicKey, status: existing }
    else New / unresolved account
        AIR->>AIR: Create or initialize AIR account
        AIR-->>Script: { userId, did, publicKey, status: new }
    end
    Script->>DS: Retrieve user's source data
    DS-->>Script: User credential data
    Script->>Script: Build VC
    Script->>Script: Sign VC with issuer-controlled key
    Script->>Script: Encrypt VC to user's publicKey
    Script->>Chain: POST /dstorage/vcs { holderDid, schemaId, expiresAt, data, iv, authTag, encryptedKey, externalId }
    Chain-->>Script: 201 { storagePath }
    Script->>Script: Record storagePath / issuance result
```

## Prerequisites

Before running direct issuance you need:

1. **Credential services enabled** — Generate your Issuer DID with `air-issuer-service` and register it with AIR to unlock issuer functionality. See the activation flow in [Issuing Credentials](/airkit/usage/credential/issuing-credentials).
2. **Partner ID and Issuer DID** — Partner ID from the Developer Dashboard (Accounts → General). Your Issuer DID is derived from your own signing keys; AIR does not generate it for you.
3. **Issuance program** — A published credential program in the dashboard (Issuer → Programs).
4. **JWKS endpoint** — A public URL serving your public key for JWT verification. See [Partner Authentication](/airkit/usage/partner-authentication).

## Auth for custom v1 jobs

Custom v1 direct issuance authenticates via a **Partner JWT** attached to each request, signed with `scope: "issue"`. The JWT does **not** carry an `email` claim — the recipient is identified when you call `initialize-user`. For setup, key generation, and examples see [Partner Authentication](/airkit/usage/partner-authentication).

## Recipient email for custom v1 jobs

For the v1 contract, the recipient's email is the routing key that determines which AIR Account the credential lands in. Supply it in the `initialize-user` request body, not the JWT. Because it is the routing key:

* Resolve the recipient's email from the triggering event or CSV row; never hardcode a shared value.
* Never reuse a partner, service, or admin email as a recipient.
* Dedupe by recipient email + program ID before issuing, so an event replay or loop can't fan out to the wrong account.

## Next Steps

* [Issuance API Reference](/airkit/usage/credential/issuance-api)
* [Partner Authentication](/airkit/usage/partner-authentication)
* [Issuing Credentials (Hosted Issuer Backend)](/airkit/usage/credential/issuing-credentials)
