# Create Business Lookup Source: https://docs.beltic.com/api-reference/endpoint/businesses-create POST /v1/businesses Create a new business registry lookup request. This will queue a task to fetch business information from country-specific vendor APIs. **Processing Modes:** - **Async Mode** (`async: true`): Returns immediately with `201 Created` and processing status. Client should poll the GET endpoint for completion. - **Sync Mode** (`async: false`): Waits up to 45 seconds for completion. - If processing completes within 45 seconds: Returns `200 OK` with full business data. - If timeout occurs: Returns `201 Created` with processing status (client should poll for completion). **Country Requirements:** - **US**: Requires `country`, `official_name`, and `address` - **BR**: Requires `country` and `registration_number` - **GB**: Requires `country` and `registration_number` - **ZZ**: Test country, requires `country` and at least one input field (`official_name`, `registration_number`, or `address`) **Workflow:** 1. Call this endpoint to create a business lookup task 2. The task is queued and processed by a country-specific service 3. Poll the GET endpoint (or wait in sync mode) to retrieve results **Note:** The business record is created immediately with `async_status: processing`. The record will be updated with results when processing completes. # Get Business Source: https://docs.beltic.com/api-reference/endpoint/businesses-get GET /v1/businesses/{id} Retrieve a business record by ID. Returns the current state of the business lookup, including processing status and any results. **Status Values:** - `processing`: Lookup is still in progress - `succeeded`: Lookup completed successfully with business data - `not_found`: Business was not found in the registry - `canceled`: Lookup was canceled due to an error **Response Format:** - If status is `succeeded`, `data.attributes` contains all business information - If status is `processing`, `data.attributes` may be empty or partial - `data.meta` contains `async_status`, `created_at`, `search_parameters`, and optionally `errors` # List Businesses Source: https://docs.beltic.com/api-reference/endpoint/businesses-list GET /v1/businesses List business records with pagination and filtering support. **Filters:** - `filter[async_status]`: Filter by processing status (processing, succeeded, not_found, canceled) - `filter[country]`: Filter by country code (e.g., US, BR) **Pagination:** - `page[size]`: Number of records per page (default: 15) - `page[after]`: Cursor for next page - `page[before]`: Cursor for previous page - `page[total]`: Include total count in response **Example:** ``` GET /v1/businesses?filter[async_status]=succeeded&filter[country]=US&page[size]=10 ``` # Record an Audit Event Source: https://docs.beltic.com/api-reference/endpoint/credentials-audit-events-create POST /v1/audit/events # List Audit Events Source: https://docs.beltic.com/api-reference/endpoint/credentials-audit-events-list GET /v1/audit/events # Create a Webhook Stream Source: https://docs.beltic.com/api-reference/endpoint/credentials-audit-streams-create POST /v1/audit/streams # Delete a Webhook Stream Source: https://docs.beltic.com/api-reference/endpoint/credentials-audit-streams-delete DELETE /v1/audit/streams/{id} # List Webhook Streams Source: https://docs.beltic.com/api-reference/endpoint/credentials-audit-streams-list GET /v1/audit/streams # Batch Issue Credentials Source: https://docs.beltic.com/api-reference/endpoint/credentials-batch-issue POST /v1/credentials/batch-issue Issue up to 1,000 credentials in a single call. Useful for bulk provisioning — for example, minting agent authorization credentials for an existing fleet of agents in one request rather than 1,000 individual calls. Each item is processed sequentially. If an item fails, processing continues — the response reports per-item success/failure in the same order as the input array. Each item may include a `correlation_id` which is echoed back so callers can match results to inputs. Limits: - Maximum 1,000 items per request - No transactional rollback — failures don't undo successful items # Issue a Credential Source: https://docs.beltic.com/api-reference/endpoint/credentials-create POST /v1/credentials Issue a new credential of one of four types — `business`, `user`, `agent_authorization`, or `outcome_attestation` — discriminated by the `credential_type` field in the request body. Returns a fully signed JWT-VC in `signed_payload` alongside the persisted credential resource. The JWT follows the W3C Verifiable Credentials 2.0 profile, is signed with ES256, and includes a Status List 2021 entry so it can be verified and revocation-checked by any compliant verifier. # Delete a Credential Source: https://docs.beltic.com/api-reference/endpoint/credentials-delete DELETE /v1/credentials/{id} Remove a credential from the database entirely. Reserved for rare cases like GDPR Article 17 erasure requests or credentials issued in error before any use. **The audit trail for this credential is destroyed.** The status list bit stays flipped — bit slots are not reused once allocated. For normal lifecycle management, use `POST /:id/revoke` instead. Revoke preserves the full audit trail. # Get a Credential Source: https://docs.beltic.com/api-reference/endpoint/credentials-get GET /v1/credentials/{id} Fetch a single credential by its public ID (`cred_`). Returns the full flat credential resource, including the signed JWT in `signed_payload`. # List Credentials Source: https://docs.beltic.com/api-reference/endpoint/credentials-list GET /v1/credentials List credentials in the calling org, with cursor-based pagination. Optionally filter by `subject_id`, `developer_id`, `credential_type`, `attestation_type` (for OutcomeAttestation), or `status`. # Revoke a Credential Source: https://docs.beltic.com/api-reference/endpoint/credentials-revoke POST /v1/credentials/{id}/revoke Flip the credential's status_list_index bit to 1, mark status=revoked, record revoked_at. Audit trail is preserved (use `DELETE` for GDPR erasure). Verifiers fetching the status list at `/.well-known/status-lists/v1` will see the revocation within seconds. Returns 409 if the credential is not currently active (already revoked, expired, or suspended). # Verify a Credential Source: https://docs.beltic.com/api-reference/endpoint/credentials-verify POST /v1/credentials/verify Verify a presented credential and return a structured outcome. The verifier runs a deterministic pipeline against the JWT-VC: parse, resolve the issuer's signing key from the published JWKS, check the ES256 signature, validate the standard claims (`iat` / `exp` / `nbf`, and `audience` if provided), confirm the payload matches the credential-type schema, look up the live revocation status, and — for agent credentials — evaluate the authorization policy against the supplied request context. Verification outcomes (revoked, expired, signature mismatch, policy denied, etc.) return HTTP 200 with `{ valid: false, reason }` so callers can branch on outcome without try/catching transport errors. Only infrastructure failures use the error envelope. # DID Document Source: https://docs.beltic.com/api-reference/endpoint/credentials-well-known-did GET /.well-known/did.json W3C DID document for the Beltic issuer. Verifiers following the did:web resolution method fetch this to confirm the issuer identity and retrieve verification methods. Each `verificationMethod` entry corresponds to a current public signing key. `Cache-Control: max-age=3600`. # JSON Web Key Set Source: https://docs.beltic.com/api-reference/endpoint/credentials-well-known-jwks GET /.well-known/jwks.json Public key directory for Beltic-issued credentials. Verifiers fetch this to validate JWT signatures. Supports multiple keys to handle rotation: keys marked `active` are used for new signatures, while `next` and `retired` keys remain available to verify credentials signed by prior keys. `Cache-Control: max-age=3600` — verifiers should cache. # Status List 2021 Bitstring Source: https://docs.beltic.com/api-reference/endpoint/credentials-well-known-status-list GET /.well-known/status-lists/v1 # Create Document Template Source: https://docs.beltic.com/api-reference/endpoint/document-templates-create POST /v1/document-templates Create a new document template to standardize document processing workflows. Templates define reusable configurations including extraction schemas and fraud detection rules. **Template Components:** - **Schema**: Defines the expected structure of extracted data - **Fraud Config**: Pipeline configuration for fraud detection. Set `requires_digital_signature: true` when documents processed with this template must contain a signature; if no signature is detected, the fraud result includes a missing-signature RISK indicator. - **Validation Fields**: Date checks should be represented as boolean fields with `beltic:validation` **Validation Field Pattern:** - Keep the source date field as `type: ["string", "null"]` with `"custom:type": "date"` - Add a sibling boolean field with `"beltic:validation"` - Use `field_ref` to reference the source date field in the same object scope - Works in top-level objects, nested objects, and array item objects **Best Practices:** - Use descriptive names and detailed descriptions for team clarity - Test with sample documents before using in production - Keep templates focused on specific document types (e.g., "Passport", "Driver License") # Get Document Template Source: https://docs.beltic.com/api-reference/endpoint/document-templates-get GET /v1/document-templates/{id} Retrieve a specific document template by its unique identifier. Returns the template configuration. **Response Includes:** - Template metadata (name, description, status) - Complete configuration (JSON schema, extraction config, fraud config) - Status flag (is_active) - Timestamps (created_at, updated_at) **Validation Fields in Schema:** - Date validations are represented as boolean fields containing `beltic:validation` - `field_ref` points to a sibling date field within the same object scope # List Document Templates Source: https://docs.beltic.com/api-reference/endpoint/document-templates-list GET /v1/document-templates Retrieve a paginated list of document templates with optional filtering. Useful for browsing available templates and selecting one for document processing. **Pagination:** - Control page size with `page[size]` (default: 15, max: 100) - Navigate using cursor-based pagination with `page[after]` and `page[before]` - Include total count with `page[total]=true` (note: may impact performance) - Use response `links` for easy navigation between pages **Filtering:** - Filter by `status` to show only 'published' templates - Filter by `is_active` to show only currently active templates **Common Queries:** - Active templates: `?filter[is_active]=true` - Recent templates: `?page[size]=10` (sorted by creation date) # Update Document Template Source: https://docs.beltic.com/api-reference/endpoint/document-templates-update PATCH /v1/document-templates/{id} Update an existing document template with partial attributes. Only provided fields will be updated. **Updatable Fields:** - **name**: Template name (must be unique) - **description**: Template description - **extraction_config**: Extraction settings including JSON schema - **fraud_config**: Fraud detection settings, including optional `requires_digital_signature` for documents that must contain a signature **Validation Schema Convention:** - Date-based validation should be stored on boolean fields (not on date fields) - Boolean validation fields use `beltic:validation` with `kind`, `field_ref`, and rule parameters - `field_ref` is scope-relative and must reference a sibling date field name **Notes:** - The template ID in the request body must match the path parameter - Omitted fields will retain their current values - Template status cannot be changed after creation # Create Document Source: https://docs.beltic.com/api-reference/endpoint/documents-create POST /v1/documents Create a new document for processing. This initializes a document record and returns a pre-signed S3 URL for file upload. **Configuration Options (mutually exclusive):** - **Template-based**: Provide `document_template_id` to use a predefined template configuration - **Ad-hoc**: Provide `extraction_config` and `fraud_config` directly in the request **Additional Options:** - `file_url`: Optionally provide a URL to download the file from - `fraud_config.requires_digital_signature`: In ad-hoc mode, set to `true` when the document must contain a signature; if no signature is detected, the fraud result includes a missing-signature RISK indicator **Workflow:** 1. Call this endpoint to create the document and receive an upload URL 2. Upload your file to the pre-signed URL using PUT method (or provide `file_url` to skip this step) 3. Once uploaded, the document will automatically begin processing 4. Poll the document GET endpoint to monitor processing status **Limits:** - **Maximum file size**: 50MB. Files exceeding this limit will be rejected and the document will be marked as failed with error code `FILE_TOO_LARGE`. **Note:** The pre-signed upload URL expires after the time specified in `expires_in` (typically 1 hour). # Get Document Source: https://docs.beltic.com/api-reference/endpoint/documents-get GET /v1/documents/{id} Retrieve a document by its unique identifier. Returns the complete document record including processing status, extracted data, and fraud detection results. **Document Status Values:** - `pending`: Document created but file not yet uploaded - `submitted`: File uploaded, queued for processing - `processing`: Currently being processed - `processed`: Processing completed successfully - `failed`: Processing failed (check error details) **Use Cases:** - Check document processing status - Retrieve extracted data after processing completes - Access fraud detection analysis results **Sparse Fieldsets (JSON:API):** - Use `fields[document]` with a comma-separated list to return only specific fields (attributes and relationships) - `id` and `type` are always returned; an empty value returns only `id`/`type` - Omit the parameter to return all fields - Unknown field names return `400 Bad Request` # List Documents Source: https://docs.beltic.com/api-reference/endpoint/documents-list GET /v1/documents Retrieve a paginated list of documents with optional filtering. Uses cursor-based pagination for efficient data retrieval. **Pagination:** - Use `page[size]` to control results per page (default: 15, max: 100) - Navigate using `page[after]` or `page[before]` cursors from the response - Set `page[total]=true` to include total count (may impact performance on large datasets) - Response includes `links` for easy navigation (first, prev, next, last) **Filtering:** - Filter by `status` to get documents in a specific processing state or multiple states (comma-separated) - Filter by `template_id` to get documents using a specific template **Sparse Fieldsets (JSON:API):** - Use `fields[document]` with a comma-separated list to return only specific fields (attributes and relationships) - `id` and `type` are always returned; an empty value returns only `id`/`type` - Omit the parameter to return all fields - Unknown field names return `400 Bad Request` **Example Queries:** - Single status: `GET /v1/documents?page[size]=20&filter[status]=processed&page[after]=eyJ2YWx1ZXM...` - Multiple statuses: `GET /v1/documents?page[size]=20&filter[status]=pending,submitted,processing,processed,failed,redacted` - Sparse fields: `GET /v1/documents?fields[document]=status,file` # List Executions Source: https://docs.beltic.com/api-reference/endpoint/executions-list GET /v1/workflows/executions List top-level workflow executions across all workflows in the caller's organization and environment, ordered newest first. Subworkflow child executions are intentionally excluded. # Create Account Source: https://docs.beltic.com/api-reference/endpoint/identity-accounts-create POST /v1/identity/accounts Create a new account with the provided attributes # Get Account Source: https://docs.beltic.com/api-reference/endpoint/identity-accounts-get GET /v1/identity/accounts/{id} Retrieve a specific account by its unique identifier # List Accounts Source: https://docs.beltic.com/api-reference/endpoint/identity-accounts-list GET /v1/identity/accounts Retrieve a list of all accounts with optional filtering # Update Account Source: https://docs.beltic.com/api-reference/endpoint/identity-accounts-update PATCH /v1/identity/accounts/{id} Update an existing account with partial attributes # Add File to Document Address Source: https://docs.beltic.com/api-reference/endpoint/identity-document-addresses-add-file POST /v1/identity/documents/addresses/{id}/files Add a file to the document and receive a pre-signed S3 URL for upload # Create Document Address Source: https://docs.beltic.com/api-reference/endpoint/identity-document-addresses-create POST /v1/identity/documents/addresses Create a new proof of address document # Get Document Address Source: https://docs.beltic.com/api-reference/endpoint/identity-document-addresses-get GET /v1/identity/documents/addresses/{id} Retrieve a specific document address by its unique identifier # List Document Addresses Source: https://docs.beltic.com/api-reference/endpoint/identity-document-addresses-list GET /v1/identity/documents/addresses List document addresses with optional filtering and pagination # Submit Document Address Source: https://docs.beltic.com/api-reference/endpoint/identity-document-addresses-submit POST /v1/identity/documents/addresses/{id}/submit Submit a document for processing (requires at least one uploaded file) # Add File to Document IDV Source: https://docs.beltic.com/api-reference/endpoint/identity-document-idvs-add-file POST /v1/identity/documents/idvs/{id}/files Add a file to the IDV document and receive a pre-signed S3 URL for upload # Create Document IDV Source: https://docs.beltic.com/api-reference/endpoint/identity-document-idvs-create POST /v1/identity/documents/idvs Create a new identity verification document. Optionally attach files which will receive presigned upload URLs. # Get Document IDV Source: https://docs.beltic.com/api-reference/endpoint/identity-document-idvs-get GET /v1/identity/documents/idvs/{id} Retrieve a single government ID verification document by ID # List Document IDVs Source: https://docs.beltic.com/api-reference/endpoint/identity-document-idvs-list GET /v1/identity/documents/idvs List all government ID verification documents with optional filtering and pagination # Approve Session Source: https://docs.beltic.com/api-reference/endpoint/identity-sessions-approve POST /v1/identity/sessions/{id}/approve Approve a session and mark it as decisioned # Create Session Source: https://docs.beltic.com/api-reference/endpoint/identity-sessions-create POST /v1/identity/sessions Create a new session with the provided attributes # Decline Session Source: https://docs.beltic.com/api-reference/endpoint/identity-sessions-decline POST /v1/identity/sessions/{id}/decline Decline a session and mark it as decisioned # Expire Session Source: https://docs.beltic.com/api-reference/endpoint/identity-sessions-expire POST /v1/identity/sessions/{id}/expire Manually expire a session # Get Session Source: https://docs.beltic.com/api-reference/endpoint/identity-sessions-get GET /v1/identity/sessions/{id} Retrieve a specific session by its unique identifier # List Sessions Source: https://docs.beltic.com/api-reference/endpoint/identity-sessions-list GET /v1/identity/sessions Retrieve a list of all sessions with optional filtering. Use filter[account] to filter by account ID and filter[session_device] to filter by session device ID. # Update Session Source: https://docs.beltic.com/api-reference/endpoint/identity-sessions-update PATCH /v1/identity/sessions/{id} Update an existing session with partial attributes # Create Verification Source: https://docs.beltic.com/api-reference/endpoint/identity-verifications-create POST /v1/identity/verifications Create a new verification request. Processing is performed synchronously. The `data.type` field determines the verification type and must be one of: - `verification/idv`: Identity document verification (requires a `document/idv` relationship) - `verification/address`: Address document verification (requires a `document/address` relationship) - `verification/business`: Business verification / KYB (requires a `business` relationship) The `session` relationship is **optional**. When `meta.auto_update_session` is `true`, a session must be provided and will be updated with the extracted data upon a passing verification. **Meta options:** - `auto_update_session`: When `true` and the verification passes, the session is automatically updated with the extracted information. Requires a `session` relationship. # Get Verification Source: https://docs.beltic.com/api-reference/endpoint/identity-verifications-get GET /v1/identity/verifications/{id} Retrieve a single verification by ID # List Verifications Source: https://docs.beltic.com/api-reference/endpoint/identity-verifications-list GET /v1/identity/verifications Retrieve a paginated list of verifications with optional filters # Create Screening Source: https://docs.beltic.com/api-reference/endpoint/screenings-create POST /v1/screenings Create a new compliance screening request. The `data.type` field determines the screening type and must be one of: - `screening/politically-exposed-person`: PEP screening - `screening/watchlist`: Sanctions and watchlist screening - `screening/business-watchlist`: Business sanctions screening - `screening/business-adverse-media`: Business adverse media screening Each type accepts a typed `query` object specific to that screening type. **Execution Modes:** - `async`: Returns immediately with pending status, processes in background (default) - `sync`: Waits for processing to complete (timeout: 55 seconds) # Get Screening Source: https://docs.beltic.com/api-reference/endpoint/screenings-get GET /v1/screenings/{id} Retrieve a screening by its unique identifier. Returns the latest state including partial or in-progress states. # List Screenings Source: https://docs.beltic.com/api-reference/endpoint/screenings-list GET /v1/screenings List screenings with optional filters for type and status. Supports cursor-based pagination. # Create Webhook Config Source: https://docs.beltic.com/api-reference/endpoint/webhook-config-create POST /v1/webhooks/configs Create a new webhook configuration to receive notifications when document status changes. **Important:** The webhook secret is returned ONLY in this response. Store it securely - it cannot be retrieved again. **Webhook Delivery:** - Webhooks are delivered via HTTP POST to the specified URL - Use the secret to validate the webhook payload signature using the HMAC-SHA256 algorithm - Non-2xx responses trigger automatic retries with exponential backoff - After 5 failed attempts, the delivery is marked as failed and sent to DLQ **Subscribed Statuses:** - `pending`: Document created, awaiting file upload - `submitted`: Document submitted for processing - `processing`: Document is being processed - `processed`: Document processing completed successfully - `failed`: Document processing failed # Delete Webhook Config Source: https://docs.beltic.com/api-reference/endpoint/webhook-config-delete DELETE /v1/webhooks/configs/:id Delete a webhook configuration. Pending deliveries for this config will not be retried. # Get Webhook Config Source: https://docs.beltic.com/api-reference/endpoint/webhook-config-get GET /v1/webhooks/configs/:id Get a specific webhook configuration by ID. Secret value is masked for security. # List Webhook Configs Source: https://docs.beltic.com/api-reference/endpoint/webhook-config-list GET /v1/webhooks/configs List all webhook configurations for the current organization. Secret values are masked for security. # Update Webhook Config Source: https://docs.beltic.com/api-reference/endpoint/webhook-config-update PATCH /v1/webhooks/configs/:id Update a webhook configuration. You can update the URL, subscribed statuses, or active state. The secret cannot be changed (use rotate-secret endpoint in future versions). # Resend Webhook Source: https://docs.beltic.com/api-reference/endpoint/webhook-resend POST /v1/webhooks/resend Manually trigger a webhook notification for a specific document and status. **Use Cases:** - Client missed a webhook due to downtime - Need to re-process a webhook after fixing endpoint issues - Testing webhook integration **Behavior:** - Creates a new delivery attempt for the specified document/status - If webhook_config_id is not provided, sends to all active configs subscribed to the status - Previous failed deliveries are not affected (new delivery record is created) # Check Website Source: https://docs.beltic.com/api-reference/endpoint/website-checks-create POST /v1/website-checks Perform comprehensive website verification including SSL, WHOIS, screenshots, page speed, and AI-powered risk analysis. **Processing modes** (controlled by `data.meta.async`): - **Sync** (`async: false` or omitted): Runs the full orchestrator inline and returns 200 (cache hit) or 201 with the completed result. Vendor calls can be slow — connections may close before the response is returned. Use async for slow vendors. - **Async** (`async: true`): Returns 201 immediately with a record in `status: "pending"` and an `id`. The work runs in the background. Poll `GET /v1/website-checks/{id}` until `status` is `completed` or `failed`. If a fresh cached result already exists, returns 200 with the cached payload immediately and skips the queue. # Execute Workflow Source: https://docs.beltic.com/api-reference/endpoint/workflow-execute POST /v1/workflows/execute Run a workflow synchronously. Returns `200` if the run reaches a terminal state (`completed`, `failed`, `cancelled`) and `202` if the run pauses awaiting external input (`paused`). By default the latest deployed workflow state is used. Pass `useDraftState: true` to run the draft state, or `workflow` to run an inline state graph. # List Workflow Executions Source: https://docs.beltic.com/api-reference/endpoint/workflow-executions-list GET /v1/workflows/{id}/executions List execution history for a single workflow. Subworkflow child executions are intentionally excluded. # List Workflows Source: https://docs.beltic.com/api-reference/endpoint/workflows-list GET /v1/workflows List workflow definitions in the caller's organization and environment. Returns metadata only; the workflow state graph is not included. # Authentication Source: https://docs.beltic.com/guides/authentication API keys, environments, scopes, rate limits, and retry guidance for the Beltic API. ## API Keys Every request to `https://api.beltic.com/v1` must include an API key in the `X-Api-Key` header: ```http theme={null} X-Api-Key: sk_production_... ``` API keys are created and managed in the [Beltic Console](https://console.beltic.com) under **Settings → API Keys**. The raw key value is shown exactly once at creation — copy it immediately and store it in a secrets manager. It cannot be recovered after you leave the page. ## Environments Beltic has two environments, each with its own API key prefix: | Environment | Key prefix | Base URL | | ----------- | ------------------- | --------------------------- | | Production | `sk_production_...` | `https://api.beltic.com/v1` | | Staging | `sk_staging_...` | `https://api.beltic.com/v1` | The base URL is the same for both environments — the key prefix determines which environment your requests target. **Never use a production key in development or test code.** Staging credentials are isolated from production data and can be freely reset. ```bash theme={null} # Staging export BELTIC_API_KEY=sk_staging_... # Production export BELTIC_API_KEY=sk_production_... ``` ## Key Scopes Keys carry a fixed permission set. Request only the scopes your service actually needs — a key used only to verify credentials shouldn't also be able to revoke them. **Workflow API** | Scope | Required for | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `workflows:read` | `GET /v1/workflows`, `GET /v1/workflows/{id}` (metadata; full definition requires `workflows:editor:read`) | | `workflows:editor:read` | Viewing a workflow's full definition in `GET /v1/workflows/{id}` (the editor / preview). Implied by `workflows:write` | | `workflows:write` | `POST /v1/workflows`, `PATCH /v1/workflows/{id}`, `PUT /v1/workflows/{id}/state`, `POST /v1/workflows/{id}/deploy`, `DELETE /v1/workflows/{id}` | | `workflows:execute` | `POST /v1/workflows/execute` | | `workflows:executions:read` | `GET /v1/workflows/executions`, `GET /v1/workflows/{id}/executions`, `GET /v1/workflows/status/{executionId}` | **Credentials API** | Scope | Required for | | -------------------- | ---------------------------------------------------------------------------------- | | `credentials:write` | `POST /v1/credentials`, `POST /v1/credentials/batch-issue` | | `credentials:read` | `GET /v1/credentials`, `GET /v1/credentials/{id}` | | `credentials:verify` | `POST /v1/credentials/verify` | | `credentials:revoke` | `POST /v1/credentials/{id}/revoke` | | `credentials:delete` | `DELETE /v1/credentials/{id}` | | `audit:read` | `GET /v1/audit/events` | | `audit:write` | `POST /v1/audit/events` | | `audit:streams` | `POST /v1/audit/streams`, `GET /v1/audit/streams`, `DELETE /v1/audit/streams/{id}` | **Document API** | Scope | Required for | | -------------------------- | ------------------------------------------------------------------ | | `documents:write` | `POST /v1/documents` | | `documents:read` | `GET /v1/documents`, `GET /v1/documents/{id}` | | `document_templates:write` | `POST /v1/document-templates`, `PATCH /v1/document-templates/{id}` | | `document_templates:read` | `GET /v1/document-templates`, `GET /v1/document-templates/{id}` | **Business & Screening APIs** | Scope | Required for | | ---------------------- | ----------------------------------------------- | | `businesses:write` | `POST /v1/businesses` | | `businesses:read` | `GET /v1/businesses`, `GET /v1/businesses/{id}` | | `screenings:write` | `POST /v1/screenings` | | `screenings:read` | `GET /v1/screenings`, `GET /v1/screenings/{id}` | | `website_checks:write` | `POST /v1/website-checks` | **Webhooks** | Scope | Required for | | ---------------- | ----------------------------------------------------------------------------------------------------------- | | `webhooks:write` | `POST /v1/webhooks`, `PATCH /v1/webhooks/{id}`, `DELETE /v1/webhooks/{id}`, `POST /v1/webhooks/{id}/resend` | | `webhooks:read` | `GET /v1/webhooks`, `GET /v1/webhooks/{id}` | Requests with a valid key but missing scope return `403 Forbidden`. ## Error Responses Authentication errors return standard HTTP codes with a JSON body: ```json theme={null} { "error": "unauthorized", "message": "API key is invalid or has been revoked" } ``` | HTTP | Code | Cause | | ----- | -------------- | ---------------------------------- | | `401` | `unauthorized` | Key missing, malformed, or revoked | | `403` | `forbidden` | Key valid but lacks required scope | | `429` | `rate_limited` | Too many requests — see below | ## Rate Limits Rate limits are applied per API key: | Tier | Requests / minute | Burst | | ------------------ | ----------------- | ----- | | Default | 300 | 500 | | Workflow execution | 60 | 100 | | Batch issue | 10 | 10 | When a limit is exceeded, the API returns `429 Too Many Requests` with a `Retry-After` header indicating how many seconds to wait: ```http theme={null} HTTP/1.1 429 Too Many Requests Retry-After: 3 X-RateLimit-Limit: 300 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1716292800 ``` ## Retries The Beltic API is safe to retry on transient errors. Follow these conventions: * **Retry on:** `429`, `500`, `502`, `503`, `504` * **Do not retry on:** `400`, `401`, `403`, `404`, `422` — these are deterministic failures that won't resolve on retry * **Use exponential backoff** with jitter, starting at 1 second, capping at 30 seconds * **Honour `Retry-After`** on `429` responses — don't backoff shorter than what the header specifies For mutation requests (`POST`, `DELETE`), use [idempotency keys](#idempotency) to ensure retries don't produce duplicate side effects. ## Idempotency Mutation endpoints (`POST /v1/credentials`, `POST /v1/workflows/execute`) accept an `Idempotency-Key` header. Submitting the same key twice within 24 hours returns the original response without re-executing: ```bash theme={null} curl -X POST https://api.beltic.com/v1/credentials \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Idempotency-Key: your-unique-request-id" \ -H "Content-Type: application/json" \ -d @payload.json ``` Concurrent requests with the same key return `409 Conflict`. Use a UUID or a hash of the request payload as the key. ## Public Endpoints (No Auth) The following endpoints do not require authentication — they are used by third-party verifiers who don't have a Beltic account: | Endpoint | Purpose | | ------------------------------------- | ------------------------------------------ | | `GET /.well-known/jwks.json` | Issuer's public signing keys | | `GET /.well-known/did.json` | Issuer DID document | | `GET /.well-known/status-lists/v1` | Revocation bitstring | | `POST /v1/credentials/_public/verify` | Verify a credential JWT without an API key | See [Public Endpoints](/guides/credentials/well-known-endpoints) for details. # Attaching Evidence Source: https://docs.beltic.com/guides/credentials/attaching-evidence Upload supporting documents and bind them to an issued credential via a tamper-evident W3C evidence[] claim. ## Overview Credentials carry an `evidence_refs[]` array — opaque audit-trail breadcrumbs that point at the source data the credential is based on. For supporting documents (passport scans, driver's-license photos, residence-permit PDFs, utility bills), Beltic offers a first-class evidence service: upload the bytes once, get back an `ev_`, then reference it from any number of credentials. When you issue a credential whose `evidence_refs[]` contains an `evidence:` entry, Beltic hydrates the record and emits a W3C VCDM 2.0 `evidence[]` block inside the signed JWT-VC. That block carries a `digestSRI: "sha256-..."` — the Subresource Integrity digest of the document. Any verifier who later obtains the original bytes (out-of-band, e.g. via the upcoming admin reveal flow) can recompute the hash and confirm document integrity without round-tripping Beltic. ## Prerequisites * A Beltic API key with `credentials:evidence:upload` permission. This is a distinct scope from `credentials:write` so KYC partners who only collect documents can be issued evidence-only keys without the ability to issue or revoke credentials themselves. * A supporting document in PDF, JPEG, PNG, WEBP, TIFF, or HEIC/HEIF format. Max 10 MB. ## Step 1: Upload the Document ```bash theme={null} curl -X POST https://api.beltic.com/v1/evidence \ -H "X-Api-Key: $BELTIC_API_KEY" \ -F "file=@passport.pdf;type=application/pdf" \ -F "document_type=passport" \ -F "filename=alivia-passport.pdf" ``` `file` is the only required form field. `document_type` and `filename` are both optional but recommended — they surface in the admin reveal flow and in the W3C `evidence[]` claim emitted at issue time. A successful upload returns 201 with the evidence resource: ```json theme={null} { "id": "ev_01HQABCDEFGH", "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "content_type": "application/pdf", "size_bytes": 134217, "filename": "alivia-passport.pdf", "document_type": "passport", "created_at": "2026-05-24T19:42:11.000Z" } ``` ### Idempotency Uploads are content-addressed by SHA-256. Re-uploading bytes whose hash already exists for your org returns the existing resource with HTTP 200 (instead of 201) and skips the S3 round-trip — useful for resumable uploads or when the same passport scan backs multiple credentials. ## Step 2: Reference the Evidence in a Credential Pass the returned ID with the `evidence:` prefix in the issue request's `evidence_refs[]`: ```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": "user", "subject": { "id": "usr_alivia", "type": "person" }, "claims": { "kyc_status": "approved", "trust_level": "idv_verified", "nationality": "US" }, "evidence_refs": ["evidence:ev_01HQABCDEFGH"] }' ``` You can mix `evidence:` entries with other opaque ref formats — only the `evidence:` ones trigger hydration. Anything else passes through to the credential row verbatim. ## What Lands in the JWT-VC When the credential is issued, the inner `vc` block gains an `evidence` array per W3C VCDM 2.0: ```json theme={null} { "vc": { "@context": ["https://www.w3.org/ns/credentials/v2"], "type": ["VerifiableCredential", "BelticUserCredential"], "credentialSubject": { "id": "usr_alivia", "kyc_status": "approved" }, "evidence": [ { "type": ["DocumentEvidence"], "id": "evidence:ev_01HQABCDEFGH", "documentType": "passport", "filename": "alivia-passport.pdf", "digestSRI": "sha256-n4bQgYhMfWWaL-qgxVrQFaO_TxsrCwTSjFTRyo2cFsM" } ] } } ``` The `digestSRI` field follows the [W3C Subresource Integrity](https://www.w3.org/TR/SRI/) spec — `sha256-` prefix followed by the base64url-encoded binary SHA-256 (no padding). A verifier who later receives the document bytes can recompute the digest and compare without needing to fetch anything from Beltic. ## Failure Modes | Status | Code | What happened | | ------ | ------------------------ | ------------------------------------------- | | 400 | `malformed_request` | Multipart body could not be parsed | | 400 | `missing_required_field` | No `file` part in the multipart body | | 400 | `validation_failed` | File is empty or exceeds the 10 MB limit | | 422 | `unprocessable_entity` | Content type not in the accepted list | | 401 | `unauthorized` | API key invalid or missing | | 403 | `forbidden` | API key lacks `credentials:evidence:upload` | If you reference an `evidence:` from an issue request and the ID doesn't exist for your org, the issue call fails with 400 `evidence_not_found` and the `details.missing_ids` array lists the unresolved IDs. Cross-org IDs look identical to stale ones — the lookup is org-scoped, by design. ## Reading Evidence Back Once uploaded, evidence can be fetched two ways. Both require an API key with the dedicated `credentials:evidence:read` permission — granted independently of `credentials:read` so compliance reviewers can be given read-only evidence keys without seeing credential rows. ### Metadata ```bash theme={null} curl https://api.beltic.com/v1/evidence/ev_01HQABCDEFGH \ -H "X-Api-Key: $BELTIC_API_KEY" ``` Returns the same `EvidenceResource` shape as the upload response. The bytes are NOT included. ### Bytes (presigned download URL) ```bash theme={null} curl https://api.beltic.com/v1/evidence/ev_01HQABCDEFGH/download \ -H "X-Api-Key: $BELTIC_API_KEY" ``` Returns a short-lived presigned S3 URL the caller can `GET` directly without proxying through Beltic: ```json theme={null} { "url": "https://beltic-prod-credentials-evidence.s3.amazonaws.com/org_abc/ev_01HQABCDEFGH.pdf?X-Amz-Algorithm=...", "expires_at": "2026-05-25T19:51:00.000Z", "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "content_type": "application/pdf", "size_bytes": 134217 } ``` The URL is the bearer token for the bytes the moment it's issued — anyone who obtains it (logs, screenshots, browser history) can pull the document for the remainder of the TTL. **Default TTL: 60 seconds.** Long enough for a click → fetch cycle; short enough that screenshotted URLs are dead by the time anyone could exploit them. Pass `expires_in_seconds` (between 60 and 300) to extend up to the 5-minute hard cap when downloading over slow networks: ```bash theme={null} curl "https://api.beltic.com/v1/evidence/ev_01HQABCDEFGH/download?expires_in_seconds=300" \ -H "X-Api-Key: $BELTIC_API_KEY" ``` Every URL generation is audit-logged with the requesting API key id, so a leak post-mortem can attribute every reveal to a specific consumer. **Why so short?** Browsers don't redact URLs from history; logs frequently capture them; chat apps render them as click-through links. A 60-second TTL means a URL pasted to Slack is essentially dead before anyone could click it. Callers who need bytes for longer can simply re-request — the second call is also audit-logged. After fetching the bytes the caller can recompute SHA-256 and compare against the `sha256` field (and against the `digestSRI` on any credential that references this evidence) to confirm the document hasn't been tampered with in transit. ## Retention Evidence bytes are retained for 7 years from upload (US FinCEN BSA + EU AML5D minimum for KYC document retention). The S3 lifecycle transitions objects to Intelligent-Tiering after 30 days so cold evidence costs near-zero. Storage retention is independent of credential lifecycle — revoking a credential does not delete its evidence. For GDPR Article 17 erasure requests, an admin path (not yet exposed publicly) can hard-delete evidence rows + their S3 objects on demand. ## Self-Service Reveal for Credential Subjects The credential subject (the person the credential is about) often isn't a Beltic-org user with their own API key. To let them view their credential plus its evidence without minting a per-user key, the org can email them a one-time access link: ```bash theme={null} curl -X POST https://api.beltic.com/v1/credentials/cred_abc123/reveal-link \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "alivia@example.com", "first_name": "Alivia" }' ``` Beltic sends the email from its own domain via SES (`noreply@beltic.com` by default). The recipient clicks the link, which lands on a Beltic-hosted page at `portal.beltic.com/r?token=&credential_id=`. The page hands the token to `POST /v1/credentials/_reveal/authenticate`, which: 1. Hashes the raw token server-side and looks up the row. 2. Atomically marks it redeemed (one-time use — second clicks return 410 Gone). 3. Mints a Beltic-issued credential-scoped JWT. The raw token is never stored on the Beltic side — only its SHA-256 hash lives in the `reveal_tokens` Postgres table. A database leak yields hashes the attacker can't pre-image back to working URLs. **The session JWT is scoped to a single credential** — server-side. The Beltic-issued token (HS256, signed with `REVEAL_TOKEN_SECRET`) carries the `credential_id` it's bound to in its claims. Every handler that reads credential or evidence data refuses requests where the requested resource doesn't match the bound scope. This is the security difference from any unscoped session token. If a token-holder tries to fetch a sibling credential or unrelated evidence: ```json theme={null} { "error": { "code": "forbidden", "message": "Reveal token is scoped to a different credential…" } } ``` **Audit:** every `reveal-link` mint fires a `credential.reveal_link_sent` event keyed to the org's API key. Redemptions and per-byte downloads emit their own events so the org sees the full lifecycle. **TTL:** the email link expires 10 minutes after `reveal-link` is called (one-time use). The Beltic-issued reveal JWT lives for 15 minutes after redemption (hard cap 60 minutes). If the recipient misses either window, request a new link. **Email customization:** reveal emails are sent with Beltic branding today. Per-org template customization (logo, from-name, copy) and a configurable from-address are on the roadmap — reach out if you need them for your rollout. ## See Also * [Issuing a Credential](issuing-a-credential) * [Verifying a Credential](verifying-a-credential) — covers how `evidence[]` is exposed in the verify response # Batch Issuing Credentials Source: https://docs.beltic.com/guides/credentials/batch-issuing-credentials Issue many credentials in a single async job — ideal for backfills, bulk onboarding, and CLI uploads. ## Overview The batch-issue endpoint accepts up to 10,000 credentials in a single call and processes them asynchronously. You get a `job_id` back immediately; the credentials are issued in the background by a worker Lambda, and you poll job status or subscribe to a webhook stream to find out when the batch is done. Use it for: * One-time backfills when you've already KYB-verified a set of businesses out-of-band. * CLI-driven uploads where an operator pastes a JSON file with thousands of records. * Periodic syncs from an internal system that issues credentials nightly. For low-volume, interactive issuance (≤ 10 credentials), call `POST /v1/credentials` directly per credential — it's faster and synchronous. ## Prerequisites * A Beltic API key with `credentials:write` permission * A JSON array of credential payloads (each one a valid `POST /v1/credentials` body) ## Step 1: Submit the Batch ```bash theme={null} curl -X POST https://api.beltic.com/v1/credentials/batch-issue \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "credentials": [ { "credential_type": "business", "subject": { "id": "biz_001", "type": "organisation", "name": "Acme Corp" }, "claims": { "kyb_status": "approved", "verified_at": "2026-05-20T00:00:00Z", "jurisdiction": "US-DE" } }, { "credential_type": "business", "subject": { "id": "biz_002", "type": "organisation", "name": "Widget Co" }, "claims": { "kyb_status": "approved", "verified_at": "2026-05-20T00:00:00Z", "jurisdiction": "US-NY" } } ] }' ``` ### Response ```json theme={null} { "job_id": "job_01HQ7Q...", "status": "queued", "total": 2, "processed": 0, "succeeded": 0, "failed": 0, "created_at": "2026-05-21T16:00:00Z" } ``` The batch is accepted and queued. The HTTP response returns within \~200ms; actual issuance happens in the background. ## Step 2: Poll for Status (or use webhooks) Poll the same job\_id endpoint to check progress: ```bash theme={null} curl "https://api.beltic.com/v1/credentials/batch-issue/$JOB_ID" \ -H "X-Api-Key: $BELTIC_API_KEY" ``` ```json theme={null} { "job_id": "job_01HQ7Q...", "status": "completed", "total": 2, "processed": 2, "succeeded": 2, "failed": 0, "credential_ids": [ "cred_01HQ7R...", "cred_01HQ7S..." ], "started_at": "2026-05-21T16:00:01Z", "completed_at": "2026-05-21T16:00:08Z" } ``` Status progression: `queued` → `processing` → `completed` (or `failed` if every item errored). ### Webhook Alternative For long-running batches you don't want to poll, create a webhook stream that fires on `credential.batch_completed`: ```bash theme={null} curl -X POST https://api.beltic.com/v1/audit/streams \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.example.com/webhooks/beltic", "actions": ["credential.batch_completed", "credential.batch_failed"] }' ``` See the audit-streams endpoint reference for signature verification details. ## Partial Failures A batch is treated as best-effort: if one credential fails validation (e.g. an invalid `kyb_status`), the rest still issue. The job's `failed` count reflects how many didn't make it; per-item failure details are surfaced in the job response's `errors` array: ```json theme={null} { "job_id": "job_01HQ7Q...", "status": "completed", "total": 100, "processed": 100, "succeeded": 97, "failed": 3, "credential_ids": ["cred_...", "cred_..."], "errors": [ { "index": 42, "code": "validation_failed", "message": "claims.kyb_status: expected 'approved' | 'pending' | 'declined' | 'manual_review', got 'verified'" } ] } ``` The `index` field is the 0-based position of the failed item in your original `credentials` array — use it to reconcile with your source data. ## Limits | Constraint | Value | | ----------------------------- | -------------------------------- | | Max items per batch | 10,000 | | Max request body size | 25 MB | | Concurrent batch jobs per org | 10 | | Per-org batch throughput | 100 credentials/second sustained | Submitting a 100k-credential backfill? Split it across multiple jobs. ## Idempotency Like single-issue, batch-issue supports the `Idempotency-Key` header. Use one key per logical batch — re-submitting with the same key within 24 hours returns the original `job_id` without enqueuing duplicate work. ## CLI Usage The Beltic CLI wraps batch-issue with a progress bar and per-item retry. If you have a JSON file of credential payloads: ```bash theme={null} beltic credentials batch-issue --file ./credentials.json --wait ``` The CLI calls `POST /v1/credentials/batch-issue`, polls for completion, and exits non-zero if any item failed. Pair it with CI/CD to run nightly backfills. ## Next Steps Validate any of the issued credentials. Query the audit log for batch issuance events. # Issuing a Credential Source: https://docs.beltic.com/guides/credentials/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" } ``` **British spelling matters**: `subject.type` for a company is `"organisation"`, not `"organization"`. The schema follows the W3C VC business profile. ### Subject Shapes by Type The required fields on `subject` and `claims` depend on `credential_type`: ```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`). ```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`). ```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. ```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 | ## 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 Validate the JWT-VC you just received. Issue thousands of credentials in a single async job. # Credentials API Overview Source: https://docs.beltic.com/guides/credentials/overview Issue, verify, and revoke verifiable credentials for businesses, users, and AI agents with the Beltic Credentials API. ## What is the Credentials API? The Beltic Credentials API issues cryptographically signed verifiable credentials that prove a fact about a subject — a business has been KYB'd, a user is over 21, an AI agent is authorized to spend up to \$500 on behalf of its owner. Credentials are signed JWT-VCs (W3C Verifiable Credentials 2.0) that any verifier can validate against Beltic's public keys without ever calling back to your servers. Use cases include: * Letting an authorized agent transact on a customer's behalf without sharing payment credentials. * Proving to a counterparty that a business has been verified, without revealing the underlying KYB data. * Onboarding flows where a user's identity check is reusable across services. ## Core Concepts Four discriminator-gated shapes: `business`, `user`, `agent_authorization`, and `outcome_attestation`. Each enforces its own claims schema at the API boundary. The entity the credential is about — an organisation, person, agent, or document. The subject ID becomes part of the credential and is what verifiers index against. Beltic signs every credential with an ES256 key managed in AWS KMS. The signing key is published at `/.well-known/jwks.json`; any verifier can resolve it. Credentials are revocable via Status List 2021. Each credential is assigned a bit in your organisation's bitstring; revocation flips the bit, and verifiers detect it on their next check. ## Credential Lifecycle Call `POST /v1/credentials` with a `credential_type`, a `subject`, and a `claims` object. Beltic validates the payload against the type-specific schema, allocates a status-list slot, signs the credential, and returns both the persisted record and the signed JWT-VC. Anyone — your service, a partner, an external counterparty — can call `POST /v1/credentials/verify` with the JWT to confirm it's valid, unexpired, and not revoked. The verify pipeline runs a seven-step check that includes signature verification against the issuer's JWKS, claim validation, and a Status List 2021 lookup. Call `POST /v1/credentials/{id}/revoke` to mark a credential revoked. The credential's bit in the org bitstring is flipped; verifiers see the revocation on their next status-list fetch (Cache-Control max-age 60s). Every issue, verify, revoke, and delete writes an immutable audit event. Query the audit log via `GET /v1/audit/events`, or subscribe to webhook streams to react to events as they happen. ## Authentication All endpoints (except the public `/.well-known/*` paths and the public verify) require an API key passed as `X-Api-Key`. Keys are scoped by environment (`sk_staging_*` or `sk_production_*`) and carry a fixed set of permissions: | Permission | Required for | | -------------------- | ------------------------------------------------------------------ | | `credentials:write` | Issue (`POST /v1/credentials`, `POST /v1/credentials/batch-issue`) | | `credentials:read` | List, get | | `credentials:verify` | Verify | | `credentials:revoke` | Revoke | | `credentials:delete` | Delete | API keys are created in the [Beltic Console](https://console.beltic.com). The raw key value is shown once at creation and never recoverable — store it securely. ## Flat JSON Responses The Credentials API returns flat JSON, not JSON:API. Resources come back with their fields at the top level: ```json theme={null} { "id": "cred_01HQXYZ...", "credential_id": "cred_01HQXYZ...", "credential_type": "agent_authorization", "subject": { "id": "agent_widgetcorp_assistant", "type": "agent", "name": "WidgetCorp Customer Assistant" }, "claims": { "permissions": [ { "resource_type": "wallet", "resource_id": "*", "actions": ["payment_authorize", "checkout"], "conditions": [{ "field": "transaction_amount", "op": "lte", "value": 50000 }] } ] }, "signed_payload": "eyJhbGciOiJFUzI1NiIs...", "status": "active", "issued_at": "2026-05-21T10:30:00Z", "expires_at": "2026-08-19T10:30:00Z" } ``` This is intentional — the API is designed for direct integration without an SDK and without per-language JSON:API parsers. ## Where to Next Walk through `POST /v1/credentials` end-to-end, including credential-type-specific payloads. Verify a JWT-VC and understand the seven-step verify pipeline. Revoke a credential and learn how the change propagates to verifiers. Issue thousands of credentials in a single async job. # Revoking a Credential Source: https://docs.beltic.com/guides/credentials/revoking-a-credential Invalidate a credential immediately and propagate the change to all verifiers via Status List 2021. ## Overview Revocation flips a single bit in your organisation's Status List 2021 bitstring. Verifiers — including the Beltic verify endpoint and any offline verifier — see the revocation on their next bitstring fetch. There's no per-credential round-trip; revocation propagation is bounded by the bitstring's cache TTL. ## Prerequisites * A Beltic API key with `credentials:revoke` permission * The `credential_id` of the credential you want to revoke ## Step 1: Send the Revoke Request ```bash theme={null} curl -X POST https://api.beltic.com/v1/credentials/$CREDENTIAL_ID/revoke \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"reason": "key_rotation"}' ``` The `reason` field is optional but recommended. Use it for audit context — common values are `key_rotation`, `compromised`, `policy_change`, `user_request`, `error`. ## Step 2: Inspect the Response ```json theme={null} { "id": "cred_01HQ7P4M6...", "credential_id": "cred_01HQ7P4M6...", "credential_type": "business", "status": "revoked", "revoked_at": "2026-05-21T15:00:00Z", "revocation_reason": "key_rotation", "status_list_index": 4287, "issued_at": "2026-05-21T10:30:00Z", "expires_at": "2026-08-19T10:30:00Z", "updated_at": "2026-05-21T15:00:00Z" } ``` The credential's `status` is now `revoked`, and the bit at `status_list_index` in your org's bitstring has been flipped. The signed JWT itself is still cryptographically valid — revocation is a separate signal that lives outside the signature. ## How Verifiers See the Change Revocation propagates through the status list, not through the credential itself: `POST /revoke` writes the updated bitstring to S3 at the public status-list URL (`/.well-known/status-lists/v1`). Every verify call fetches (or uses a cached copy of) the org's status list. Cache-Control on the response is 60 seconds by default. During step 6 of the verify pipeline, the verifier reads bit `status_list_index` from the bitstring. If it's set, the credential is treated as revoked. **Worst-case propagation delay:** 60 seconds (the cache TTL). After that, every verifier sees the revocation. ## Revocation Is Permanent Revoked credentials cannot be un-revoked. The bit stays flipped for the lifetime of the credential, and the slot is not reused — Status List 2021's protocol guarantees that a bit, once set, stays set. If you need to issue a fresh credential for the same subject, call `POST /v1/credentials` again with the same subject and claims. The new credential gets its own credential ID and its own status-list slot. ## Suspension vs Revocation Status List 2021 supports two distinct lifecycle states beyond `active` and `expired`: | Status | Bit state | Semantics | | ----------- | ------------------------------- | --------------------------------------- | | `active` | Unset | Credential is valid | | `suspended` | Set (and reversible internally) | Temporarily invalid; can be reactivated | | `revoked` | Set | Permanently invalid | | `expired` | n/a | Past its `exp` claim | V1 of the Beltic API exposes only `revoked` via the public revoke endpoint. Suspension is reserved for internal administrative use (e.g. a fraud hold) and surfaces on verify as `reason: "revoked"` with `status: "suspended"` in the response so callers can distinguish. ## Auditing Revocations Every revoke call writes an immutable audit event with the `credential.revoked` action. Query it via: ```bash theme={null} curl "https://api.beltic.com/v1/audit/events?action=credential.revoked" \ -H "X-Api-Key: $BELTIC_API_KEY" ``` Each audit event captures the credential ID, the actor (API key ID), the revocation reason, and a hash-chained `prev_hash` + `row_hash` pair for tamper detection. See the audit-events endpoint reference for the full shape. ## Error Codes | HTTP | Code | Cause | | ---- | ----------- | ------------------------------------------------ | | 404 | `not_found` | Credential ID doesn't exist in your org | | 409 | `conflict` | Credential is already revoked or already expired | | 403 | `forbidden` | API key lacks `credentials:revoke` | ## Next Steps Mint a new credential for the same subject. Confirm a credential is or isn't revoked. # Verifying a Credential Source: https://docs.beltic.com/guides/credentials/verifying-a-credential Validate a signed credential JWT against the issuer's keys, schema, status list, and policy. ## Overview Verification answers a simple question: **is this credential currently valid, and what does it say?** The Beltic Credentials API runs a deterministic seven-step pipeline for every verify call, returning a flat response that's either a success (with the full subject + claims) or a structured rejection (with a machine-readable reason code). ## Prerequisites * A Beltic API key with `credentials:verify` permission * A signed credential JWT — the `signed_payload` field from a previous issue response ## The Seven-Step Pipeline Every verify call runs through these checks, in order. The first failure short-circuits the rest. Decode the JWT header and payload. Extract `alg`, `kid`, and `iss`. Reject if malformed or if `alg` isn't ES256. Look up `iss` in the trusted-issuer list (V1 is just `did:web:beltic.com`), then fetch the public JWK by `kid` from `/.well-known/jwks.json`. Cryptographically verify the JWT against the resolved key using `jose.jwtVerify`. Reject on signature mismatch. Validate `iat`, `exp`, `nbf`. Optionally validate `aud` if `context.audience` was provided. Match the JWT claims against the registered credential-type schema. Catches forged credentials whose signature is valid but whose claim shape is wrong. Look up the credential in Beltic's registry, then check the Status List 2021 bit. Reject if revoked, expired, or suspended. For `agent_authorization` credentials with a verify context, evaluate the credential's `permissions[]` against the request context — fields like `amount`, `currency`, `resource`. Reject on no matching permission or condition failure. ## Basic Verify The minimum request: just the JWT. ```bash theme={null} curl -X POST https://api.beltic.com/v1/credentials/verify \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"credential\": \"$CREDENTIAL_JWT\"}" ``` The issue response returns the JWT as `signed_payload`; the verify endpoint accepts it as `credential`. Same value, different field names — `signed_payload` is the persisted resource field, `credential` follows the JWT-VC presentation convention. Pass the JWT string directly either way. ### Success Response ```json theme={null} { "valid": true, "credential_id": "cred_01HQ7P4M6...", "credential_type": "business", "issuer_did": "did:web:beltic.com", "subject": { "id": "biz_widgetcorp", "type": "organisation", "name": "WidgetCorp Ltd" }, "issued_at": "2026-05-21T10:30:00Z", "expires_at": "2026-08-19T10:30:00Z", "verified_at": "2026-05-21T14:22:11Z", "status": "active", "evidence_refs": [], "verification_id": "ver_01HQ8R..." } ``` * **`valid: true`** is the headline. * **`status`** is the persisted lifecycle (`active`, `revoked`, `expired`, `suspended`) — different from `valid`, which is the protocol outcome. * **`verification_id`** is unique per call. Use it as a correlation handle for audit and support. ### Rejection Response ```json theme={null} { "valid": false, "reason": "revoked", "credential_id": "cred_01HQ7P4M6...", "status": "revoked", "verified_at": "2026-05-21T14:30:00Z", "verification_id": "ver_01HQ8S...", "details": { "revoked_at": "2026-05-21T14:25:00Z", "reason": "rotated_key" } } ``` ## Verify with Policy Context For `agent_authorization` credentials, you can pass a `context` object to evaluate the agent's permissions against a specific request. This is how downstream services (e.g. a payment gateway) check that the agent is authorized for *this particular call*. ```bash theme={null} curl -X POST https://api.beltic.com/v1/credentials/verify \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "credential": "'"$AGENT_JWT"'", "context": { "resource_type": "wallet", "resource_id": "*", "action": "payment_authorize", "transaction_amount": 4999, "transaction_currency": "usd", "audience": "stripe.com" } }' ``` If the credential's permissions allow the action, the response includes a `policy_match`: ```json theme={null} { "valid": true, "credential_id": "cred_...", "credential_type": "agent_authorization", "policy_match": { "matched": true, "permission_index": 0, "conditions_evaluated": [ { "field": "amount", "op": "lte", "result": "pass" }, { "field": "currency", "op": "eq", "result": "pass" } ] }, "verification_id": "ver_..." } ``` If a condition fails, you get a structured rejection: ```json theme={null} { "valid": false, "reason": "condition_failed", "credential_id": "cred_...", "details": { "deny_reason": "condition_failed:amount", "conditions_evaluated": [ { "field": "amount", "op": "lte", "result": "fail" } ] }, "verification_id": "ver_..." } ``` ## Reject Reason Codes | Reason | Pipeline step | Meaning | | ------------------------ | ------------- | ------------------------------------------------------ | | `malformed_jwt` | 1 | JWT couldn't be parsed | | `alg_not_allowed` | 1 | Algorithm isn't ES256 | | `issuer_not_trusted` | 2 | `iss` isn't in the trust list | | `kid_not_found` | 2 | Key ID doesn't resolve in the issuer's JWKS | | `signature_mismatch` | 3 | Signature failed verification | | `expired` | 4 or 6 | Credential past its `exp`, or status list says expired | | `not_yet_valid` | 4 | `nbf` is in the future | | `audience_mismatch` | 4 | `aud` doesn't match `context.audience` | | `schema_mismatch` | 5 | JWT claim shape doesn't match the credential type | | `revoked` | 6 | Status List 2021 bit is flipped | | `policy_deny` | 7 | Generic policy denial | | `no_matching_permission` | 7 | No permission in the credential matched the request | | `condition_failed` | 7 | A permission matched but its conditions failed | ## Verifying Offline (Without the API) The full pipeline runs server-side, but the cryptographic checks (steps 1-5) can be replicated offline: 1. Fetch `https://api.beltic.com/.well-known/jwks.json` and cache it. Honour the response's `Cache-Control` header (typically 1 hour). 2. Use any JWT library to verify the JWS against the JWKs. 3. Validate `iat`, `exp`, `nbf` yourself. 4. To check revocation, fetch `https://api.beltic.com/.well-known/status-lists/v1` and decode the bitstring at the credential's `status_list_index`. The Beltic verify endpoint is recommended for most callers — it handles trust-list management, JWKS caching, and policy evaluation. Offline verification is appropriate when you need to verify in air-gapped environments or want to minimize calls to Beltic. ## Public Verify (No Auth) There's also a public verify endpoint at `POST /v1/credentials/_public/verify` that doesn't require an API key. It returns the same shape but doesn't expose org-internal fields. Use it for verifier integrations where you can't ship an API key (e.g. browser-side checks). ## Next Steps Mark a credential revoked and understand how verifiers see the change. Mint a new credential to verify. # Public Endpoints (.well-known) Source: https://docs.beltic.com/guides/credentials/well-known-endpoints JWKS, DID document, and Status List 2021 bitstring — the public endpoints verifiers fetch to validate credentials. ## Overview Beltic publishes three `.well-known` endpoints that any verifier — your own service, a counterparty, an offline tool — can fetch without authentication. These are the building blocks of W3C-compliant verifiable credentials: the JWKS exposes the signing keys, the DID document describes the issuer identity, and the status list reports revocations. ## `/.well-known/jwks.json` Returns the issuer's JSON Web Key Set: the public keys credentials are signed with. Verifiers fetch this to validate signatures. ```bash theme={null} curl https://api.beltic.com/.well-known/jwks.json ``` ```json theme={null} { "keys": [ { "kty": "EC", "crv": "P-256", "kid": "K8L9...", "x": "...", "y": "...", "alg": "ES256", "use": "sig" } ] } ``` * The `kid` matches the `kid` claim in every JWT-VC's header. * V1 publishes a single signing key; multi-key rotation lands in a later phase. * Cache-Control is 1 hour. Verifiers should respect it. ## `/.well-known/did.json` Returns the W3C DID document for `did:web:beltic.com`. The DID document describes the issuer identity and references the JWKS endpoint. ```bash theme={null} curl https://api.beltic.com/.well-known/did.json ``` ```json theme={null} { "@context": ["https://www.w3.org/ns/did/v1"], "id": "did:web:beltic.com", "verificationMethod": [ { "id": "did:web:beltic.com#K8L9...", "type": "JsonWebKey2020", "controller": "did:web:beltic.com", "publicKeyJwk": { /* same key as in jwks.json */ } } ], "assertionMethod": ["did:web:beltic.com#K8L9..."] } ``` Use this when integrating with verifiers that resolve issuers via DID rather than direct JWKS fetch — most W3C-conformant verifier libraries do. ## `/.well-known/status-lists/v1` Returns the Status List 2021 bitstring for the org whose credentials are being verified. The bit at `status_list_index` is `1` for revoked credentials, `0` for active. ```bash theme={null} curl https://api.beltic.com/.well-known/status-lists/v1?org=org_01HQ... ``` ```json theme={null} { "@context": [ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/vc/status-list/2021/v1" ], "id": "https://api.beltic.com/.well-known/status-lists/v1?org=org_01HQ...", "type": ["VerifiableCredential", "StatusList2021Credential"], "issuer": "did:web:beltic.com", "credentialSubject": { "id": "https://api.beltic.com/.well-known/status-lists/v1?org=org_01HQ...#list", "type": "StatusList2021", "statusPurpose": "revocation", "encodedList": "H4sIAAAAA..." } } ``` * **`encodedList`** is a base64url-encoded, gzipped byte-aligned bitstring. Decode, gunzip, then check bit `status_list_index` MSB-first within each byte. * Cache-Control is 60 seconds — short enough that revocations propagate quickly, long enough that high-throughput verifiers don't hammer the endpoint. * The `org` query parameter targets a specific organisation's bitstring. Each org has its own. ### Decoding the Bitstring ```ts theme={null} // Pseudocode for checking bit `index` in encodedList const compressed = base64urlDecode(encodedList); const bytes = gunzip(compressed); const byteIdx = Math.floor(index / 8); const bitOffset = 7 - (index % 8); const isRevoked = (bytes[byteIdx] & (1 << bitOffset)) !== 0; ``` ## When to Use Which | You want to… | Fetch | | ---------------------------------------------------- | --------------------------------------------------------- | | Verify a JWT signature offline | `/.well-known/jwks.json` | | Integrate with a W3C-conformant verifier library | `/.well-known/did.json` | | Check whether a specific credential has been revoked | `/.well-known/status-lists/v1?org=...` | | Run the full verify pipeline server-side | `POST /v1/credentials/verify` (handles all three for you) | ## Caching Recommendations * **JWKS:** cache for 1 hour. Re-fetch only on `kid` mismatch. * **DID document:** cache for 1 hour. Treat it as the same lifetime as JWKS. * **Status list:** cache for 60 seconds. If your application can tolerate longer staleness, increase locally — but understand that revocations won't propagate faster than your cache TTL. # Accounts Source: https://docs.beltic.com/guides/identity/accounts Accounts represent external entities in the Beltic Identity platform. Use them to correlate users, track verification status, and manage access. ## What is an Account? An **Account** is the primary resource for representing an external entity — a person or a business — in the Beltic Identity platform. It serves as the correlation point between your system and Beltic. When you onboard a user on your platform, you create a corresponding Account in Beltic. From there, you can: * **Track verification status** by reading the account's status * **Link sessions and documents** to build a complete identity profile * **Perform actions** such as suspending or blocking the user on your side based on the account state * **Store your own reference** via the `external_id` field for easy lookup ## Account Statuses | Status | Description | | ----------- | ------------------------------------------------------------- | | `active` | The account is active and in good standing | | `pending` | The account is awaiting verification or review | | `inactive` | The account has been deactivated | | `suspended` | The account has been suspended due to policy or risk concerns | You can read the account status at any time and use it to gate access on your platform. For example, if an account is `suspended`, you might block the user from performing sensitive operations. ## Creating an Account Create an account by specifying the entity type and providing identity information: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/accounts \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "account", "attributes": { "external_id": "user-12345", "entity_type": "person", "person": { "name": { "first": "Jane", "last": "Doe" }, "birth_date": "1990-05-15" }, "contact": { "email": "jane@example.com", "phone": "+1234567890" } } } }' ``` ### Response ```json theme={null} { "data": { "type": "account", "id": "acc_01HQ...", "attributes": { "status": "pending", "external_id": "user-12345", "entity_type": "person", "person": { "name": { "first": "Jane", "middle": null, "last": "Doe" }, "birth_date": "1990-05-15", "sex": null }, "business": null, "contact": { "email": "jane@example.com", "phone": "+1234567890" }, "address": null, "identity_numbers": [], "created_at": "2025-01-15T10:30:00Z", "updated_at": null, "redacted_at": null }, "relationships": { "sessions": { "data": [] }, "documents": { "data": [] } } } } ``` ## Entity Types Accounts support two entity types: For individual users. Provide identity details under the `person` field: ```json theme={null} { "entity_type": "person", "person": { "name": { "first": "Jane", "middle": "M", "last": "Doe" }, "birth_date": "1990-05-15", "sex": "female" } } ``` For business entities. Provide details under the `business` field: ```json theme={null} { "entity_type": "business", "business": { "legal_name": "Acme Corp", "registration_number": "REG-12345", "tax_id": "TAX-67890" } } ``` ## Using `external_id` The `external_id` field lets you store your own user identifier on the account. This is the recommended way to correlate Beltic accounts with your own database: ```bash theme={null} # Create with your user ID curl -X POST https://api.beltic.com/v1/identity/accounts \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "account", "attributes": { "external_id": "usr_abc123", "entity_type": "person" } } }' ``` You can then use the external ID to look up accounts when needed. ## Updating an Account Update account attributes with a PATCH request: ```bash theme={null} curl -X PATCH https://api.beltic.com/v1/identity/accounts/{account_id} \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "account", "id": "acc_01HQ...", "attributes": { "contact": { "email": "new-email@example.com" } } } }' ``` ## Reading Account Status Poll or read the account status to make decisions on your side: ```bash theme={null} curl -X GET https://api.beltic.com/v1/identity/accounts/{account_id} \ -H "X-Api-Key: YOUR_API_KEY" ``` Use the `status` field to drive your application logic: ```javascript theme={null} const account = await getAccount(accountId); switch (account.data.attributes.status) { case 'active': // Allow full access break; case 'pending': // Show verification pending message break; case 'suspended': // Block sensitive operations break; case 'inactive': // Prompt re-verification break; } ``` ## Redacting an Account When you need to delete personal data (e.g., for GDPR compliance), redact the account: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/accounts/{account_id}/redact \ -H "X-Api-Key: YOUR_API_KEY" ``` Redaction is irreversible. All personal data on the account will be permanently removed. The account record itself is retained with a `redacted_at` timestamp for audit purposes. ## Relationships An account connects to other Identity resources: * **Sessions** — Verification sessions created for this account * **Documents** — Identity documents associated with this account These relationships are included in the account response and allow you to navigate the complete identity profile from a single entry point. ## Next Steps Once you have an account, the next step is to [create a session](/guides/identity/sessions) to begin collecting applicant information and running verifications. # Documents Source: https://docs.beltic.com/guides/identity/documents Upload and manage identity documents such as government IDs and proof of address. ## What are Documents? Documents represent the identity files that an applicant provides during verification. The Identity API supports two types of documents: | Type | Resource Type | Purpose | | -------------------- | ------------------ | ------------------------------------------------------------------------------------------ | | **Government ID** | `document/idv` | Passports, driver's licenses, national IDs, and other government-issued identity documents | | **Proof of Address** | `document/address` | Utility bills, bank statements, or other documents that prove a residential address | Each document can be used **standalone** (for direct API-based verification) or **within a session** (as part of a full verification flow). ## Document IDV (`document/idv`) A `document/idv` represents a government-issued identity document. When processed through a verification, the system extracts personal data (name, date of birth, document number, etc.) and runs authenticity checks. ### Creating a Document IDV ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/documents/idvs \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "document/idv", "relationships": { "session": { "data": { "type": "session", "id": "sess_01HQ..." } } } } }' ``` The response includes **presigned upload URLs** for uploading document images: ```json theme={null} { "data": { "type": "document/idv", "id": "doc_01HQ...", "attributes": { "status": "pending", "document_type": null, "files": [], "front_side": null, "back_side": null, "portrait": null, "signature": null, "created_at": "2025-01-15T10:30:00Z", "submitted_at": null, "processed_at": null }, "relationships": { "session": { "data": { "type": "session", "id": "sess_01HQ..." } } } } } ``` ### Adding Files to a Document Upload document images by adding files: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/documents/idvs/{document_id}/files \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "document/idv", "attributes": { "filename": "front.jpg", "content_type": "image/jpeg", "byte_size": 245000 } } }' ``` This returns a presigned URL you can use to upload the actual file: ```bash theme={null} curl -X PUT "{presigned_upload_url}" \ -H "Content-Type: image/jpeg" \ --data-binary @front.jpg ``` For two-sided documents (e.g., driver's licenses), add both front and back images as separate files. ### Document IDV Statuses | Status | Description | | ----------- | ------------------------------------------------- | | `pending` | Document created, waiting for file upload | | `submitted` | Files uploaded, document submitted for processing | | `processed` | Processing complete, data extracted | | `failed` | Processing failed | ### Extracted Data After processing through a verification, the document is enriched with extracted data: * **Personal info**: Name, date of birth, birth place, sex, nationality * **Document details**: Document number, type, class, issue/expiry dates, issuing authority, issuing country * **Address**: Full address extracted from the document * **Images**: Cropped front side, back side, portrait photo, and signature images ## Document Address (`document/address`) A `document/address` represents a proof of address document. The flow is similar to `document/idv`: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/documents/addresses \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "document/address", "relationships": { "session": { "data": { "type": "session", "id": "sess_01HQ..." } } } } }' ``` After adding files, submit the document for processing: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/documents/addresses/{document_id}/submit \ -H "X-Api-Key: YOUR_API_KEY" ``` ## Standalone Documents Documents can be created **without** a session. This is useful when you want to verify a document through the API without the full session-based flow: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/documents/idvs \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "document/idv" } }' ``` You can then use the [Verifications API](/guides/identity/verifications) to run checks against the document. See [Running a Document Verification](/guides/identity/running-a-document-verification) for a complete walkthrough. ## File Requirements * **Supported formats**: JPEG, PNG * **Recommended resolution**: At least 300 DPI for best extraction accuracy * **File size**: Maximum 10MB per file * **Quality tips**: * Ensure the entire document is visible with no cropping * Avoid glare, shadows, and blurriness * Place the document on a contrasting background ## Next Steps Once your documents are uploaded, [run a verification](/guides/identity/verifications) to check their authenticity and extract identity data. # Identity API Overview Source: https://docs.beltic.com/guides/identity/overview Verify identities, manage accounts, and run document verifications with the Beltic Identity API. ## What is the Identity API? The Beltic Identity API provides a complete toolkit for identity verification and KYC (Know Your Customer) workflows. It lets you onboard users, collect and verify identity documents, and make decisions on applicants — all through a single, consistent API. Whether you need to verify a government-issued ID, collect proof of address, or manage the lifecycle of an applicant, the Identity API gives you the building blocks to do it programmatically. ## Core Resources The Identity API is built around four key resources that work together: Represent an external entity (a person or business) in your system. Use accounts to correlate your users with Beltic, track their verification status, and perform actions like blocking. Where applicant information is collected. Sessions hold identity data — entered manually or extracted automatically from documents — and track the overall verification progress. Represent identity documents such as government IDs or proof of address. Documents are uploaded, processed, and their data is extracted for verification. Run verification checks against documents. Each verification produces a set of checks with pass/fail results, giving you detailed insight into document authenticity. ## How It All Fits Together A typical identity verification flow looks like this: Create an account to represent the user in your system. You can store an `external_id` to link it back to your own database. Create a session linked to the account. The session collects all the applicant's personal information (name, date of birth, address, etc.). Create a `document/idv` (for government IDs) or `document/address` (for proof of address) and upload the document images. Create a verification linked to the document. Beltic processes the document, runs authenticity checks, extracts data, and returns the results. Based on the verification results, approve or decline the session. The account status reflects the overall state of the applicant. ## API-Only Document Verification If you don't need the full session-based flow, you can also run **standalone document verifications** entirely through the API. This is useful when you already have document images and just need to verify them. Learn how to verify a government ID in 3 API calls — no SDK or session required. ## JSON:API Format All Identity API endpoints follow the [JSON:API specification](https://jsonapi.org/). Resources are returned in a consistent format: ```json theme={null} { "data": { "type": "account", "id": "acc_01HQ...", "attributes": { "status": "active", "external_id": "user-123", "entity_type": "person", "person": { "name": { "first": "Jane", "last": "Doe" } } }, "relationships": { "sessions": { "data": [{ "type": "session", "id": "sess_01HQ..." }] } } } } ``` Key conventions: * **`type`** identifies the resource kind (`account`, `session`, `document/idv`, `verification/idv`, etc.) * **`attributes`** contains the resource data * **`relationships`** links to related resources * **`meta`** carries additional request/response metadata (e.g., presigned URLs, pagination) ## Authentication All requests require an API key passed via the `X-Api-Key` header. See [Authentication](/guides/authentication) for details. ```bash theme={null} curl -X GET https://api.beltic.com/v1/identity/accounts \ -H "X-Api-Key: YOUR_API_KEY" ``` ## Next Steps Learn how to create and manage accounts Run your first document verification in minutes # Running a Document Verification Source: https://docs.beltic.com/guides/identity/running-a-document-verification Verify a government ID document through the API — no SDK or session required. ## Overview This guide shows you how to verify a government-issued identity document (passport, driver's license, national ID) using only the Beltic API. This is the fastest path to document verification — no SDK integration, no session management, and no client-side setup required. You will: 1. Create a `document/idv` with file declarations and get presigned upload URLs 2. Upload the document images to the presigned URLs After the files are uploaded, the document is automatically processed — a verification is created, authenticity checks are run, and data is extracted from the document. ## Prerequisites * A Beltic API key with `documents:write` and `verifications:write` permissions * A document image (JPEG or PNG) of a government-issued ID ## Step 1: Create the Document Create a `document/idv` resource with the files you intend to upload declared in the request body. Since we're running a standalone verification, we don't need to link it to a session or account. ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/documents/idvs \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "document/idv", "attributes": { "files": [ { "filename": "id_card.jpeg" } ] } } }' ``` The response contains the document ID and presigned upload URLs for each declared file: ```json theme={null} { "data": { "type": "document/idv", "id": "doc_01HQ...", "attributes": { "status": "pending", "document_type": null, "files": [ { "id": "file_01...", "filename": "id_card.jpeg", "status": "pending_upload" } ], "created_at": "2025-01-15T10:30:00Z" } }, "meta": { "files": [ { "id": "file_01...", "presigned_upload_url": "https://files.beltic.com/doc_01HQ.../id_card.jpeg?X-Amz-Signature=...", "expires_in": 3600 } ] } } ``` For two-sided documents (like a driver's license or national ID card), declare both files in the request. This improves extraction accuracy and enables additional security checks. ```json theme={null} { "data": { "type": "document/idv", "attributes": { "files": [ { "filename": "id_card_front.jpeg" }, { "filename": "id_card_back.jpeg" } ] } } } ``` ## Step 2: Upload the Document Images Upload each file to its corresponding presigned URL using a PUT request: ```bash theme={null} curl -X PUT "{presigned_upload_url}" \ -H "Content-Type: image/jpeg" \ --data-binary @id_card.jpeg ``` The presigned URL expires after the time specified in `expires_in` (typically 1 hour). Upload your files before it expires. Once all files are uploaded, the document is **automatically processed** — Beltic runs the verification, performs authenticity checks, and extracts identity data from the document. No additional API call is needed. ## Retrieving the Results After uploading, poll the document to check when processing is complete: ```bash theme={null} curl -X GET https://api.beltic.com/v1/identity/documents/idvs/{document_id} \ -H "X-Api-Key: YOUR_API_KEY" ``` When the status changes from `pending` to `processed`, the document response includes all extracted data and the linked verification with check results: ```json theme={null} { "data": { "type": "document/idv", "id": "doc_01HQ...", "attributes": { "status": "processed", "document_type": "passport", "name": { "first": "Jane", "middle": null, "last": "Doe" }, "birth_date": "1990-05-15", "sex": "female", "document_number": "AB1234567", "issue_date": "2020-03-01", "expiry_date": "2030-03-01", "issuing_authority": "Department of State", "nationality": "US", "issuing_country": "US", "address": null, "front_side": { "id": "file_01...", "filename": "front_cropped.jpg" }, "portrait": { "id": "file_02...", "filename": "portrait.jpg" }, "back_side": null, "signature": null, "created_at": "2025-01-15T10:30:00Z", "submitted_at": "2025-01-15T10:30:05Z", "processed_at": "2025-01-15T10:30:08Z" } } } ``` You can also retrieve the verification directly to inspect individual check results: ```bash theme={null} curl -X GET "https://api.beltic.com/v1/identity/verifications?filter[document_id]=doc_01HQ..." \ -H "X-Api-Key: YOUR_API_KEY" ``` ### Verification Response ```json theme={null} { "data": [ { "type": "verification/idv", "id": "ver_01HQ...", "attributes": { "status": "passed", "capture_method": "api", "document_type": "passport", "name": { "first": "Jane", "middle": null, "last": "Doe" }, "birth_date": "1990-05-15", "document_number": "AB1234567", "expiry_date": "2030-03-01", "issuing_country": "US", "checks": [ { "name": "status_optical", "status": "passed", "requirement": "required", "reasons": [], "metadata": {} }, { "name": "optical_expiry", "status": "passed", "requirement": "required", "reasons": [], "metadata": {} }, { "name": "optical_mrz", "status": "passed", "requirement": "required", "reasons": [], "metadata": {} }, { "name": "optical_doc_type", "status": "passed", "requirement": "required", "reasons": [], "metadata": {} }, { "name": "image_quality_glare", "status": "not_applicable", "requirement": "not_required", "reasons": [], "metadata": {} } ], "created_at": "2025-01-15T10:30:05Z", "completed_at": "2025-01-15T10:30:08Z" } } ] } ``` ## Interpreting Results ### Overall Status | Status | What it means | What to do | | ---------------- | ------------------------------------------- | -------------------------------- | | `passed` | The document passed all required checks | Accept the verification | | `failed` | One or more required checks failed | Reject or investigate further | | `requires_retry` | Image quality issues prevented verification | Ask the user to retake the photo | ### Key Checks to Monitor For most use cases, pay attention to these checks: * **`status_optical`** — Was the document successfully read? * **`optical_expiry`** — Is the document expired? * **`optical_mrz`** — Does the MRZ (machine-readable zone) validate? * **`optical_security`** — Do security features check out? * **`image_quality_*`** — Was the image quality sufficient? ### Handling Failures ```javascript theme={null} const verification = response.data[0]; const { status, checks } = verification.attributes; if (status === 'passed') { console.log('Document verified successfully'); console.log('Name:', verification.attributes.name); console.log('DOB:', verification.attributes.birth_date); console.log('Doc #:', verification.attributes.document_number); } else if (status === 'requires_retry') { const retryChecks = checks.filter(c => c.requirement === 'requires_retry'); console.log('Please retake the photo. Issues:', retryChecks.map(c => c.name)); } else if (status === 'failed') { const failedChecks = checks.filter(c => c.status === 'failed'); console.log('Verification failed:', failedChecks.map(c => ({ check: c.name, reasons: c.reasons }))); } ``` ## Complete Example Here's a complete Node.js example that runs a document verification end-to-end: ```javascript theme={null} import fs from 'fs'; const API_KEY = 'YOUR_API_KEY'; const BASE_URL = 'https://api.beltic.com/v1/identity'; async function verifyDocument(imagePath) { const headers = { 'X-Api-Key': API_KEY, 'Content-Type': 'application/json' }; const filename = imagePath.split('/').pop(); // Step 1: Create the document with file declarations const docResponse = await fetch(`${BASE_URL}/documents/idvs`, { method: 'POST', headers, body: JSON.stringify({ data: { type: 'document/idv', attributes: { files: [{ filename }] } } }) }); const doc = await docResponse.json(); const documentId = doc.data.id; const uploadUrl = doc.meta.files[0].presigned_upload_url; console.log(`Created document: ${documentId}`); // Step 2: Upload the file await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': 'image/jpeg' }, body: fs.readFileSync(imagePath) }); console.log('File uploaded — processing automatically...'); // Step 3: Poll until processed let document; while (true) { const statusResponse = await fetch(`${BASE_URL}/documents/idvs/${documentId}`, { headers: { 'X-Api-Key': API_KEY } }); document = await statusResponse.json(); const status = document.data.attributes.status; if (status === 'processed') { console.log('Document processed!'); break; } else if (status === 'failed') { throw new Error('Document processing failed'); } console.log(`Status: ${status} — waiting...`); await new Promise(resolve => setTimeout(resolve, 3000)); } // Print extracted data const attrs = document.data.attributes; console.log(`Name: ${attrs.name?.first} ${attrs.name?.last}`); console.log(`DOB: ${attrs.birth_date}`); console.log(`Document #: ${attrs.document_number}`); console.log(`Expiry: ${attrs.expiry_date}`); return document; } // Usage verifyDocument('./id_card.jpeg'); ``` ## Tips for Best Results * Use high-resolution images (at least 300 DPI) * Ensure even lighting with no glare or shadows * Capture the full document with all edges visible * Place the document on a dark, contrasting background For documents with information on both sides (e.g., driver's licenses), declare and upload both images. This enables: * More complete data extraction * Additional security checks * Better overall verification accuracy The IDV verification engine supports a wide range of government-issued documents: * Passports * Driver's licenses * National identity cards * Residence permits * Travel documents * And many more ## Next Steps * **Need sessions?** Learn how to [run verifications within a session](/guides/identity/sessions) for full onboarding flows * **API Reference**: See the complete [Document IDV API reference](/api-reference/endpoint/identity-document-idvs-create) # Sessions Source: https://docs.beltic.com/guides/identity/sessions Sessions collect applicant information and track the verification lifecycle from creation to decision. ## What is a Session? A **Session** is where applicant information is collected and the verification process is tracked. Think of it as a verification case — it holds all the personal data, documents, and verification results for a single onboarding attempt. Sessions are the central hub of the verification workflow: * **Manual data entry**: You can set the applicant's name, date of birth, address, and other details directly on the session. * **Automatic extraction**: When a document is verified, extracted data (name, DOB, address, etc.) can be automatically written back to the session. * **Status tracking**: The session status reflects the overall progress of the verification — from creation through completion to a final decision. ## Session Statuses Sessions follow a lifecycle represented by these statuses: | Status | Description | | ------------------- | ----------------------------------------------------- | | `created` | Session has been created but no activity has occurred | | `started` | The applicant has begun the verification process | | `pending` | Information has been submitted and is being processed | | `completed` | All verification steps are complete | | `failed` | The verification process failed | | `marked_for_review` | The session requires manual review | | `approved` | A reviewer has approved the session | | `declined` | A reviewer has declined the session | | `expired` | The session expired before completion | ### Status Flow ``` created → started → pending → completed → approved → declined → marked_for_review → approved → declined → failed → expired ``` ## Creating a Session Create a session linked to an account: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/sessions \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "session", "attributes": { "entity_type": "person" }, "relationships": { "account": { "data": { "type": "account", "id": "acc_01HQ..." } } } } }' ``` ### Response ```json theme={null} { "data": { "type": "session", "id": "sess_01HQ...", "attributes": { "status": "created", "creator_type": "api", "entity_type": "person", "person": null, "business": null, "contact": null, "address": null, "identity_numbers": null, "created_at": "2025-01-15T10:30:00Z", "updated_at": "2025-01-15T10:30:00Z", "started_at": null, "expires_at": null, "completed_at": null, "failed_at": null, "marked_for_review_at": null, "decisioned_at": null, "expired_at": null, "redacted_at": null }, "relationships": { "account": { "data": { "type": "account", "id": "acc_01HQ..." } }, "documents": { "data": [] }, "verifications": { "data": [] }, "session_device": { "data": null } } } } ``` ## Populating Session Data You can provide applicant information in two ways: ### Manual Input Update the session directly with applicant details: ```bash theme={null} curl -X PATCH https://api.beltic.com/v1/identity/sessions/{session_id} \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "session", "id": "sess_01HQ...", "attributes": { "person": { "name": { "first": "Jane", "last": "Doe" }, "birth_date": "1990-05-15" }, "address": { "line1": "123 Main St", "city": "San Francisco", "subdivision": "CA", "postal_code": "94102", "country_code": "US" }, "contact": { "email": "jane@example.com", "phone": "+1234567890" } } } }' ``` ### Automatic Extraction from Documents When you create a verification with `auto_update_session` set to `true`, extracted data from the document is automatically written to the session: ```json theme={null} { "data": { "type": "verification/idv", "relationships": { "document": { "data": { "type": "document/idv", "id": "doc_01HQ..." } }, "session": { "data": { "type": "session", "id": "sess_01HQ..." } } }, "meta": { "auto_update_session": true } } } ``` When the verification completes, the session's `person` fields (name, date of birth, etc.) are populated from the document data — saving you from having to manually copy the information. Both `document/idv` (government ID) and `document/address` (proof of address) verifications can auto-update the session when linked. ## Making Decisions Once verification is complete, you can approve or decline the session: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/sessions/{session_id}/approve \ -H "X-Api-Key: YOUR_API_KEY" ``` ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/sessions/{session_id}/decline \ -H "X-Api-Key: YOUR_API_KEY" ``` These actions require the `sessions:decide` permission on your API key. ## Expiring a Session If a session is no longer needed or has been idle for too long, you can expire it: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/sessions/{session_id}/expire \ -H "X-Api-Key: YOUR_API_KEY" ``` ## Listing Sessions Retrieve sessions with optional filters: ```bash theme={null} # List all sessions for an account curl -X GET "https://api.beltic.com/v1/identity/sessions?filter[account_id]=acc_01HQ..." \ -H "X-Api-Key: YOUR_API_KEY" ``` ## Redacting a Session Remove all personal data from a session: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/sessions/{session_id}/redact \ -H "X-Api-Key: YOUR_API_KEY" ``` Redaction is irreversible. All personal data on the session will be permanently removed. ## Relationships Sessions connect to: * **Account** — The account this session belongs to * **Documents** — All documents uploaded during this session (`document/idv`, `document/address`) * **Verifications** — All verification results for this session * **Session Device** — Device information if the session was started from a client device ## Next Steps With a session created, you can now [upload documents](/guides/identity/documents) and [run verifications](/guides/identity/verifications) against them. # Verifications Source: https://docs.beltic.com/guides/identity/verifications Run verification checks against identity documents and understand the results. ## What is a Verification? A **Verification** is the result of running checks against an identity document. When you create a verification, Beltic processes the associated document, performs a series of authenticity and data-quality checks, and returns a detailed result. Verifications are the core of the Identity API's value — they tell you whether a document is genuine, whether the data can be trusted, and whether any issues were detected. ## Verification Types | Type | Resource Type | Description | | ----------- | ---------------------- | ---------------------------------------------------------------------------------- | | **IDV** | `verification/idv` | Verifies government-issued identity documents (passports, driver's licenses, etc.) | | **Address** | `verification/address` | Verifies proof of address documents | ## How Verification Works When you create a verification, the following happens synchronously: 1. **Document processing** — The document images are analyzed using advanced document recognition 2. **Data extraction** — Personal information is extracted from the document (name, DOB, document number, etc.) 3. **Check execution** — Multiple verification checks are run against the document 4. **Status determination** — An overall status is computed from individual check results 5. **Response** — The complete verification result is returned Verification creation is a **synchronous** operation. The response includes the full result — no polling required. ## Creating a Verification ### With a Session and Document The most common pattern — verify a document within a session: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/verifications \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "verification/idv", "relationships": { "document": { "data": { "type": "document/idv", "id": "doc_01HQ..." } }, "session": { "data": { "type": "session", "id": "sess_01HQ..." } } }, "meta": { "auto_update_session": true } } }' ``` ### Standalone (Without a Session) You can also run a verification without a session — useful for API-only integrations: ```bash theme={null} curl -X POST https://api.beltic.com/v1/identity/verifications \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "verification/idv", "relationships": { "document": { "data": { "type": "document/idv", "id": "doc_01HQ..." } } } } }' ``` ## Verification Response A verification response contains the status, extracted data, and check results: ```json theme={null} { "data": { "type": "verification/idv", "id": "ver_01HQ...", "attributes": { "status": "passed", "capture_method": "api", "document_type": "passport", "classification_type": "identity", "name": { "first": "Jane", "middle": null, "last": "Doe" }, "birth_date": "1990-05-15", "sex": "female", "document_number": "AB1234567", "issue_date": "2020-03-01", "expiry_date": "2030-03-01", "issuing_authority": "Department of State", "nationality": "US", "issuing_country": "US", "address": null, "checks": [ { "name": "status_optical", "status": "passed", "requirement": "required", "reasons": [], "metadata": {} }, { "name": "optical_expiry", "status": "passed", "requirement": "required", "reasons": [], "metadata": {} }, { "name": "optical_mrz", "status": "passed", "requirement": "required", "reasons": [], "metadata": {} } ], "files": [], "front_side": { "id": "file_01...", "filename": "front.jpg" }, "back_side": null, "portrait": { "id": "file_02...", "filename": "portrait.jpg" }, "signature": null, "created_at": "2025-01-15T10:35:00Z", "submitted_at": "2025-01-15T10:35:00Z", "completed_at": "2025-01-15T10:35:02Z" }, "relationships": { "session": { "data": { "type": "session", "id": "sess_01HQ..." } } } } } ``` ## Verification Statuses | Status | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `initiated` | Verification has been created and processing has started | | `passed` | All required checks passed | | `failed` | One or more required checks failed | | `requires_retry` | The verification could not be completed — typically due to image quality issues. The applicant should retry with better images. | | `canceled` | The verification was canceled | ## Understanding Checks Each verification includes an array of **checks** — individual tests performed against the document. Every check has: | Field | Description | | ------------- | ----------------------------------------------------------------------------------------- | | `name` | Identifier of the check (e.g., `optical_mrz`, `status_optical`) | | `status` | Result: `passed`, `failed`, or `not_applicable` | | `requirement` | How the check affects the overall result: `required`, `not_required`, or `requires_retry` | | `reasons` | Array of reason strings if the check failed | | `metadata` | Additional details about the check | ### Check Categories High-level status indicators from the document processing engine: * `status_optical` — Overall optical analysis result * `status_portrait` — Portrait/photo analysis result * `status_rfid` — RFID chip reading result (if applicable) Detailed analysis of the document's visual elements: * `optical_doc_type` — Document type recognition * `optical_expiry` — Document expiration check * `optical_image_qa` — Image quality assessment * `optical_mrz` — Machine Readable Zone validation * `optical_security` — Security feature analysis * `optical_text` — Text consistency and validity Advanced fraud detection checks: * `authenticity_uv_luminescence` — UV luminescence patterns * `authenticity_ir_b900` — Infrared B900 analysis * `authenticity_image_pattern` — Image pattern analysis * And more — the exact checks vary by document type and capture method Ensure the document images meet minimum quality standards: * `image_quality_glare` — Glare detection * `image_quality_focus` — Focus/sharpness assessment * `image_quality_resolution` — Resolution adequacy * And others for colorness, perspective, bounds, etc. ### How Status is Determined The overall verification status is computed from the individual checks: * If **any** check with `requirement: "required"` has `status: "failed"` → verification is `failed` * If **any** check has `requirement: "requires_retry"` → verification is `requires_retry` * If **all** required checks pass → verification is `passed` ## Auto-Update Session When you include `"auto_update_session": true` in the verification `meta`, the session is automatically updated with the extracted data from the document: * Person name (first, middle, last) * Date of birth * Sex * Address (if present on the document) This saves you from manually copying extracted data to the session after each verification. ## Listing Verifications Retrieve verifications with optional filters: ```bash theme={null} # All verifications for a session curl -X GET "https://api.beltic.com/v1/identity/verifications?filter[session_id]=sess_01HQ..." \ -H "X-Api-Key: YOUR_API_KEY" # Filter by status curl -X GET "https://api.beltic.com/v1/identity/verifications?filter[status]=passed" \ -H "X-Api-Key: YOUR_API_KEY" # Filter by type curl -X GET "https://api.beltic.com/v1/identity/verifications?filter[type]=verification/idv" \ -H "X-Api-Key: YOUR_API_KEY" ``` ## Next Steps For a complete end-to-end example of running a document verification through the API, see [Running a Document Verification](/guides/identity/running-a-document-verification). # Managing Templates Source: https://docs.beltic.com/guides/managing-templates Learn how to create, manage, and use document templates for consistent document processing ## Overview Document templates allow you to define reusable configurations for document processing. Templates include extraction schemas, fraud detection settings, and other processing parameters that can be applied to multiple documents. Templates are particularly useful when you process similar documents repeatedly, as they eliminate the need to specify configuration for each document individually. ## Benefits of Using Templates * **Consistency**: Ensure all documents of the same type are processed with identical settings * **Efficiency**: Avoid repeating configuration in every document creation request * **Maintainability**: Update template settings once to affect all future documents using that template * **Organization**: Group related processing configurations together ## Creating a Template ### Validation Field Pattern If you need date-based validation (for example "is this date recent enough?" or "is this date still valid?"), model it as: * A date source field using `"custom:type": "date"` * A separate boolean validation field with `beltic:validation` * A `field_ref` pointing to the sibling source date field This allows backend validation to compute the boolean result after extraction. ```json theme={null} { "type": "object", "properties": { "issue_date": { "type": ["string", "null"], "custom:type": "date" }, "is_recent": { "type": ["boolean", "null"], "beltic:validation": { "kind": "date_recency", "field_ref": "issue_date", "max_age_days": 90 } } }, "required": ["issue_date", "is_recent"] } ``` Create a JSON Schema that defines the structure of data you want to extract from documents. The schema must follow our [Schema rules](/guides/schema-requirements). ```json theme={null} { "type": "object", "properties": { "invoice_number": { "type": ["string", "null"], "description": "Invoice identifier" }, "amount": { "type": ["number", "null"], "description": "Total invoice amount" }, "date": { "type": ["string", "null"], "description": "Invoice date" }, "vendor": { "type": ["string", "null"], "description": "Vendor name" } }, "required": ["invoice_number", "amount", "date", "vendor"] } ``` Set up your extraction and fraud detection preferences: ```json theme={null} { "extraction_config": { "enabled": true, "schema": { // Your schema from step 1 }, "extraction_rules": "Extract all invoice details accurately." }, "fraud_config": { "enabled": true } } ``` Use the [Create Document Template](/api-reference/endpoint/document-templates-create) endpoint to create your template: ```bash theme={null} curl -X POST https://api.beltic.com/v1/document-templates \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "document-template", "attributes": { "name": "Invoice Processing Template", "description": "Template for processing invoices", "extraction_config": { "enabled": true, "schema": { "type": "object", "properties": { "invoice_number": { "type": ["string", "null"], "description": "Invoice identifier" }, "amount": { "type": ["number", "null"], "description": "Total invoice amount" }, "date": { "type": ["string", "null"], "description": "Invoice date" }, "vendor": { "type": ["string", "null"], "description": "Vendor name" } }, "required": ["invoice_number", "amount", "date", "vendor"] }, "extraction_rules": "Extract all invoice details accurately." }, "fraud_config": { "enabled": true } } } }' ``` The response includes the template ID which you'll use when creating documents. ## Listing Templates Retrieve all available templates using the [List Document Templates](/api-reference/endpoint/document-templates-list) endpoint: ```bash theme={null} curl -X GET "https://api.beltic.com/v1/document-templates?filter[is_active]=true" \ -H "X-Api-Key: YOUR_API_KEY" ``` ### Filtering Options * **Active templates only**: `?filter[is_active]=true` * **Published templates**: `?filter[status]=published` * **Pagination**: Use `page[size]` to control results per page (default: 15, max: 100) ### Example Response ```json theme={null} { "data": [ { "type": "document-template", "id": "123e4567-e89b-12d3-a456-426614174000", "attributes": { "name": "Invoice Processing Template", "description": "Template for processing invoices", "status": "published", "is_active": true, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } } ], "links": { "first": "/v1/document-templates?page[after]=...", "last": "/v1/document-templates?page[before]=..." } } ``` ## Retrieving a Template Get detailed information about a specific template using the [Get Document Template](/api-reference/endpoint/document-templates-get) endpoint: ```bash theme={null} curl -X GET https://api.beltic.com/v1/document-templates/{template_id} \ -H "X-Api-Key: YOUR_API_KEY" ``` The response includes the complete template configuration, including the extraction schema and fraud detection settings. ## Updating a Template Modify an existing template using the [Update Document Template](/api-reference/endpoint/document-templates-update) endpoint: ```bash theme={null} curl -X PATCH https://api.beltic.com/v1/document-templates/{template_id} \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "document-template", "id": "{template_id}", "attributes": { "name": "Updated Invoice Template", "extraction_config": { "schema": { // Updated schema } } } } }' ``` **Note**: Updates to templates only affect new documents created after the update. Documents already in processing will continue using the configuration that was active when they were created. ## Using Templates with Documents When creating a document, reference a template by providing the `document_template_id`: ```bash theme={null} curl -X POST https://api.beltic.com/v1/documents \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "document", "meta": { "document_template_id": "123e4567-e89b-12d3-a456-426614174000" } } }' ``` The document will automatically use the template's extraction schema and fraud detection configuration. ## Best Practices * Use descriptive names that clearly indicate the document type (e.g., "Invoice Processing", "Contract Analysis") * Include detailed descriptions explaining when to use each template * Start with essential fields and expand as needed * Use `description` fields to provide context for extraction * Remember that fields must be nullable: `"type": ["string", "null"]` * Test templates with sample documents before using in production * Monitor template usage to identify which templates need updates # Managing Webhooks Source: https://docs.beltic.com/guides/managing-webhooks Learn how to configure, manage, and troubleshoot webhook notifications for real-time document status updates ## Overview Webhooks provide real-time notifications when document processing status changes occur. When configured, your application will receive HTTP POST requests to specified endpoints whenever documents transition through different processing states. Webhooks are essential for building responsive applications that need immediate awareness of document processing results, failures, or other status changes. ## Webhook Delivery ### Delivery Mechanism Webhooks are delivered via HTTP POST requests to your configured endpoint: * **Method**: POST * **Content-Type**: application/json * **Authentication**: Include `X-Webhook-Token` header with your webhook token * **Payload**: JSON:API formatted document status change event ### Retry Policy Our webhook system implements intelligent retry logic: * **Initial Delivery**: Immediate attempt after status change * **Max Retries**: 5 attempts before marking as failed * **Dead Letter Queue**: Failed deliveries are queued for manual retry ### Security Always use HTTPS endpoints in production. HTTP is only allowed in sandbox environments. Verify the `X-Webhook-Token` header matches your stored webhook token. Store tokens securely - they cannot be retrieved after creation. Use the document ID and status from the webhook payload to handle duplicate deliveries gracefully. ## Verifying Webhook Signatures You can use the `X-Webhook-Signature` header to verify that the webhook payload was sent by Beltic. The signature is generated using an HMAC-SHA256 of the request body signed with your webhook secret. Here is a complete example of how to verify the signature in a Node.js application: ```typescript theme={null} import crypto from 'crypto'; /** * Example function to verify a webhook signature * * @param payload - The raw request body as a string * @param signature - The X-Webhook-Signature header value * @param secret - The secret key provided when creating the webhook * @returns boolean - True if signature is valid */ export function verifyWebhookSignature( payload: string, signature: string, secret: string ): boolean { if (!payload || !signature || !secret) { return false; } const expectedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); // Use timingSafeEqual to prevent timing attacks const signatureBuffer = Buffer.from(signature); const expectedBuffer = Buffer.from(expectedSignature); if (signatureBuffer.length !== expectedBuffer.length) { return false; } return crypto.timingSafeEqual(signatureBuffer, expectedBuffer); } // Example usage with Express import express from 'express'; const app = express(); // Use raw body parser to ensure exact payload matching app.use(express.raw({ type: 'application/json' })); app.post('/webhook', (req, res) => { const signature = req.headers['x-webhook-signature'] as string; const secret = process.env.WEBHOOK_SECRET; if (!verifyWebhookSignature(req.body.toString(), signature, secret)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body.toString()); // Process event... }); ``` ## Creating a Webhook Configuration Create an HTTPS endpoint capable of receiving POST requests with JSON payloads. The endpoint should: * Accept `application/json` content type * Validate the `X-Webhook-Token` header * Return 2xx status codes for successful processing * Handle duplicate deliveries gracefully ```javascript theme={null} // Example webhook receiver (Node.js/Express) app.post('/webhooks/documents', (req, res) => { const token = req.headers['x-webhook-token']; // Validate token if (token !== process.env.WEBHOOK_TOKEN) { return res.status(401).json({ error: 'Invalid token' }); } // Process webhook payload const { data } = req.body; console.log(`Document ${data.id} status changed to ${data.attributes.status}`); // Return success res.status(200).json({ received: true }); }); ``` Select which document status changes should trigger webhooks: * `processed`: Document processing completed successfully * `failed`: Document processing failed Use the [Create Webhook Config](/api-reference/endpoint/webhook-config-create) endpoint: ```bash theme={null} curl -X POST https://api.beltic.com/v1/webhooks/configs \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "webhook_config", "attributes": { "url": "https://your-app.com/webhooks/documents", "subscribed_statuses": ["processed", "failed"] } } }' ``` **Important**: Store the returned token securely - it cannot be retrieved again. ## Listing Webhook Configurations Retrieve all webhook configurations for your organization: ```bash theme={null} curl -X GET "https://api.beltic.com/v1/webhooks/configs" \ -H "X-Api-Key: YOUR_API_KEY" ``` ### Filtering Options * **Active configs only**: Include `?filter[is_active]=true` * **Pagination**: Use `page[size]` (default: 15, max: 100) ## Managing Webhook Configurations ### Retrieving a Specific Configuration Get detailed information about a webhook config: ```bash theme={null} curl -X GET https://api.beltic.com/v1/webhooks/configs/{config_id} \ -H "X-Api-Key: YOUR_API_KEY" ``` ### Updating a Configuration Modify webhook settings using the update endpoint: ```bash theme={null} curl -X PATCH https://api.beltic.com/v1/webhooks/configs/{config_id} \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "webhook_config", "id": "{config_id}", "attributes": { "url": "https://your-app.com/webhooks/documents/v2", "subscribed_statuses": ["processed", "failed"], "is_active": true } } }' ``` ### Deactivating a Configuration Temporarily disable a webhook without deleting it: ```bash theme={null} curl -X PATCH https://api.beltic.com/v1/webhooks/configs/{config_id} \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "webhook_config", "id": "{config_id}", "attributes": { "is_active": false } } }' ``` ## Webhook Payload Format All webhook deliveries use JSON:API format: ```json theme={null} { "data": { "type": "document", "id": "123e4567-e89b-12d3-a456-426614174000", "attributes": { "status": "processed", "created_at": "2026-01-16T10:00:00.000Z", "updated_at": "2026-01-16T10:30:00.000Z" }, "relationships": { "document_template": { "data": { "type": "document_template", "id": "456e7890-e89b-12d3-a456-426614174000" } } } }, "meta": { "event_type": "document.status_changed", "previous_status": "processing", "timestamp": "2026-01-16T10:30:00.000Z" } } ``` ## Handling Webhook Failures ### Manual Resend If a webhook was missed or needs to be re-delivered, use the resend endpoint: ```bash theme={null} curl -X POST https://api.beltic.com/v1/webhooks/resend \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "webhook_resend", "attributes": { "document_id": "123e4567-e89b-12d3-a456-426614174000" } } }' ``` This creates a new delivery attempt for the specified document/status combination. ## Testing Webhooks ### Development Testing Use tools like ngrok or localtunnel to expose local endpoints: ```bash theme={null} # Using ngrok to expose local port 3000 ngrok http 3000 # Configure webhook with ngrok URL curl -X POST https://api.beltic.com/v1/webhooks/configs \ -H "X-Api-Key: YOUR_SANDBOX_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "webhook_config", "attributes": { "url": "https://abc123.ngrok.io/webhooks/documents", "subscribed_statuses": ["processed", "failed"] } } }' ``` ### Manual Testing Test webhook delivery by manually resending: ```bash theme={null} # Resend webhook for a specific document curl -X POST https://api.beltic.com/v1/webhooks/resend \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "webhook_resend", "attributes": { "document_id": "your-document-id", "webhook_config_id": "your-webhook-config-id" } } }' ``` ## Best Practices * Use HTTPS endpoints with valid SSL certificates * Implement proper logging for webhook deliveries * Return appropriate HTTP status codes (200 for success, 4xx/5xx for errors) * Process webhooks asynchronously to avoid timeouts * Always validate the `X-Webhook-Token` header * Store webhook tokens securely (environment variables, secret management) * Implement rate limiting to protect against abuse * Use idempotency keys to handle duplicate deliveries * Log all webhook deliveries with timestamps and payloads * Implement alerting for high failure rates * Have fallback mechanisms for critical webhook-dependent features * Monitor webhook endpoint health and performance * Use different webhook URLs for different environments * Document webhook configurations and their purposes * Regularly review and clean up unused webhook configurations * Test webhook configurations after deployments ## Troubleshooting ### Common Issues **Webhook Not Received** * Check that the webhook configuration is active * Verify the endpoint URL is accessible and returns 2xx responses * Ensure the document status is in your subscribed statuses list **Authentication Failures** * Verify the `X-Webhook-Token` header is included * Confirm the token matches what was returned during configuration creation * Check for token encoding issues **Duplicate Deliveries** * Implement idempotency using document ID and status * Log all received webhooks to detect duplicates * Handle duplicate deliveries gracefully **Timeout Issues** * Process webhooks asynchronously in background jobs * Return immediate 202 Accepted responses * Implement proper error handling and retries in your application # Running an Extraction Source: https://docs.beltic.com/guides/running-an-extraction Complete guide to extracting data from documents using the Beltic Document API ## Overview Document extraction allows you to automatically extract structured data from documents using AI-powered processing. This guide walks you through the complete workflow from document creation to retrieving extracted results. ## Extraction Workflow The extraction process follows these steps: 1. **Create a document** - Initialize a document with extraction configuration 2. **Upload the file** - Upload your document file using the pre-signed URL 3. **Monitor processing** - Poll the document status until extraction completes 4. **Retrieve results** - Access the extracted data from the document response ## Step 1: Create a Document You can create a document in two ways: using a template or with ad-hoc configuration. If you have a [document template](/guides/managing-templates) set up, reference it when creating the document: ```bash theme={null} curl -X POST https://api.beltic.com/v1/documents \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "document", "meta": { "document_template_id": "123e4567-e89b-12d3-a456-426614174000" } } }' ``` The template provides the extraction schema and fraud detection settings automatically. Provide extraction configuration directly in the request: ```bash theme={null} curl -X POST https://api.beltic.com/v1/documents \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "document", "meta": { "extraction_config": { "enabled": true, "schema": { "type": "object", "properties": { "invoice_number": { "type": ["string", "null"], "description": "Invoice identifier" }, "amount": { "type": ["number", "null"], "description": "Total invoice amount" }, "date": { "type": ["string", "null"], "description": "Invoice date" }, "vendor": { "type": ["string", "null"], "description": "Vendor name" } }, "required": ["invoice_number", "amount", "date", "vendor"] }, "extraction_rules": "Extract all invoice details accurately." }, "fraud_config": { "enabled": true } } } }' ``` See the [Schema reference](/guides/schema-requirements) for schema requirements. ### Response The API returns a document object with a pre-signed upload URL: ```json theme={null} { "data": { "type": "document", "id": "9941d601-0dd4-4a33-8043-4f158b480f0e", "attributes": { "status": "pending_upload", "created_at": "2024-01-15T10:30:00Z" } }, "meta": { "presigned_upload_url": "https://files.beltic.com/9941d601-0dd4-4a33-8043-4f158b480f0e/file?X-Amz-Signature=...", "expires_in": 3600 } } ``` **Important**: The pre-signed URL expires after the time specified in `expires_in` (typically 1 hour). Upload your file before it expires. ## Step 2: Upload the File Upload your document file to the pre-signed URL using a PUT request: ```bash theme={null} curl -X PUT "{presigned_upload_url}" \ -H "Content-Type: application/pdf" \ --data-binary @invoice.pdf ``` ### File Requirements * **Maximum file size**: 50MB * **Supported formats**: PDF, images (PNG, JPEG) * **Content-Type**: Should match the file type (e.g., `application/pdf` for PDFs) ### Alternative: Using file\_url Instead of uploading, you can provide a `file_url` when creating the document to have Beltic download the file: ```bash theme={null} curl -X POST https://api.beltic.com/v1/documents \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "document", "meta": { "document_template_id": "123e4567-e89b-12d3-a456-426614174000", "file_url": "https://example.com/documents/invoice.pdf" } } }' ``` This skips the upload step and processing begins automatically once the file is downloaded. ## Step 3: Monitor Processing Status After uploading, the document status changes to `processing`. Poll the [Get Document](/api-reference/endpoint/documents-get) endpoint to check status: ```bash theme={null} curl -X GET https://api.beltic.com/v1/documents/{document_id} \ -H "X-Api-Key: YOUR_API_KEY" ``` ### Document Statuses * **pending\_upload**: Document created, waiting for file upload * **processing**: File uploaded, extraction in progress * **completed**: Extraction finished successfully * **failed**: Processing failed (check `error_code` and `error_message`) ### Polling Strategy * **Interval**: Poll every 5-10 seconds for most documents * **Timeout**: Set a maximum wait time (e.g., 5 minutes) based on your use case ## Step 4: Retrieve Extracted Data Once the document status is `completed`, the extracted data is available in the document response: ```bash theme={null} curl -X GET https://api.beltic.com/v1/documents/{document_id} \ -H "X-Api-Key: YOUR_API_KEY" ``` ### Response Structure ```json theme={null} { "data": { "type": "document", "id": "9941d601-0dd4-4a33-8043-4f158b480f0e", "attributes": { "status": "completed", "extracted_data": { "invoice_number": "INV-2024-001", "amount": 1250.50, "date": "2024-01-15", "vendor": "Acme Corporation" }, "fraud_result": { "score": "NORMAL", "file_metadata": { "producer": "Adobe PDF Library", "creator": "Microsoft Word", "creation_date": "2024-01-15T08:00:00Z", "mod_date": "2024-01-15T09:30:00Z", "author": "John Doe", "title": "Invoice", "keywords": null, "subject": null }, "indicators": [ { "id": "indicator-001", "type": "TRUST", "category": "Document Quality", "title": "High Quality Scan", "description": "Document shows high quality scanning with clear text", "origin": "QUALITY" } ], "document_classification": { "id": "class-001", "type": "invoice", "document_class_type": "financial", "detailed_type": "commercial_invoice" } }, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:32:15Z" } } } ``` ### Extracted Data Format The `extracted_data` object matches your extraction schema. Fields that couldn't be extracted will be `null`. ### Fraud Analysis If fraud detection is enabled, the response includes `fraud_result` with: * **score**: Risk assessment level - one of `"NORMAL"`, `"TRUSTED"`, `"WARNING"`, or `"HIGH_RISK"` * **file\_metadata**: Extracted file metadata including: * `producer`: Software that created the PDF * `creator`: Application used to create the document * `creation_date`: When the file was created * `mod_date`: When the file was last modified * `author`: Document author * `title`, `keywords`, `subject`: Additional metadata fields * **indicators**: Array of fraud detection indicators, each containing: * `id`: Unique indicator identifier * `type`: Indicator type - `"RISK"` (negative), `"TRUST"` (positive), or `"INFO"` (neutral) * `category`: Indicator category * `title`: Short indicator title * `description`: Detailed indicator description * `origin`: Source of indicator - `"FRAUD"` or `"QUALITY"` * **document\_classification**: Document type classification with: * `id`: Classification identifier * `type`: Document type * `document_class_type`: Document class category * `detailed_type`: Specific document subtype ## Error Handling When document processing fails, the document status is set to `failed` and the `processing_errors` array contains one or more error objects. Each error follows the JSON:API error format with the following structure: ```json theme={null} { "status": "500", "code": "EXTRACTION_FAILED", "title": "Data extraction failed", "detail": "Additional sanitized error details (may be null)", "meta": { "occurred_at": "2024-01-15T10:32:15Z" } } ``` ### Error Codes All error codes are sanitized and safe to expose to API consumers. Vendor names and internal implementation details are removed from error messages. General document processing failure. This error occurs when both extraction and fraud detection fail, or when an unexpected error occurs during processing. **Possible causes:** * Multiple processing steps failed simultaneously * Internal service error * Unhandled exception during processing **Resolution:** Retry the request. If the issue persists, try with a different file or contact support. The AI-powered data extraction process failed. This can occur due to document quality issues, unsupported formats, or extraction service errors. **Possible causes:** * Unsupported document format or structure * Extraction service unavailable or timeout * Document content doesn't match the extraction schema **Resolution:** * Verify document format is supported (PDF, PNG, JPEG) * Check that the extraction schema matches the document structure * Retry with a different file if the issue persists The fraud detection analysis failed. This occurs when the fraud detection service encounters an error while analyzing document authenticity. **Possible causes:** * Fraud detection service unavailable * Document format incompatible with fraud analysis * Internal fraud detection processing error **Resolution:** Retry the request. If the issue persists, try with a different file or contact support. Document processing exceeded the maximum allowed time. Processing was terminated to prevent indefinite waiting. **Possible causes:** * Very large or complex documents * Slow processing services * Network latency issues **Resolution:** * Try processing the document again * For large documents, consider splitting into smaller files * If the issue persists, contact support. The uploaded file exceeds the maximum allowed size of 50MB. **Resolution:** * Split large documents into smaller files * Use a file compression tool to reduce file size * Consider using a different file format that results in a smaller file size The file could not be processed due to format issues or corruption. **Possible causes:** * Unsupported file format * Corrupted or damaged file * File is not a valid document (e.g., empty file, wrong MIME type) * File structure is incompatible with processing **Resolution:** * Verify the file format is supported (PDF, PNG, JPEG) * Ensure the file is not corrupted * Try re-saving or re-exporting the document * Check that the file is a valid document and not empty The file at the provided `file_url` could not be accessed or downloaded when creating a document. **Possible causes:** * Invalid or malformed URL in `file_url` * File does not exist at the provided URL * URL requires authentication that wasn't provided * Network error preventing file download * URL is inaccessible (blocked, expired, or requires special permissions) * Server hosting the file returned an error (404, 403, 500, etc.) **Resolution:** * Verify the `file_url` is correct and accessible * Test the URL in a browser or with `curl` to ensure it's reachable * Ensure the URL doesn't require authentication or special headers * Check that the file server is not blocking requests from Beltic's IP addresses * For private files, use the pre-signed upload URL method instead of `file_url` * Verify the URL hasn't expired (for time-limited URLs) ### Handling Errors in Code Always check the document `status` field and handle `failed` status appropriately: ```javascript theme={null} async function checkDocumentStatus(documentId, apiKey) { const response = await fetch(`https://api.beltic.com/v1/documents/${documentId}`, { headers: { 'X-Api-Key': apiKey } }); const data = await response.json(); const status = data.data.attributes.status; if (status === 'failed') { const error = data.data.attributes.processing_errors[0]; console.error(`Error: ${error.code} (${error.status})`); console.error(`Title: ${error.title}`); console.error(`Detail: ${error.detail || 'No additional details'}`); // Handle specific error codes switch (error.code) { case 'FILE_TOO_LARGE': console.error('File is too large. Please compress or split the document.'); break; case 'INVALID_FILE': console.error('File format is invalid or corrupted. Please check the file.'); break; case 'TIMEOUT': console.error('Processing timed out. Please try again.'); break; default: console.error('Processing failed. Please retry or contact support.'); } } return data; } ``` ### Error Response Format When a document fails, the response includes a `processing_errors` array in the document attributes: ```json theme={null} { "data": { "type": "document", "id": "9941d601-0dd4-4a33-8043-4f158b480f0e", "attributes": { "status": "failed", "processing_errors": [ { "status": "500", "code": "EXTRACTION_FAILED", "title": "Data extraction failed", "detail": "Unable to extract data from document", "meta": { "occurred_at": "2024-01-15T10:32:15Z" } } ] } } } ``` ## Best Practices * Design schemas with realistic expectations - not all fields may be extractable * Provide clear `description` fields to guide extraction * Test schemas with sample documents before production use * Use high-quality scans or PDFs for best extraction results * Ensure text is readable and not obscured * Avoid heavily compressed or low-resolution images * For multi-page documents, ensure all pages are included * Use templates for repeated document types to reduce configuration overhead * Cache template IDs to avoid repeated lookups * Handle rate limits appropriately (429 status code) * Never expose API keys in client-side code * Use secure file URLs when providing `file_url` (HTTPS only) * Validate extracted data before using in business logic ## Complete Example Here's a complete example using a template: ```javascript theme={null} async function runExtraction(apiKey, templateId, file) { // Step 1: Create document const createResponse = await fetch('https://api.beltic.com/v1/documents', { method: 'POST', headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ data: { type: 'document', meta: { document_template_id: templateId } } }) }); const createData = await createResponse.json(); const documentId = createData.data.id; const uploadUrl = createData.meta.presigned_upload_url; console.log(`Created document: ${documentId}`); // Step 2: Upload file await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': 'application/pdf' }, body: file }); console.log('File uploaded, processing...'); // Step 3: Poll for completion while (true) { const docResponse = await fetch(`https://api.beltic.com/v1/documents/${documentId}`, { headers: { 'X-Api-Key': apiKey } }); const docData = await docResponse.json(); const status = docData.data.attributes.status; if (status === 'completed') { console.log('Extraction completed!'); console.log(docData.data.attributes.extracted_data); return docData; } else if (status === 'failed') { const error = docData.data.attributes.processing_errors?.[0]; console.error('Extraction failed!', error?.title || 'Unknown error'); throw new Error(error?.detail || 'Document processing failed'); } await new Promise(resolve => setTimeout(resolve, 5000)); } } // Usage const file = await fetch('invoice.pdf').then(r => r.blob()); await runExtraction('YOUR_API_KEY', '123e4567-e89b-12d3-a456-426614174000', file); ``` # Schema Requirements Source: https://docs.beltic.com/guides/schema-requirements JSON Schema reference for document extraction ## Overview We use [JSON Schema](https://json-schema.org/) to define the structure of extracted data. ## Rules * Root must be an `object` type * Allowed types: `string`, `number`, `integer`, `boolean`, `object`, `array` * Primitive fields must be nullable: `"type": ["string", "null"]` * Maximum nesting level: 3 * Array items: objects or primitives (`string`, `number`, `integer`, `boolean`) * Enums: strings only, must include `null` * Use `description` fields to provide context * Date values should use `"custom:type": "date"` on string fields * Validation rules should be modeled as a boolean field with `beltic:validation` and a sibling `field_ref` ## Unsupported Features * Schema composition (`anyOf`, `oneOf`, `allOf`) * Regular expressions * Conditional validation * Constant values ## Examples ### Basic Schema ```json theme={null} { "type": "object", "properties": { "invoice_number": { "type": ["string", "null"], "description": "Invoice identifier" }, "amount": { "type": ["number", "null"], "description": "Invoice amount" } }, "required": ["invoice_number", "amount"] } ``` ### With Nested Objects ```json theme={null} { "type": "object", "properties": { "address": { "type": "object", "properties": { "street": { "type": ["string", "null"] }, "city": { "type": ["string", "null"] } }, "required": ["street", "city"] } } } ``` ### With Arrays ```json theme={null} { "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": ["string", "null"] }, "quantity": { "type": ["number", "null"] } } } } } } ``` ### With Enums ```json theme={null} { "type": "object", "properties": { "status": { "enum": ["pending", "approved", "rejected", null], "description": "Document status" } } } ``` ### Validation Fields (Date Reference) Date validations are configured on a boolean field, not on the date field itself. Use this pattern: * Source date field: `type: ["string", "null"]` + `"custom:type": "date"` * Validation field: `type: ["boolean", "null"]` + `beltic:validation` * `field_ref` must point to a sibling date field name in the same scope ```json theme={null} { "type": "object", "properties": { "issue_date": { "type": ["string", "null"], "custom:type": "date", "description": "Document issue date" }, "is_recent": { "type": ["boolean", "null"], "description": "Whether issue_date is recent enough", "beltic:validation": { "kind": "date_recency", "field_ref": "issue_date", "max_age_days": 90 } } }, "required": ["issue_date", "is_recent"] } ``` ### Validation Fields in Nested Objects/Arrays `field_ref` is scope-relative: * Inside an object, it references a field in that same object * Inside an array item object, it references a field in each item ```json theme={null} { "type": "object", "properties": { "documents": { "type": "array", "items": { "type": "object", "properties": { "expiry_date": { "type": ["string", "null"], "custom:type": "date" }, "is_valid": { "type": ["boolean", "null"], "beltic:validation": { "kind": "date_expiry", "field_ref": "expiry_date", "min_valid_days": 30 } } }, "required": ["expiry_date", "is_valid"] } } }, "required": ["documents"] } ``` # Executing a Workflow Source: https://docs.beltic.com/guides/workflows/executing-a-workflow Run a workflow with POST /v1/workflows/execute — inputs, handling 200 vs 202 responses, pause points, and partial re-runs. ## Overview `POST /v1/workflows/execute` runs a workflow and returns the result. By default it uses the latest deployed version of the workflow. The call is synchronous — it holds the connection until the workflow reaches a terminal state or pauses. **Typical execution times:** | Workflow type | P50 | P95 | | --------------------------------- | ----- | ----- | | KYC (document + liveness) | 2.5s | 6s | | KYB (business lookup + sanctions) | 1.8s | 4s | | Sanctions screening only | 400ms | 900ms | | Credential issuance only | 120ms | 300ms | These are end-to-end wall-clock times under normal load. Design your UX accordingly — KYC flows warrant a loading state; credential-only calls are fast enough to be invisible. ## Prerequisites * A Beltic API key (`X-Api-Key` header — see [Authentication](/guides/authentication)) * A deployed workflow — configure and deploy workflows from the [Beltic Console](https://console.beltic.com) ## Basic Execution ```bash theme={null} curl -X POST https://api.beltic.com/v1/workflows/execute \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflowId": "wf_kyc_standard", "options": { "input": { "subject_id": "user_jane_doe_001" } } }' ``` ### Request Fields | Field | Type | Required | Description | | ------------------------------ | ------- | -------- | ------------------------------------------------ | | `workflowId` | string | Yes | ID of the deployed workflow to run | | `options.input` | object | No | Input data passed to the first block | | `options.environmentVariables` | object | No | Override environment variables for this run | | `options.workflowVariables` | object | No | Override workflow-level variables | | `options.maxParallelNodes` | number | No | Cap parallel block execution (default: 2) | | `useDraftState` | boolean | No | Run the draft workflow state instead of deployed | ## Handling the Response ### 200 — Terminal (Completed or Failed) The workflow ran to a terminal state. Check `success` and `status` to determine outcome: ```json theme={null} { "success": true, "status": "completed", "executionId": "exec_01HQ7P...", "output": { "document_verification": { "status": "passed", "document_type": "passport", "name": { "first": "Jane", "last": "Doe" }, "birth_date": "1990-05-15", "nationality": "US" }, "sanctions_screening": { "status": "clear" }, "credential_issue": { "credential_id": "cred_01HQ...", "credential_type": "user", "signed_payload": "eyJhbGciOiJFUzI1NiIs...", "status": "active", "issued_at": "2026-05-21T10:30:00Z", "expires_at": "2026-08-19T10:30:00Z" } } } ``` If a block failed, `success` will be `false` and `error` will describe what went wrong: ```json theme={null} { "success": false, "status": "failed", "executionId": "exec_01HQ7Q...", "error": "document_verification: document expired", "output": { "document_verification": { "status": "failed", "reason": "optical_expiry" } } } ``` ### 202 — Paused The workflow reached a block that requires external input before it can continue — a manual compliance review, sponsor bank approval, or a human-in-the-loop decision: ```json theme={null} { "success": false, "status": "paused", "executionId": "exec_01HQ7R...", "pausePoints": [ { "blockId": "sponsor_bank_review", "reason": "policy_mismatch", "description": "Subject's jurisdiction requires sponsor bank sign-off", "requiredInput": ["approval_reference", "reviewer_id"] } ] } ``` Store the `executionId`. When the external review is complete, resume by running `POST /v1/workflows/execute` again with `runFromBlock` pointing to the block after the pause point. ## Resuming a Paused Execution ```bash theme={null} curl -X POST https://api.beltic.com/v1/workflows/execute \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflowId": "wf_kyc_standard", "runFromBlock": { "executionId": "exec_01HQ7R...", "startBlockId": "credential_issue" }, "options": { "input": { "approval_reference": "BANK-2026-00412", "reviewer_id": "rev_jones" } } }' ``` The resumed execution picks up state from `exec_01HQ7R...` and runs from `credential_issue` forward, skipping the already-completed blocks. ## Listing Past Executions Check the history of any workflow: ```bash theme={null} curl "https://api.beltic.com/v1/workflows/wf_kyc_standard/executions" \ -H "X-Api-Key: $BELTIC_API_KEY" ``` ```json theme={null} [ { "executionId": "exec_01HQ7P...", "status": "completed", "updatedAt": "2026-05-21T10:30:10Z" }, { "executionId": "exec_01HQ7R...", "status": "paused", "updatedAt": "2026-05-21T09:15:02Z" } ] ``` ## Testing with Draft State Before deploying a workflow change, run it against draft state to validate the changes: ```bash theme={null} curl -X POST https://api.beltic.com/v1/workflows/execute \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflowId": "wf_kyc_standard", "useDraftState": true, "options": { "input": { "subject_id": "test_user_001" } } }' ``` Never run draft state in production. Use a staging API key (`sk_staging_*`) when testing draft workflows. ## Error Codes | HTTP | Meaning | | ----- | ---------------------------------------------------------------------------------------------- | | `400` | Request body failed validation | | `401` | API key missing or invalid | | `403` | API key lacks execution permissions | | `404` | Workflow ID not found or not deployed | | `409` | Execution ID conflict (duplicate idempotency key) | | `422` | Workflow is valid but semantically unexecutable (e.g. `runFromBlock` references unknown block) | ## Next Steps Deep-dive into the credential that workflows emit How to verify the signed credential at transaction time Full end-to-end example — workflow execution, sponsor bank queue, and transaction authorization # Workflow API Overview Source: https://docs.beltic.com/guides/workflows/overview The Beltic Workflow API is the execution plane for verification and credentialing — replace multi-step API sequences with a single call. ## What is the Workflow API? The Workflow API is the execution plane that orchestrates everything Beltic does — document verification, identity checks, sanctions screening, business lookups, and credential issuance — into a single callable process. Instead of manually sequencing eight API calls (create account → create session → upload document → run verification → check screening → issue credential), you configure a workflow once and call `POST /v1/workflows/execute`. The workflow runs the blocks in order and returns the output, including any signed credentials, in a single response. ```bash theme={null} curl -X POST https://api.beltic.com/v1/workflows/execute \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflowId": "wf_kyc_standard", "options": { "input": { "subject_id": "user_jane_doe_001", "document_type": "passport" } } }' ``` ## How Workflows Are Structured A workflow is a state graph of **blocks** — discrete units of work that run in sequence or in parallel. Each block type wraps a specific Beltic capability: | Block type | What it does | | ----------------------- | -------------------------------------------------------------------------------------------- | | `document_verification` | Extracts and verifies a government-issued ID or proof of address | | `business_lookup` | Looks up company registration data by name or registration number | | `sanctions_screening` | Runs the subject against global sanctions, PEP, and adverse media lists | | `identity_verification` | Runs liveness and document-match checks | | `credential_issue` | Issues a signed JWT-VC credential from the verified data — this is typically the final block | The credential block is what turns a verification outcome into a **portable, cryptographic artifact**. The signed credential the workflow emits can be stored, forwarded to partners, and verified at every future transaction point — without re-running the workflow. ## Synchronous vs Paused Executions Workflow execution is synchronous by default. The HTTP response tells you the outcome: | Status | HTTP code | Meaning | | ----------- | --------- | ----------------------------------------------------------------------------------------------------------------- | | `completed` | `200` | Workflow ran to completion. Output contains the credential. | | `failed` | `200` | Workflow ran but a block failed (e.g. document rejected). Check `output` for details. | | `paused` | `202` | Workflow paused — awaiting external input (e.g. manual review at a sponsor bank). Resume when input is available. | | `cancelled` | `200` | Workflow was cancelled mid-run. | ### The 202 Pause Flow Some verification steps can't be resolved automatically — a policy mismatch may require a manual review from a compliance officer or a sponsor bank before a credential can be issued. When this happens, the workflow pauses and returns `202` with a `pausePoints` array describing what input is needed to continue. ```json theme={null} { "success": false, "status": "paused", "executionId": "exec_01HQ...", "pausePoints": [ { "blockId": "sponsor_bank_review", "reason": "policy_mismatch", "requiredInput": ["approval_reference", "reviewer_id"] } ] } ``` Once the external review completes, resume the execution by re-running with the `runFromBlock` parameter pointing to the block after the pause. ## Execution Output Every completed execution returns an `output` object keyed by block ID: ```json theme={null} { "success": true, "status": "completed", "executionId": "exec_01HQ7P...", "output": { "document_verification": { "status": "passed", "document_type": "passport", "name": { "first": "Jane", "last": "Doe" }, "birth_date": "1990-05-15" }, "sanctions_screening": { "status": "clear" }, "credential_issue": { "credential_id": "cred_01HQ...", "credential_type": "user", "signed_payload": "eyJhbGciOiJFUzI1NiIs...", "status": "active", "expires_at": "2026-08-19T10:30:00Z" } } } ``` The `signed_payload` in the credential block output is the JWT-VC. Store it — it's the artifact your downstream services verify against. ## Configuring a Workflow Workflows are configured in the [Beltic Console](https://console.beltic.com) using a visual block editor. You add blocks, connect them in order, configure each block's inputs and conditions, and deploy — similar to how you'd configure a CI/CD pipeline, but for verification logic. **What happens at deploy time:** Beltic validates the workflow graph, assigns a stable `workflowId` (e.g. `wf_kyc_standard`), and creates an immutable deployed version. The deployed version is what `POST /v1/workflows/execute` runs by default. Earlier versions are preserved and can still be targeted via the executions API for debugging. **Draft vs deployed:** While editing, the workflow is in draft state. You can test draft workflows by passing `"useDraftState": true` to `POST /v1/workflows/execute` — useful for staging validation before promoting a change to production. **Code-defined workflows** (declarative YAML/JSON config and Git-based deployment) are on the roadmap. The REST API for workflow execution is already stable and versioned — switching from Console-deployed to code-defined workflows will not require changes to your integration. *** ## Workflows vs Direct API Calls The Workflow API supersedes the Identity API for most use cases. The direct comparison: | | Identity API (legacy) | Workflow API | | ------------------- | --------------------- | ----------------------- | | Number of API calls | 5–8 per verification | 1 | | Credential issuance | Manual, separate call | Built into the workflow | | Pause / resume | Not supported | Native | | Audit trail | Partial | Full, per-block | | Subworkflows | No | Yes | The Identity API remains available for existing integrations but is not recommended for new builds. ## Where to Next Walk through POST /v1/workflows/execute — inputs, outputs, pause handling, and partial re-runs Understand the signed credential that workflows emit — and how to verify it at transaction time See workflows and credentials working together — from user onboarding through transaction authorization # Getting Started Source: https://docs.beltic.com/index Beltic is a verification platform for humans, businesses, and AI agents. Configure a workflow once, trigger it with a single API call. ## What Beltic Does Beltic provides programmable infrastructure for verifying the entities that matter to your business — people, companies, and AI agents. Everything in Beltic is built around the **Workflow API**. A workflow is a pipeline of blocks — discrete verification steps like document checks, liveness, sanctions screening, and business lookups — that you configure once in the Console and trigger with a single API call. Beltic runs the blocks, handles retries and parallel execution, and returns the full block-by-block output. **Credentialing is one of those blocks.** When you add a `credential_issue` block to your workflow, Beltic takes the verified output of the preceding blocks and mints a signed JWT-VC from it — a cryptographic artifact that encodes what was verified, who was verified, and when. That credential can be stored, forwarded to partners, and verified at every future transaction point without re-running the workflow. Not every customer needs the full workflow. If you already have your own verification process — your own KYC provider, your own document stack, your own risk engine — you can skip the workflow entirely and call the **Credentials API** directly. Send Beltic the data you've already collected, and Beltic wraps it in a signed, portable credential. You get the cryptographic trust layer without changing your existing pipeline. Two patterns, same result: | Pattern | When to use | | ------------------------------- | ------------------------------------------------------------------------------------------- | | **Workflow + credential block** | You want Beltic to run the verification and issue the credential in one call | | **Direct credential issuance** | You have your own verification process and want to attach the result to a Beltic credential | ## Quickstart ### 1. Get an API key Create an account at [console.beltic.com](https://console.beltic.com), navigate to **Settings → API Keys**, and create a key with `workflows:execute` scope. Use a `sk_staging_...` key while testing. ### 2. Execute a workflow `wf_kyc_standard` is a placeholder — replace it with the ID of your own workflow. When setting up a workflow in the Console, you choose its name; that name becomes the `workflowId` you pass here. ```bash curl theme={null} curl -X POST https://api.beltic.com/v1/workflows/execute \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflowId": "wf_kyc_standard", "options": { "input": { "subject_id": "user_jane_doe_001", "document_type": "passport", "document_front_url": "https://your-storage.com/passport_front.jpg", "jurisdiction": "US" } } }' ``` ```typescript TypeScript theme={null} const response = await fetch('https://api.beltic.com/v1/workflows/execute', { method: 'POST', headers: { 'X-Api-Key': process.env.BELTIC_API_KEY!, 'Content-Type': 'application/json', }, body: JSON.stringify({ workflowId: 'wf_kyc_standard', options: { input: { subject_id: 'user_jane_doe_001', document_type: 'passport', document_front_url: 'https://your-storage.com/passport_front.jpg', jurisdiction: 'US', }, }, }), }); const result = await response.json(); ``` ### 3. Handle the result A `200` response means the workflow reached a terminal state — check `result.status` for `completed` or `failed`. A `202` means the workflow paused and is waiting on external input (e.g. a manual compliance review) before it can continue. That's the core loop. From here, you can read the output directly, trigger downstream logic, or optionally use the Credentials API to package the result into a portable signed JWT for future use. **TypeScript SDK — coming soon.** A first-class TypeScript SDK with generated types, automatic retries, and environment handling is in active development. The REST API is stable and production-ready today. [Sign up for early access →](https://beltic.com) *** ## Core APIs The execution plane. Configure verification pipelines once and trigger them with a single call. Built-in pause/resume for human-in-the-loop steps. Package a verification result into a portable, cryptographically signed JWT. Present it at future checkpoints without re-running the workflow. Extract structured data from documents with AI-powered processing and fraud detection. Create and manage business entities. Look up registered companies and track their verification state across your platform. Run sanctions, PEP, and adverse media checks against individuals and entities. Integrates directly into workflows or as a standalone call. Analyse a business's web presence — category classification, risk signals, and policy compliance — in a single request. *** ## Examples End-to-end walkthrough: user signup, sponsor bank queue, and transaction authorization — with exactly what your backend calls and what your user sees at every step. Cryptographic proof that a verified human authorized an agentic transaction — with scoped permissions any endpoint can verify. *** ## Authentication All requests require an API key in the `X-Api-Key` header: ```http theme={null} X-Api-Key: sk_production_... ``` Use `sk_staging_...` keys in development. Keys are created in the [Console](https://console.beltic.com) and scoped per permission. See [Authentication](/guides/authentication) for rate limits, retry guidance, and idempotency. # Agent Authorization Source: https://docs.beltic.com/use-cases/agent-authorization How to issue cryptographic proof that a real, verified human authorized an agentic transaction — so any counterparty can trust the agent without trusting the platform. ## Products Used in This Guide Everything in Beltic is built around the **Workflow API**. A workflow is a pipeline of verification blocks that you configure once and trigger with a single call. **Credentialing is one of those blocks** — add a `credential_issue` block and Beltic mints a signed JWT from the verified output automatically. This guide uses the Workflow API to verify the human first, with the `credential_issue` block producing the identity credential the agent chain depends on. It then uses the **Credentials API** directly to issue the `agent_authorization` credential — because the agent credential isn't an output of a verification workflow, it's a standalone authorization that references the already-verified human. If you have your own verification process, you can skip the workflow entirely and call the Credentials API directly — send Beltic the data you've already collected and it wraps it in a signed credential. The trust layer works the same way regardless of how the data got there. *** ## The Problem AI agents can now initiate transactions autonomously — placing orders, authorizing payments, booking services. The infrastructure to *execute* these transactions exists. What doesn't exist is a standard way to prove that a real human authorized them. When an agent arrives at a payment endpoint today, the receiving system has no reliable way to answer: did a verified human authorize this? Under what constraints? Is that authorization still valid? Was the human even present? Without that proof, every agentic transaction is a trust gap. Payment processors flag it. Banks can't satisfy AML requirements. Fraud systems default to blocking it. Beltic closes that gap with a signed, portable credential that travels with the agent and carries the human's verification & authorization as a cryptographic fact. But that credential is only as strong as the identity behind it — which means the story starts with verifying the human first. *** ## Step 1: Verify the Human Through a Workflow Before any agent can be authorized, the human delegating to it needs to be verified. This is where Beltic's workflow engine comes in. A workflow is a configured pipeline of verification blocks — each block performs a discrete step. You define the pipeline once in the Console, then trigger it with a single API call. Beltic runs every block in sequence, handles edge cases internally, and returns the complete output when done. For a KYC verification, a typical workflow runs blocks like these in order: * **`identity_check`** — validates the document, checks name and DOB * **`liveness_check`** — confirms the person is physically present * **`sanctions_screening`** — runs against OFAC, UN, EU watchlists * **`credential_issue`** — mints a signed JWT-VC from the verified output You trigger it with one call: ```bash theme={null} curl -X POST https://api.beltic.com/v1/workflows/execute \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflowId": "wf_kyc_standard", "options": { "input": { "subject_id": "user_jane_doe_001", "entity_type": "person", "document_type": "passport", "document_front_url": "https://your-storage.com/docs/jane_passport_front.jpg", "jurisdiction": "US" } } }' ``` The response contains the block-by-block output of every step that ran: ```json theme={null} { "success": true, "status": "completed", "executionId": "exec_01HQ7P4...", "output": { "identity_check": { "status": "passed", "document_type": "passport", "name_match": true, "dob_match": true }, "liveness_check": { "status": "passed", "confidence": 0.98 }, "sanctions_screening": { "status": "clear", "lists_checked": ["OFAC", "UN", "EU"] }, "credential_issue": { "credential_id": "cred_01HQ7P4M...", "credential_type": "user", "subject": { "id": "user_jane_doe_001", "type": "person" }, "claims": { "kyc_status": "approved", "trust_level": "idv_verified", "verified_at": "2026-05-22T10:30:00Z", "jurisdiction": "US" }, "signed_payload": "eyJhbGciOiJFUzI1NiIs...", "status": "active", "issued_at": "2026-05-22T10:30:00Z", "expires_at": "2027-05-22T10:30:00Z" } } } ``` Each key in `output` is a block. The `credential_issue` block at the end takes the verified output of every preceding block and mints a signed credential from it — a tamper-proof record that the user passed every check. **Store `credential_issue.credential_id` and `signed_payload` against the user record.** The `subject.id` — `user_jane_doe_001` — is now the anchor for any agent the user subsequently authorizes. The chain that starts here is: verified human → authorized agent → agent transaction. Replace `wf_kyc_standard` with the ID of your own workflow. When setting up a workflow in the Console, you choose its name — that name becomes the `workflowId` you pass here. *** ## Step 2: Issue the Agent Authorization Credential The user has been verified. Now they open your app and configure what their agent can do — which resources, which actions, up to what spend. Your backend receives that confirmation and calls Beltic to mint the agent credential: ```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 '{ "credential_type": "agent_authorization", "subject": { "id": "agent_jane_assistant_session_8821", "type": "agent", "name": "User's Assistant" }, "claims": { "delegated_by_subject_id": "user_jane_doe_001", "role": ["payment_agent"], "permissions": [ { "resource_type": "wallet", "resource_id": "*", "actions": ["payment_authorize", "checkout"], "conditions": [ { "field": "transaction_amount", "op": "lte", "value": 15000 }, { "field": "transaction_currency", "op": "eq", "value": "usd" } ] } ], "spend_limit": { "amount": 50000, "currency": "usd", "period": "daily" }, "max_idle_duration": "PT30M", "human_present": true }, "expires_at": "2026-05-22T18:00:00Z" }' ``` Two fields carry the human proof: * **`delegated_by_subject_id`** — the `subject.id` of the KYC-verified user from Step 1. This is the cryptographic link that lets any verifier trace the agent back to a real, verified identity. Required on any wallet-scoped permission. * **`human_present: true`** — attests that the human was actively present and confirmed this authorization at issuance time. A signal downstream systems rely on for AML and fraud scoring. The response contains a `signed_payload` JWT. Give this to the agent runtime — it presents it on every downstream call. *** ## Step 3: The Agent Transacts — the Credential Travels With It When the agent reaches a checkout or payment endpoint, it includes the `signed_payload` JWT in the request. The merchant or payment processor receiving it calls Beltic's public verify endpoint — no API key, no Beltic account needed: ```bash theme={null} curl -X POST https://api.beltic.com/v1/credentials/_public/verify \ -H "Content-Type: application/json" \ -d '{ "credential": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Iks4TDkuLi4ifQ...", "context": { "resource_type": "wallet", "resource_id": "*", "action": "payment_authorize", "transaction_amount": 8500, "transaction_currency": "usd" } }' ``` **Permitted:** ```json theme={null} { "valid": true, "credential_type": "agent_authorization", "subject": { "id": "agent_jane_assistant_session_8821", "type": "agent" }, "claims": { "delegated_by_subject_id": "user_jane_doe_001", "human_present": true, "spend_limit": { "amount": 50000, "currency": "usd", "period": "daily" } }, "policy_match": { "permitted": true, "matched_permission": { "resource_type": "wallet", "actions": ["payment_authorize", "checkout"] } }, "status": "active", "verified_at": "2026-05-22T11:05:00Z", "verification_id": "ver_01HQ9B3R..." } ``` What the verifier can now assert with cryptographic confidence: * A real, KYC-verified human (`user_jane_doe_001`) authorized this agent — traceable back to a workflow that ran identity check, liveness, and sanctions screening * The human was actively present at authorization time * This specific action (`payment_authorize`, \$85, USD) is within the agent's permitted scope * The authorization is still valid — not expired, not revoked * The daily spend limit has not been exceeded **Action outside permitted scope:** ```json theme={null} { "valid": true, "policy_match": { "permitted": false, "reason": "action_not_permitted", "detail": "Action 'refund_issue' is not listed in the agent's permissions" } } ``` **Spend limit exceeded:** ```json theme={null} { "valid": false, "reason": "spend_limit_exceeded", "detail": "Daily spend limit of $500.00 USD would be exceeded by this transaction" } ``` *** ## Step 4: Attest the Transaction Outcome For high-value transactions or regulated flows, wrap the final outcome in a signed `outcome_attestation` credential. This creates a portable record that the transaction was authorized by a verified human and completed — useful for AML audit trails, sponsor bank reporting, or dispute resolution. ```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": "outcome_attestation", "attestation_type": "transaction_attested", "subject": { "id": "txn_8821_checkout", "type": "transaction" }, "claims": { "transaction_id": "txn_8821_checkout", "amount": 8500, "currency": "usd", "credential_id": "cred_01HQ9A2M...", "verification_id": "ver_01HQ9B3R...", "approved_by_user": true, "attested_at": "2026-05-22T11:05:12Z" } }' ``` The result is a signed JWT that any auditor, regulator, or sponsor bank can verify: at this timestamp, this transaction was authorized by a verified human, executed by their agent, within the permitted scope. *** ## Step 5: Revoke When the Session Ends Agent credentials should be short-lived. When the session ends, the user removes the agent, or a suspicious action is detected — revoke immediately: ```bash theme={null} curl -X POST https://api.beltic.com/v1/credentials/cred_01HQ9A2M.../revoke \ -H "X-Api-Key: $BELTIC_API_KEY" ``` Revocation propagates to the Status List bitstring within seconds. Every downstream system that calls Beltic to verify this credential gets `valid: false` — instantly, with no coordination between platforms. The agent is locked out everywhere at once. *** ## The Complete Chain ``` KYC workflow executes └── identity_check block → passed └── liveness_check block → passed └── sanctions_screening block → clear └── credential_issue block → user credential (user_jane_doe_001, idv_verified) User authorizes their agent └── POST /v1/credentials (agent_authorization) delegated_by_subject_id: user_jane_doe_001 human_present: true permissions: [wallet → payment_authorize, checkout, max $150] → agent credential (signed_payload handed to agent runtime) Agent reaches checkout └── POST /v1/credentials/_public/verify (with transaction context) → valid: true, policy_match: permitted → verifier knows: real human, verified identity, authorized action, within limit Transaction completed └── POST /v1/credentials (outcome_attestation) → signed record linked to credential_id + verification_id ``` Each step is cryptographically linked to the one before it. A compliance officer, regulator, or bank can follow the chain from a specific transaction back to the identity verification workflow that authorized it — without calling your platform. *** ## Related Full reference for POST /v1/workflows/execute — block outputs, pause/resume, and all response shapes Full reference for agent\_authorization credential claims and all four credential types The same identity credential used in a fintech onboarding and transaction authorization context The full verify pipeline — context evaluation, policy matching, and rejection codes # Fintech Company Using Beltic Source: https://docs.beltic.com/use-cases/fintech-onboarding A complete data flow walkthrough — from user signup through transaction authorization. What your backend calls, what your user sees, and where Beltic fits at every step. ## Products Used in This Guide Everything in Beltic is built around the **Workflow API**. A workflow is a pipeline of verification blocks — document checks, liveness, sanctions screening, business lookups — that you configure once and trigger with a single API call. **Credentialing is one of those blocks.** Add a `credential_issue` block to your workflow and Beltic automatically mints a signed JWT from the verified output. That credential travels with the user from onboarding through every future transaction — no re-verification needed. If you already have your own verification process, you can skip the workflow and call the **Credentials API** directly. Send Beltic the data you've collected and it wraps it in a signed, portable credential. Same trust layer, without changing your existing pipeline. This guide uses both — the Workflow API at onboarding to verify and credentialise in one call, and the Credentials API at transaction time to verify the credential against the payment context. *** ## The Setup You're a developer at a fintech company — a stablecoin wallet, a card issuer, a payment platform. You need to verify users before they can transact, and you need to check their authorization on every payment they make. You call Beltic at exactly two moments: 1. **At signup** — verify the user, handle the result, get them onboarded as fast as possible 2. **At every transaction** — verify their credential with transaction context and sending an attestation with verification, authorization and transaction, downstream Everything in between — the verification logic, document checks, sanctions screening, credential signing, revocation infrastructure — runs inside Beltic. Your backend makes API calls. Your product owns the UX. *** ## Part 1: User Onboarding User onboarding verification flow ### What your user sees The user opens your app and signs up. They fill in their details, upload a document, and hit submit. They see a loading state. Seconds later they're either in — or they see "we're reviewing your application." That's it. Two outcomes, both handled by a single API call. ### What your backend does The moment the user hits submit, your server calls `POST /v1/workflows/execute`: ```bash theme={null} curl -X POST https://api.beltic.com/v1/workflows/execute \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "workflowId": "wf_kyc_standard", "options": { "input": { "subject_id": "user_jane_doe_001", "entity_type": "person", "document_type": "passport", "document_front_url": "https://your-storage.com/docs/jane_passport_front.jpg", "jurisdiction": "US" } } }' ``` Beltic runs the verification pipeline — document check, liveness, sanctions screening — and responds. The whole thing takes seconds. Replace `wf_kyc_standard` with the ID of your own workflow. When setting up a workflow in the Console, you choose its name — that name becomes the `workflowId` you pass here. *** ### Outcome A: User approved immediately (`200`) Beltic returns `200`. The workflow completed. The document checked out, sanctions came back clear, all policies passed. ```json theme={null} { "success": true, "status": "completed", "executionId": "exec_01HQ7P4...", "output": { "identity_check": { "status": "passed", "trust_level": "idv_verified" }, "sanctions_screening": { "status": "clear" }, "credential_issue": { "credential_id": "cred_01HQ7P4M...", "credential_type": "user", "signed_payload": "eyJhbGciOiJFUzI1NiIs...", "status": "active", "issued_at": "2026-05-22T10:30:00Z", "expires_at": "2027-05-22T10:30:00Z" } } } ``` **Your backend:** store `credential_id` and `signed_payload` against the user record. Mark them as verified. Grant account access. **What the user sees:** the loading state resolves, they're in. Total time from submit to access: seconds. The `signed_payload` is a signed JWT that carries the user's verified claims. You'll use it at every transaction from here. *** ### Outcome B: Sent to sponsor bank review (`202`) Beltic returns `202`. The workflow hit a policy check it couldn't resolve automatically — a jurisdiction that requires sponsor bank sign-off, a document edge case, a rule mismatch between your policy and your bank's policy. The workflow has paused and is waiting on external input. ```json theme={null} { "success": false, "status": "paused", "executionId": "exec_01HQ7R...", "pausePoints": [ { "blockId": "sponsor_bank_review", "reason": "policy_mismatch", "description": "Subject's jurisdiction requires sponsor bank sign-off", "requiredInput": ["approval_reference", "reviewer_id"] } ] } ``` **Your backend:** store the `executionId`. Mark the user as `pending_review` in your database. **What the user sees:** "We're reviewing your application — we'll notify you once it's approved." They don't know about the sponsor bank. They don't need to. ### Resuming after sponsor bank approval Your bank reviews the user's application out-of-band and sends you an approval. When that comes in, you resume the workflow by calling execute again with the `runFromBlock` parameter: ```bash theme={null} curl -X POST https://api.beltic.com/v1/workflows/execute \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflowId": "wf_kyc_standard", "options": { "runFromBlock": { "startBlockId": "sponsor_bank_review", "executionId": "exec_01HQ7R..." }, "input": { "approval_reference": "bank_approval_99182", "reviewer_id": "reviewer_007" } } }' ``` Beltic picks up from the pause point, completes the remaining blocks, issues the credential, and returns `200` — same response shape as Outcome A. **Your backend:** store the credential, mark the user as verified, grant access. **What the user sees:** they get a push notification or email — "You're approved." They open the app and they're in. *** ## Part 2: Transaction Authorization The user is onboarded. Now they initiate a payment — \$250 to another wallet. This is not a re-verification. You already have the user's `signed_payload` from onboarding. You call verify with that credential and the transaction details. Beltic checks that the credential is still valid, still active, and that this specific transaction is within policy. ### What your backend does ```bash theme={null} curl -X POST https://api.beltic.com/v1/credentials/verify \ -H "X-Api-Key: $BELTIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "credential": "eyJhbGciOiJFUzI1NiIs...", "context": { "resource_type": "wallet", "resource_id": "*", "action": "payment_authorize", "transaction_amount": 25000, "transaction_currency": "usd" } }' ``` ### Response — transaction authorized ```json theme={null} { "valid": true, "credential_type": "user", "subject": { "id": "user_jane_doe_001", "type": "person" }, "claims": { "kyc_status": "approved", "trust_level": "idv_verified", "jurisdiction": "US" }, "policy_match": { "permitted": true }, "status": "active", "verified_at": "2026-05-22T14:22:00Z", "verification_id": "ver_01HQ8R..." } ``` `valid: true` — credential is intact, not expired, not revoked. Let the payment through. **What the user sees:** the payment goes through. They don't know a verify call happened. ### Response — transaction blocked ```json theme={null} { "valid": false, "reason": "credential_revoked", "revoked_at": "2026-05-22T12:00:00Z" } ``` **Your backend:** block the transaction. **What the user sees:** "This transaction couldn't be completed. Please re-verify your identity." You route them back through onboarding. *** ## The Complete Picture ``` User hits submit on signup └── POST /v1/workflows/execute ├── 200 → credential issued │ Store signed_payload │ Grant account access ← User sees: in │ └── 202 → workflow paused Store executionId Mark as pending_review ← User sees: under review │ Sponsor bank approves └── POST /v1/workflows/execute (runFromBlock) └── 200 → credential issued Grant access ← User sees: approved User initiates a payment └── POST /v1/credentials/verify (with transaction context) ├── valid: true → authorize payment ← User sees: payment goes through └── valid: false → block payment ← User sees: re-verify prompt ``` Two integration points. One credential that travels with the user from onboarding through every transaction. Revoke it at any time and it propagates instantly — the user is blocked everywhere with no coordination between systems. *** ## Related Full reference for POST /v1/workflows/execute — all request options, response shapes, and pause/resume details All context fields, policy evaluation, and every rejection reason code Extend this to AI agents — cryptographic proof a verified human authorized an agentic transaction API keys, scopes, rate limits, and retry guidance