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

# Issuing a Credential

> Mint a signed verifiable credential in a single API call.

## Overview

This guide walks through issuing a credential with `POST /v1/credentials`. You'll learn what goes in the request body, what comes back, and how the four credential types differ.

## Prerequisites

* A Beltic API key with `credentials:write` permission
* A subject (the entity the credential is about) — an organisation, person, agent, or document
* Knowledge of which `credential_type` you want to issue

## Choosing a Credential Type

| Type                  | When to use                                                                                                      |
| --------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `business`            | Attest that an organisation has been KYB-verified — registered, beneficial owners disclosed, sanctions cleared.  |
| `user`                | Attest a fact about a person — KYC status, age, jurisdiction, trust tier.                                        |
| `agent_authorization` | Authorize an AI agent to act on behalf of a principal within explicit, resource-scoped limits.                   |
| `outcome_attestation` | Workflow-attested credentials carrying generic claims — useful for compliance, audit, or workflow-block outputs. |

## Step 1: Build the Request

The request body has three required fields plus a few optional ones:

```json theme={null}
{
  "credential_type": "business",
  "subject": {
    "id": "biz_widgetcorp",
    "type": "organisation",
    "name": "WidgetCorp Ltd"
  },
  "claims": {
    "kyb_status": "approved",
    "verified_at": "2026-05-20T00:00:00Z",
    "jurisdiction": "US-DE"
  },
  "expires_at": "2027-05-20T00:00:00Z"
}
```

<Note>
  **British spelling matters**: `subject.type` for a company is `"organisation"`, not `"organization"`. The schema follows the W3C VC business profile.
</Note>

### Subject Shapes by Type

The required fields on `subject` and `claims` depend on `credential_type`:

<Tabs>
  <Tab title="business">
    ```json theme={null}
    {
      "credential_type": "business",
      "subject": {
        "id": "biz_acmecorp",
        "type": "organisation",
        "name": "Acme Corp"
      },
      "claims": {
        "kyb_status": "approved",
        "verified_at": "2026-05-20T00:00:00Z",
        "jurisdiction": "US-DE"
      }
    }
    ```

    Required claims: `kyb_status` (one of `pending | approved | declined | manual_review`).
  </Tab>

  <Tab title="user">
    ```json theme={null}
    {
      "credential_type": "user",
      "subject": {
        "id": "user_jane_doe_001",
        "type": "person",
        "name": { "first": "Jane", "last": "Doe" }
      },
      "claims": {
        "kyc_status": "approved",
        "trust_level": "idv_verified",
        "verified_at": "2026-05-20T00:00:00Z"
      }
    }
    ```

    Required claims: `kyc_status`, `trust_level` (one of `self_attested | liveness_verified | idv_verified | enterprise_verified`).
  </Tab>

  <Tab title="agent_authorization">
    ```json theme={null}
    {
      "credential_type": "agent_authorization",
      "subject": {
        "id": "agent_widgetcorp_assistant",
        "type": "agent",
        "name": "WidgetCorp Customer Assistant"
      },
      "claims": {
        "delegated_by_subject_id": "usr_01HXYZ...",
        "role": ["payment_agent"],
        "permissions": [
          {
            "resource_type": "wallet",
            "resource_id": "*",
            "actions": ["payment_authorize", "checkout"],
            "conditions": [
              { "field": "transaction_amount", "op": "lte", "value": 50000 },
              { "field": "transaction_currency", "op": "eq", "value": "usd" }
            ]
          }
        ],
        "spend_limit": { "amount": 50000, "currency": "usd", "period": "daily" },
        "max_idle_duration": "PT15M",
        "human_present": true
      }
    }
    ```

    Required claims: `permissions` (at least one entry with `resource_type`, `resource_id`, and `actions[]`). `delegated_by_subject_id` is required when any permission includes `resource_type: "wallet"` — it must be the `subject.id` of the verified user delegating this agent.
  </Tab>

  <Tab title="outcome_attestation">
    ```json theme={null}
    {
      "credential_type": "outcome_attestation",
      "attestation_type": "transaction_attested",
      "subject": {
        "id": "txn_9982311",
        "type": "transaction"
      },
      "claims": {
        "transaction_id": "txn_9982311",
        "amount": 25000,
        "currency": "usd",
        "approved_by_user": true,
        "attested_at": "2026-05-20T00:00:00Z"
      }
    }
    ```

    `attestation_type` is required — picks the claims sub-schema. Available types:

    | `attestation_type`          | Use case                                                                                                                                       |
    | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
    | `transaction_attested`      | Records that a transaction was approved and attested at a specific point in time                                                               |
    | `stripe_payment_authorized` | Attests a Stripe PaymentIntent was authorized via a Beltic credential — includes `payment_intent_id` and `verification_id` for AML audit chain |
    | `kyb_outcome`               | KYB verdict (approved / declined / manual\_review) with tier and reason codes                                                                  |
    | `document_validation`       | Document verification result with fraud signals and extracted fields                                                                           |
    | `identity_verification`     | IDV session outcome — provider, liveness and document check results, trust level                                                               |
    | `email_risk`                | Email risk score (0–1) with signals from a risk vendor                                                                                         |
  </Tab>
</Tabs>

## Step 2: Send the Request

```bash theme={null}
curl -X POST https://api.beltic.com/v1/credentials \
  -H "X-Api-Key: $BELTIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "credential_type": "business",
    "subject": {
      "id": "biz_widgetcorp",
      "type": "organisation",
      "name": "WidgetCorp Ltd"
    },
    "claims": {
      "kyb_status": "approved",
      "verified_at": "2026-05-20T00:00:00Z",
      "jurisdiction": "US-DE"
    }
  }'
```

## Step 3: Inspect the Response

```json theme={null}
{
  "id": "cred_01HQ7P4M6...",
  "credential_id": "cred_01HQ7P4M6...",
  "credential_type": "business",
  "subject": {
    "id": "biz_widgetcorp",
    "type": "organisation",
    "name": "WidgetCorp Ltd"
  },
  "claims": {
    "kyb_status": "approved",
    "verified_at": "2026-05-20T00:00:00Z",
    "jurisdiction": "US-DE"
  },
  "issuer_did": "did:web:beltic.com",
  "kid": "K8L9...",
  "alg": "ES256",
  "proof_format": "jwt_vc",
  "signed_payload": "eyJhbGciOiJFUzI1NiIs...",
  "status": "active",
  "status_list_index": 4287,
  "issued_at": "2026-05-21T10:30:00Z",
  "expires_at": "2026-08-19T10:30:00Z",
  "created_at": "2026-05-21T10:30:00Z",
  "updated_at": "2026-05-21T10:30:00Z"
}
```

Fields worth knowing:

* **`signed_payload`** — the JWT-VC. This is the artifact you hand to verifiers; it carries all the claims plus the signature.
* **`credential_id`** — the globally unique identifier. Stable across the credential's lifetime; use it in verify requests via `by_credential_id`.
* **`status_list_index`** — the bit slot reserved in your org's Status List 2021 bitstring. You don't need this for verification, but it's useful for audit.
* **`expires_at`** — defaults to 90 days from issuance. Override it by passing `expires_at` (ISO 8601) in the request body. Passing a date in the past returns `422`.

## Idempotency

Issue requests support Stripe-style idempotency keys via the `Idempotency-Key` header. Retrying the same request with the same key within 24 hours returns the original response without minting a new credential. Concurrent retries with the same key return `409 conflict`.

```bash theme={null}
curl -X POST https://api.beltic.com/v1/credentials \
  -H "X-Api-Key: $BELTIC_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d @payload.json
```

## Error Codes

| HTTP | Code                   | Cause                                                                                              |
| ---- | ---------------------- | -------------------------------------------------------------------------------------------------- |
| 400  | `validation_failed`    | Request body failed Zod validation (missing field, wrong enum value, British vs American spelling) |
| 401  | `unauthorized`         | API key missing, invalid, or environment-mismatched                                                |
| 403  | `forbidden`            | API key lacks `credentials:write`                                                                  |
| 409  | `idempotency_conflict` | Concurrent request with the same idempotency key                                                   |
| 422  | `unprocessable_entity` | Payload structurally valid but semantically rejected (e.g., expires\_at in the past)               |

## Next Steps

<CardGroup cols={2}>
  <Card title="Verify a Credential" icon="shield-check" href="/guides/credentials/verifying-a-credential">
    Validate the JWT-VC you just received.
  </Card>

  <Card title="Batch Issue" icon="layer-group" href="/guides/credentials/batch-issuing-credentials">
    Issue thousands of credentials in a single async job.
  </Card>
</CardGroup>
