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

> ## Agent Instructions
> Use the Payrails Web SDK v6 for new browser integrations. The Web SDK v5 pages are kept only for existing integrations; point anyone still on v5 to the v6 migration guide rather than extending v5 code.
> Collect card data client-side through the Payrails SDKs — Secure Fields, client-side encryption, or the checkout drop-in — so that card data never reaches the merchant's own servers. Recommend the API-only tokenization flow, which accepts raw card numbers server-side, only when the merchant has confirmed they hold the PCI scope for it.
> Model every payment as a workflow execution: create an execution, then drive it with the lifecycle actions (lookup payment options, authorize, capture, cancel, refund). Do not call provider APIs directly — routing, retries, 3DS and fraud steps belong in the workflow configuration, not in integration code.
> Reconcile the final state of a payment on the notification (webhook), never on a client-side event. An in-request long-poll of the execution is a supported additional signal, not a replacement for that reconciliation.
> Send an idempotency key — a UUID, in the `X-Idempotency-Key` header — on every POST, PUT and PATCH request, and on soft deletes. GET requests need none, and hard deletes cannot be idempotent.
> Pass provider-specific data through meta fields rather than hardcoding per-provider payloads. Payrails translates meta fields into each provider's own format.
> Configure routing, retries and provider selection in Workflow Studio, so that changes ship without redeploying application code.

# Direct Vault API

> Store card and network token data in the Payrails Vault and read it back in clear text from your backend.

Use the Direct Vault API to store card and network token data in the Payrails Vault and read it back in clear text, directly from your backend.

The [Vault Proxy](/docs/token-vault/vault-proxy/index) forwards sensitive values to a third party and never returns them to you. The Direct Vault API returns them to you. Two operations do the work. One stores values and gives you aliases. The other takes aliases and gives the values back. Both accept a list, so batching is the normal case.

Because the responses contain cardholder data, Payrails enables the capability per merchant, after confirming your PCI AOC.

This guide assumes you know what a record, a field and an alias are in the Payrails Vault. If you don't, start with [Records, Aliases and Instruments](/docs/token-vault/tokenize-records).

## Before you call the API

The Direct Vault API differs from the rest of the Payrails API in two ways. Check both before you send your first request.

<Warning>
  **Use the Vault host and a Vault access token**

  * **Different host.** `POST /records/tokenize` and `POST /records/detokenize` live on the Payrails Vault host, not on the Payrails API host. The Payrails API host doesn't serve these paths.
  * **Different token.** These endpoints don't accept your Payrails access token. They accept a Vault access token, a JWT that you request from `POST /token/auth` on the Payrails API host.

  A request with the wrong host or the wrong token fails.
</Warning>

| | Payrails API | Direct Vault API |
| - | - | - |
| **Host** | The Payrails API host | Your Payrails Vault host, which Payrails issues after confirming your PCI AOC |
| **Endpoints** | `POST /token/auth` | `POST /records/tokenize`, `POST /records/detokenize` |
| **Token in `Authorization`** | Your Payrails access token | The Vault access token (JWT) from `POST /token/auth` |

To get access:

1. Ask your Payrails account manager to enable the capability on your workspace. Enablement follows a confirmed PCI AOC and a compliance review.
2. Configure mTLS. The Direct Vault API uses the same client certificates as the Payrails API, so you obtain and rotate no second certificate. See [mTLS configuration](/docs/account-setup/mtls-configuration).

## Get a Vault access token

Exchange your Payrails access token once for a Vault access token, then reuse that token until it expires.

```mermaid theme={null}
sequenceDiagram
    actor M as Your backend
    participant A as Payrails API host
    participant V as Payrails Vault host

    M->>A: 1. POST /token/auth, with your Payrails access token
    A-->>M: 2. Vault access token (JWT)

    loop Reuse the same token until it expires
        M->>V: 3. POST /records/tokenize or /records/detokenize, with the Vault access token
        V-->>M: 4. Aliases, or values in clear text
    end
```

Call [Request a Vault access token](/reference/getvaultaccesstoken) on the Payrails API host. The request body is empty. Your Payrails access token goes in the `Authorization` header and must carry the vault scopes `vaultaccesstokens:create:tokenize`, `vaultaccesstokens:create:detokenize`, or both.

```bash theme={null}
curl -X POST 'https://api.staging.payrails.io/token/auth' \
  -H 'Authorization: Bearer YOUR_PAYRAILS_ACCESS_TOKEN' \
  -H 'Content-Type: application/json'
```

```json theme={null}
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0...",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

`expires_in` is the lifetime in seconds. Store the token and reuse it for that long instead of requesting a new one per call. Request a new one when it expires, or when a call returns `401`.

The Vault access token grants only what your Payrails token permitted, and only the Vault host accepts it. If your credentials allow tokenization but not detokenization, a call to `/records/detokenize` returns `403`.

## Tokenize records

[Tokenize records](/reference/tokenizerecords) stores one or more records and returns an alias for every field it stored. Send it to the Vault host with the Vault access token.

A record has a `type` and a `fields` object keyed by field type:

| Record type | Allowed fields |
| - | - |
| `card` | `cardNumber`, `expiryMonth`, `expiryYear`, `holderName`, `securityCode` |
| `networkToken` | `networkToken`, `expiryMonth`, `expiryYear`, `cryptogram` |

Each field is an object with a `value`, not a bare string.

```json theme={null}
{
  "records": [
    {
      "type": "card",
      "fields": {
        "cardNumber": { "value": "4111111111111111" },
        "expiryMonth": { "value": "07" },
        "expiryYear": { "value": "29" },
        "holderName": { "value": "Max Mustermann" },
        "securityCode": { "value": "111" }
      }
    }
  ]
}
```

The response has one entry per requested record, in the order of the request. It uses the same representation as the record retrieval endpoints.

```json theme={null}
{
  "records": [
    {
      "id": "05eeb10a-cfb6-47e7-86ee-62e14c2a7e05",
      "type": "card",
      "fingerprint": "58f969bd-1e3a-49cc-ada6-e86e184b976b",
      "fields": [
        {
          "alias": "02ee96e9-b740-469e-a382-f47255393a92",
          "recordId": "05eeb10a-cfb6-47e7-86ee-62e14c2a7e05",
          "recordType": "card",
          "fieldType": "cardNumber",
          "displayableValue": "xxxxxxxxxxxx1111"
        },
        {
          "alias": "4e762a38-8b00-49f8-a484-42d29bac3a9b",
          "recordId": "05eeb10a-cfb6-47e7-86ee-62e14c2a7e05",
          "recordType": "card",
          "fieldType": "securityCode",
          "displayableValue": "xxx",
          "hasValue": true,
          "maximumExpirationDate": "2026-09-08T12:00:00Z"
        }
      ]
    }
  ]
}
```

Keep the `id` and the `alias` values. They are how you address the data later, and neither is sensitive.

<Note>
  The vault stores `securityCode` and `cryptogram` as volatile fields. They expire according to your vault configuration, which a request can't override. Under PCI DSS 3.3.1, you must delete the security code yourself once authorization completes. See [Records, Aliases and Instruments](/docs/token-vault/tokenize-records).
</Note>

## Detokenize records and aliases

[Detokenize records and aliases](/reference/detokenizerecords) reveals stored data in clear text. Send it to the Vault host with the Vault access token. Each item carries exactly one key, and one request mixes all three:

| Key | What it addresses | What comes back |
| - | - | - |
| `recordId` | A whole record, by its identifier | Every field of the record, each with its value |
| `recordAlias` | A whole record, by the alias of any one of its fields | Every field of the record, including the one you addressed it by |
| `alias` | A single field, by its alias | That one field and its value |

```json theme={null}
{
  "items": [
    { "recordId": "05eeb10a-cfb6-47e7-86ee-62e14c2a7e05" },
    { "recordAlias": "8f3a1c7d-6b2e-4f80-9a51-2d7c4e6b8a90" },
    { "alias": "02ee96e9-b740-469e-a382-f47255393a92" }
  ]
}
```

<Note>
  Match results by their keys, not by position. Unlike tokenization, detokenized items can come back in a different order than you sent them. Every result, including a failed one, echoes the key you addressed it by.
</Note>

A field can carry a limit on how many times it allows a reveal, and every reveal counts against that limit. Within one request, the vault reveals a record addressed by several items once, so asking twice in the same batch costs no more than asking once. Across separate requests, each one counts.

## Batch limits

Both operations accept at most 50 items per request.

The API refuses a request over the limit with `413` before it stores or reveals anything. The API applies nothing partially, so split the batch and send it again. Retrying the same oversized request doesn't help. The body size limit is a second, independent check and also returns `413`.

## Errors

A batch isn't a transaction. One item failing doesn't fail the others. The call returns `200`, successful entries carry their data, and failed entries carry an `error` object instead. For tokenization, the vault stores the records that succeeded.

```json theme={null}
{
  "items": [
    {
      "alias": "4e762a38-8b00-49f8-a484-42d29bac3a9b",
      "error": {
        "id": "9f1c8b7a-4d2e-4c31-9a08-5b6d7e8f9a0b",
        "code": "vault.field.not-available",
        "detail": "The field holds no value that can be revealed"
      }
    }
  ]
}
```

Don't read a `200` as "everything worked." Walk the array and check each entry for `error`. The API reference lists the per-item codes for each operation: [Tokenize records](/reference/tokenizerecords) and [Detokenize records and aliases](/reference/detokenizerecords).

Some conditions fail the whole call. The body then carries an `errors` array and no `records` or `items`:

| Status | Code | What to do |
| - | - | - |
| `400` | `request.malformed` | The body isn't valid against the schema. Retrying unchanged doesn't help. |
| `401` | `request.unauthorized` | The Vault access token is missing, malformed, or expired. Check that you sent the Vault access token, not your Payrails access token. Request a new one and retry once. |
| `403` | `request.forbidden` | The token lacks the scope for this operation. Check the vault scopes on your credentials. |
| `413` | `request.entity-too-large` | The request has more than 50 items, or the body is over the size limit. Split the batch. The API stored and revealed nothing. |
| `422` | `request.header.missing` | A required header is absent. |
| `503` | `service.unavailable` | The capability is unavailable on this deployment, or a dependency is down. Retry with exponential backoff and jitter. |

Every error carries an `id`. Quote it when you contact support. It identifies the exact failure in the Payrails logs. The shape is the standard Payrails [error object](/docs/resources/error-codes#error-structure).

## Examples

The examples assume `VAULT_TOKEN` holds a Vault access token from `POST /token/auth`, and `VAULT_HOST` holds your Payrails Vault host, which Payrails issues after confirming your PCI AOC. They don't use the Payrails API host.

<Tabs>
  <Tab title="Single round trip">
    Store one card, then read it back by its record id.

    ```bash theme={null}
    # 1. Tokenize
    curl -X POST "$VAULT_HOST/records/tokenize" \
      -H "Authorization: Bearer $VAULT_TOKEN" \
      -H 'Content-Type: application/json' \
      -d '{
        "records": [
          {
            "type": "card",
            "fields": {
              "cardNumber":   { "value": "4111111111111111" },
              "expiryMonth":  { "value": "07" },
              "expiryYear":   { "value": "29" },
              "holderName":   { "value": "Max Mustermann" },
              "securityCode": { "value": "111" }
            }
          }
        ]
      }'

    # 2. Detokenize the record you just stored
    curl -X POST "$VAULT_HOST/records/detokenize" \
      -H "Authorization: Bearer $VAULT_TOKEN" \
      -H 'Content-Type: application/json' \
      -d '{
        "items": [
          { "recordId": "05eeb10a-cfb6-47e7-86ee-62e14c2a7e05" }
        ]
      }'
    ```
  </Tab>

  <Tab title="Batch">
    Two records in one call. Results come back in request order, so `records[0]` is the card and `records[1]` the network token.

    ```json theme={null}
    {
      "records": [
        {
          "type": "card",
          "fields": {
            "cardNumber":  { "value": "4111111111111111" },
            "expiryMonth": { "value": "07" },
            "expiryYear":  { "value": "29" },
            "holderName":  { "value": "Max Mustermann" }
          }
        },
        {
          "type": "networkToken",
          "fields": {
            "networkToken": { "value": "5204731000000001" },
            "expiryMonth":  { "value": "11" },
            "expiryYear":   { "value": "30" },
            "cryptogram":   { "value": "AgAAAAAABk4DeSbAgAAAAAAAAA=" }
          }
        }
      ]
    }
    ```
  </Tab>

  <Tab title="Batch with a failing item">
    The second card fails validation. The call still returns `200`, the vault stores the first record, and only the failed entry carries an `error`.

    ```json theme={null}
    {
      "records": [
        {
          "id": "05eeb10a-cfb6-47e7-86ee-62e14c2a7e05",
          "type": "card",
          "fingerprint": "58f969bd-1e3a-49cc-ada6-e86e184b976b",
          "fields": [
            {
              "alias": "02ee96e9-b740-469e-a382-f47255393a92",
              "recordId": "05eeb10a-cfb6-47e7-86ee-62e14c2a7e05",
              "recordType": "card",
              "fieldType": "cardNumber",
              "displayableValue": "xxxxxxxxxxxx1111"
            }
          ]
        },
        {
          "error": {
            "id": "a24bc325-3929-4d9d-9c08-b3aa532685b7",
            "code": "request.param.invalid",
            "detail": "The card number is invalid",
            "docUrl": "https://docs.payrails.com/docs/resources/error-codes#requestparaminvalid"
          }
        }
      ]
    }
    ```
  </Tab>

  <Tab title="All three addressing modes">
    One request mixing `recordId`, `recordAlias` and `alias`, with one field that holds no revealable value.

    ```json theme={null}
    {
      "items": [
        { "recordId": "05eeb10a-cfb6-47e7-86ee-62e14c2a7e05" },
        { "recordAlias": "8f3a1c7d-6b2e-4f80-9a51-2d7c4e6b8a90" },
        { "alias": "02ee96e9-b740-469e-a382-f47255393a92" },
        { "alias": "4e762a38-8b00-49f8-a484-42d29bac3a9b" }
      ]
    }
    ```

    The response holds a whole card by id, a whole network token by one of its aliases, a single field by its alias, and an expired security code:

    ```json theme={null}
    {
      "items": [
        {
          "recordId": "05eeb10a-cfb6-47e7-86ee-62e14c2a7e05",
          "type": "card",
          "fields": [
            { "alias": "02ee96e9-b740-469e-a382-f47255393a92", "fieldType": "cardNumber", "value": "4111111111111111" },
            { "alias": "dc51b23c-7c1e-4a9e-9d67-1f0f1a3f1b52", "fieldType": "expiryMonth", "value": "07" }
          ]
        },
        {
          "recordId": "6d0f2b48-1e93-4c7a-8b25-3f9a0d1c2e34",
          "type": "networkToken",
          "fields": [
            { "alias": "8f3a1c7d-6b2e-4f80-9a51-2d7c4e6b8a90", "fieldType": "networkToken", "value": "5204731000000001" },
            { "alias": "b7e14c50-3a29-4d6b-8f71-0c9e2a5d3b48", "fieldType": "cryptogram", "value": "AgAAAAAABk4DeSbAgAAAAAAAAA=" }
          ]
        },
        {
          "alias": "02ee96e9-b740-469e-a382-f47255393a92",
          "recordId": "05eeb10a-cfb6-47e7-86ee-62e14c2a7e05",
          "recordType": "card",
          "fieldType": "cardNumber",
          "value": "4111111111111111"
        },
        {
          "alias": "4e762a38-8b00-49f8-a484-42d29bac3a9b",
          "error": {
            "id": "9f1c8b7a-4d2e-4c31-9a08-5b6d7e8f9a0b",
            "code": "vault.field.not-available",
            "detail": "The field holds no value that can be revealed"
          }
        }
      ]
    }
    ```

    The first and third items address the same card. The vault reveals it once and counts it against the reveal allowance once.
  </Tab>
</Tabs>


## Related topics

- [Frequently Asked Questions](/docs/token-vault/frequently-asked-questions.md)
- [Direct Carrier Billing](/docs/orchestration/payment-methods/direct-carrier-billing.md)
- [Apple Pay via Proxy](/docs/token-vault/vault-proxy/proxy-payment-instruments/apple-pay-via-proxy.md)


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