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

# Portal APIs

> Read account data, create purchase instructions with a short-lived activation link, bind or delete payment methods, request payment credentials, and report merchant payment results with a portal token.

These APIs use the portal user token from [Portal token](https://docs.clinkbill.com/api-reference/agent-portal/portal_token.md). They are scoped to the token's `sub` customer. Reads require `portal:read`; writes require `portal:write` and act only on that customer's data.

## Authentication

```http theme={null}
Authorization: Bearer {PORTAL_TOKEN}
```

The gateway verifies `aud=clink-agent-platform-portal`, `token_use=access`, and the required scope, then passes the verified customer identity to cwallet. There is no request parameter for selecting a different customer or email.

Clink responses use the standard envelope:

```json theme={null}
{
  "code": 200,
  "msg": "Success",
  "data": {}
}
```

The examples below show `data`.

## Common behavior

* `pageNum` and `pageSize` use 1-based pagination.
* Order lists accept `status`, `from`, and `to` filters.
* Lists are scoped to the customer, not to an Agent. Orders, refunds, and authorizations created by any Agent for that customer are returned.
* A missing record, or a record owned by another customer, is returned as `404`.
* Writes are limited to the user's own risk rules, billing address, purchase instructions, card-binding sessions, payment credentials, payment results, and payment methods. All other endpoints are read-only.
* Request amounts on the write endpoints are decimal strings in major currency units: `"12.50"` means USD 12.50. Do not send minor units.

## Get account

```http theme={null}
GET {API_BASE_URL}/agent/platform/me
```

```json theme={null}
{
  "customer_id": "ccus_...",
  "email": "user@example.com",
  "name": "Example User",
  "country": "US",
  "status": "READY"
}
```

| Field | Meaning |
| - | - |
| `customer_id` | Clink customer ID. |
| `email` | Customer email. |
| `name` | Customer display name. |
| `country` | Billing address country, when available. |
| `status` | Wallet readiness: `READY`, `NOT_READY`, or `SUSPENDED`. `NOT_READY` means the account exists but has no usable default payment method. |

## List payment methods

```http theme={null}
GET {API_BASE_URL}/agent/platform/payment-methods
```

```json theme={null}
{
  "payment_methods": [
    {
      "payment_instrument_id": "pi_...",
      "payment_method_type": "CARD",
      "payment_instrument_type": "VIRTUAL_CARD",
      "card_scheme": "VISA",
      "card_last_four": "4242",
      "expiry_month": "12",
      "expiry_year": "2030",
      "issuer_bank": "Example Bank",
      "is_default": true,
      "status": "ACTIVE"
    }
  ]
}
```

`status` is `ACTIVE` or `DISABLED`. PAN and CVV are never returned. `is_default` identifies the card used by default; the default card can be deleted.

To add a card, use [Bind a card](#bind-a-card). To bind a card and authorize a purchase in one flow, omit `payment_instrument_id` in [Create instruction](#create-instruction). To remove a card, use [Delete payment method](#delete-payment-method).

## Bind a card

Creates a short-lived card-binding session for the token's customer and returns a URL that the user opens to add a card. Requires `portal:write`.

This endpoint does not create an instruction and does not authorize spending. Use it when the user only needs to add a card. To bind a card and sign in one flow, omit `payment_instrument_id` in [Create instruction](#create-instruction) instead.

```http theme={null}
POST {API_BASE_URL}/agent/platform/bind-sessions
Authorization: Bearer {PORTAL_TOKEN}
```

There are no body fields for choosing a customer. The session is always created for the token's customer.

### Response

```json theme={null}
{
  "session_id": "vs_...",
  "url": "https://agent.clinkbill.dev/vault/vs_...",
  "expires_at": "2026-11-09T10:30:00Z"
}
```

| Field | Meaning |
| - | - |
| `session_id` | Opaque short-lived card-binding session ID. |
| `url` | Short-lived card-binding link the user opens in a browser. It contains the session ID and does not contain a portal token. |
| `expires_at` | Time after which the link no longer works. |

Each call creates a new, independent session with a short TTL. When the user opens the link and completes the card-binding ceremony, the card is added to the customer's payment methods ([List payment methods](#list-payment-methods)). No instruction or authorization is created. The link can be reopened or re-exchanged while the session is still pending; it stops working once the card-binding ceremony completes or the session expires.

> [!WARNING]
>
> Treat the card-binding link as a credential. Do not forward it, screenshot it, or write it to logs; anyone who has the link can open it while it is valid and complete card binding for that customer. Deliver it only to the intended user.

## Delete payment method

Deletes one of the token's customer's payment methods. Requires `portal:write`.

```http theme={null}
DELETE {API_BASE_URL}/agent/platform/payment-methods/{paymentInstrumentId}
Authorization: Bearer {PORTAL_TOKEN}
```

```json theme={null}
{
  "deleted": true
}
```

| Field | Meaning |
| - | - |
| `deleted` | `true` when the payment method was deleted. |

A successful deletion returns `200` with `deleted: true`. The deletion is immediate and the response does not include a user-facing page URL. A payment method that does not exist, or that belongs to another customer, returns the same `404`, so the response does not reveal whether another customer's card exists. The default card can be deleted. Repeating the request after a successful deletion returns `404`.

## List orders

```http theme={null}
GET {API_BASE_URL}/agent/platform/orders?pageNum=1&pageSize=20&status={STATUS}&from={ISO_DATE}&to={ISO_DATE}
```

| Parameter | Required | Meaning |
| - | - | - |
| `pageNum` | No | Page number, starting at 1. |
| `pageSize` | No | Page size. |
| `status` | No | Order status filter. |
| `from` | No | Inclusive lower time bound. |
| `to` | No | Inclusive upper time bound. |

```json theme={null}
{
  "total": 2,
  "page_num": 1,
  "page_size": 20,
  "truncated": false,
  "records": [
    {
      "order_id": "order_...",
      "paid_time": "2026-01-01T00:00:00Z",
      "amount": 42.5,
      "currency": "USD",
      "display_amount": "$42.50",
      "status_code": 1,
      "status": "SUCCESS",
      "merchant_name": "Example Merchant",
      "payment_method_type": "CARD",
      "payment_method_label": "Visa •••• 4242",
      "payment_method_icon": "https://...",
      "payment_instrument_last_four": "4242",
      "agent_info": {
        "clientId": "agent_..."
      }
    }
  ]
}
```

`truncated: true` means the status/time filtered scan reached Clink's internal scan bound; `total` describes the records scanned, not necessarily the customer's complete matching history. Without filters, the endpoint reads one page and does not use the bounded scan.

## Get order

```http theme={null}
GET {API_BASE_URL}/agent/platform/orders/{orderId}
```

The detail response includes order time, amount breakdown, merchant, payment method, card scheme, products, shipping address and amount, `agent_info`, and a `refund_summary`:

```json theme={null}
{
  "order_id": "order_...",
  "order_time": "2026-01-01T00:00:00Z",
  "amount": 42.5,
  "display_amount": "$42.50",
  "subtotal_amount": 40.0,
  "display_subtotal_amount": "$40.00",
  "currency": "USD",
  "status_code": 1,
  "status": "SUCCESS",
  "merchant_name": "Example Merchant",
  "merchant_url": "https://merchant.example",
  "payment_method_type": "CARD",
  "payment_method_label": "Visa •••• 4242",
  "payment_instrument_last_four": "4242",
  "card_scheme": "VISA",
  "credit_amount": 2.5,
  "display_credit_amount": "$2.50",
  "paid_amount": 42.5,
  "display_paid_amount": "$42.50",
  "products": [],
  "agent_info": {},
  "refund_summary": {
    "refund_count": 0,
    "refunded_amount": 0,
    "currency": "USD",
    "latest_status": null,
    "refunds": []
  }
}
```

## List refunds

```http theme={null}
GET {API_BASE_URL}/agent/platform/refunds?pageNum=1&pageSize=20
```

```json theme={null}
{
  "total": 1,
  "page_num": 1,
  "page_size": 20,
  "records": [
    {
      "refund_order_id": "refund_...",
      "order_id": "order_...",
      "refund_amount": 10.0,
      "refund_currency": "USD",
      "status": "SUCCESS",
      "refund_reason": "Customer request",
      "payment_instrument_id": "pi_..."
    }
  ]
}
```

## Get refund

```http theme={null}
GET {API_BASE_URL}/agent/platform/refunds/{refundOrderId}
```

Returns one refund with the fields shown above. A refund owned by another customer is returned as `404`.

## List authorizations

```http theme={null}
GET {API_BASE_URL}/agent/platform/authorizations?pageNum=1&pageSize=20&status={STATUS}
```

| Parameter | Required | Meaning |
| - | - | - |
| `pageNum` | No | Page number, starting at 1. |
| `pageSize` | No | Page size. |
| `status` | No | Authorization status filter. |

```json theme={null}
{
  "total": 1,
  "page_num": 1,
  "page_size": 20,
  "records": [
    {
      "instruction_id": "inst_...",
      "client_reference_id": "0f1e2d3c-...",
      "payment_instrument_id": "pi_...",
      "card_last_four": "4242",
      "card_type": "VIRTUAL_CARD",
      "card_scheme": "VISA",
      "title": "Example authorization",
      "description": "Purchase authorization",
      "total_amount_limit": 100.0,
      "currency_code": "USD",
      "effective_until_time": "2026-02-01T00:00:00Z",
      "is_recurring": false,
      "status": "ACTIVE",
      "agent_info": {},
      "mandates": [],
      "usages": []
    }
  ]
}
```

## Get authorization

```http theme={null}
GET {API_BASE_URL}/agent/platform/authorizations/{instructionId}
```

The detail response includes the instruction fields, `mandates`, and `usages`. `usages` are raw usage records; compute used and remaining amounts from them when rendering the portal. An instruction owned by another customer is returned as `404`.

`client_reference_id` in authorization responses is the instruction's own field. For instructions created through the portal, Clink generates a random value for it; it is not the create request's `request_id` and not the `client_reference_id` sent to [Get credential](#get-credential) and [Report credential result](#report-credential-result). Do not use it to reconcile a purchase.

## Create instruction

Creates a purchase instruction (spending authorization) for the token's customer and returns a short-lived activation link that the user opens to approve it with a Passkey. Requires `portal:write`.

`payment_instrument_id` selects one of two signing modes:

| Mode | `payment_instrument_id` | Instruction status | Activation link |
| - | - | - | - |
| Sign with an existing card | Present | `CREATED` | Opens the authorization page for that card. |
| Bind a card and sign | Omitted | `PENDING` | Opens the card-binding and authorization flow, where the user binds a card and authorizes in one Passkey ceremony. |

For the first mode, read `payment_instrument_id` from [List payment methods](#list-payment-methods). In the second mode, do not send a card number, CVV, or any other card field; the user enters card details on the Clink page. Customer identity always comes from the token; the request body has no customer or email field.

After the activation link's signing session is exchanged, an existing-card instruction continues to `/authorize/{paymentInstrumentId}?instructionId=...`; a bind-and-sign instruction continues to `/bind-authorize?instructionId=...`, where the user binds a card and authorizes in one Passkey ceremony.

```http theme={null}
POST {API_BASE_URL}/agent/platform/authorizations
Authorization: Bearer {PORTAL_TOKEN}
Content-Type: application/json
```

### Request fields

| Field | Required | Meaning |
| - | - | - |
| `request_id` | Yes | Platform-side reference and idempotency key, 8–128 characters. |
| `title` | Yes | Non-empty instruction title. |
| `description` | No | Instruction description. |
| `payment_instrument_id` | No | Existing usable payment instrument owned by the token's customer. Omit it to bind a new card and sign. |
| `is_recurring` | No | Whether the instruction contains recurring mandates. When `true`, every mandate must include `recurring_frequency`. |
| `effective_until_time` | No | Last time the instruction can be used, in UTC `yyyy-MM-dd HH:mm:ss`. Ignored in bind-and-sign mode: Clink overrides it (and each mandate's `effective_until_time`) with now + `pendingActivationWindowMinutes` (7 days by default), so read the effective value from the instruction. |
| `shipping_address` | No | Shipping address object with `name`, `line1`, `line2`, `line3`, `city`, `state`, `zip`, and `country_code`. |
| `mandates` | Yes | One or more mandates, as described below. |

Each mandate contains:

| Field | Required | Meaning |
| - | - | - |
| `description` | Yes | Non-empty purpose of the authorized spending. |
| `amount_limit` | Yes | Positive decimal string in major currency units, at most 18 integer digits and 2 decimal places. `"100.00"` means USD 100.00. |
| `currency_code` | Yes | Three-letter currency code. All mandates in one instruction must use the same currency. |
| `recurring_frequency` | For recurring instructions | `WEEKLY`, `MONTHLY`, or `YEARLY`. Required for every mandate when `is_recurring` is `true`. |
| `merchant_category_code` | No | Merchant category restriction, when applicable. |
| `preferred_merchant_name` | No | Preferred merchant name. |
| `effective_until_time` | No | Mandate expiry, in UTC `yyyy-MM-dd HH:mm:ss`. In bind-and-sign mode, Clink overrides it together with the instruction expiry; read the effective value from the instruction. |

### Example request

> [tab] Sign with an existing card
>
> ```json theme={null}
> {
>   "request_id": "platform-ref-001",
>   "title": "Office supplies budget",
>   "description": "Monthly office supplies purchase",
>   "payment_instrument_id": "pi_...",
>   "is_recurring": false,
>   "effective_until_time": "2026-12-31 23:59:59",
>   "shipping_address": {
>     "name": "Example User",
>     "line1": "1 Market Street",
>     "line2": "Suite 100",
>     "line3": null,
>     "city": "San Francisco",
>     "state": "CA",
>     "zip": "94105",
>     "country_code": "US"
>   },
>   "mandates": [
>     {
>       "description": "Purchase office supplies",
>       "amount_limit": "100.00",
>       "currency_code": "USD",
>       "recurring_frequency": null,
>       "merchant_category_code": "5732",
>       "preferred_merchant_name": null,
>       "effective_until_time": null
>     }
>   ]
> }
> ```

> [tab] Bind a card and sign
>
> ```json theme={null}
> {
>   "request_id": "platform-ref-002",
>   "title": "Office supplies budget",
>   "description": "Monthly office supplies purchase",
>   "is_recurring": false,
>   "shipping_address": {
>     "name": "Example User",
>     "line1": "1 Market Street",
>     "line2": "Suite 100",
>     "line3": null,
>     "city": "San Francisco",
>     "state": "CA",
>     "zip": "94105",
>     "country_code": "US"
>   },
>   "mandates": [
>     {
>       "description": "Purchase office supplies",
>       "amount_limit": "100.00",
>       "currency_code": "USD",
>       "recurring_frequency": null,
>       "merchant_category_code": "5732",
>       "preferred_merchant_name": null,
>       "effective_until_time": null
>     }
>   ]
> }
> ```

### Response

> [tab] Sign with an existing card
>
> ```json theme={null}
> {
>   "instruction_id": "inst_...",
>   "status": "CREATED",
>   "payment_instrument_id": "pi_...",
>   "activation_url": "https://agent.clinkbill.dev/sign/ss_...",
>   "activation_expires_at": "2026-11-09T10:30:00Z"
> }
> ```

> [tab] Bind a card and sign
>
> ```json theme={null}
> {
>   "instruction_id": "inst_...",
>   "status": "PENDING",
>   "activation_url": "https://agent.clinkbill.dev/sign/ss_...",
>   "activation_expires_at": "2026-11-09T10:30:00Z"
> }
> ```

| Field | Meaning |
| - | - |
| `instruction_id` | Created instruction ID. |
| `status` | Instruction status: `CREATED` when signing with an existing card, `PENDING` when binding a new card. |
| `payment_instrument_id` | Payment instrument the instruction uses. Absent or `null` in bind-and-sign mode, where no card is bound yet. |
| `activation_url` | Short-lived activation link the user opens to approve the instruction. In bind-and-sign mode, the page also collects the card before authorization, in the same Passkey ceremony. The link contains an opaque signing session ID, not a portal token, and can be exchanged for a normal Clink browser login session for that customer while it is valid. It supports repeated exchanges (for example, when the user refreshes the page) and stops working at `activation_expires_at` (30 minutes by default) or once the instruction becomes `ACTIVE` or final. |
| `activation_expires_at` | Time after which the activation link no longer works. |

> [!WARNING]
>
> Treat the activation link as a credential. Do not forward it, screenshot it, or write it to logs; anyone who has the link can open it while it is valid and obtain a Clink browser login session for that customer. Deliver it only to the intended user.

### Activate the instruction

Creating an instruction is not an authorization. An instruction with an existing card is `CREATED`; an instruction that will bind a card is `PENDING`. Both become `ACTIVE` only after the user completes Passkey verification on Clink's page. The link can be reopened or refreshed while it is valid, but the instruction can be signed only once.

1. Send the user to `activation_url` in a browser.
2. For `CREATED`, the user reviews the card and completes Passkey verification. For `PENDING`, the user binds a card and completes Passkey verification in the same ceremony. The page establishes the browser session and continues to the authorization step.
3. Poll `GET /agent/platform/authorizations/{instructionId}` until `status` is `ACTIVE`.

If the user does not complete the flow, the instruction stays `CREATED` or `PENDING` and authorizes no spending. Only send the link to the intended user; it is an activation link, not a portal token.

### Idempotency

`request_id` is the idempotency key, and the rules are identical in both signing modes. Clink keeps the idempotency record for 24 hours by default; after that window, retrying the same `request_id` creates a new instruction. Within the window, retrying with identical parameters returns the same `instruction_id`; a normal retry on an instruction that is not yet signed returns a fresh activation link. If the retried `request_id` refers to an instruction that is already signed or final, Clink returns the instruction without a new `activation_url` or `activation_expires_at` (both fields are omitted); read `GET /agent/platform/authorizations/{instructionId}` for the current status. Reusing a `request_id` with different parameters returns `409` with `code` `71211011` (`portal_conflict`).

## Get credential

Requests a single-use payment credential for one purchase against an activated instruction and mandate. Requires `portal:write`.

The response has two modes, depending on whether your platform has a public key registered with Clink:

* **Registered public key** (`matched: true`): Clink seals the card fields into a `key`/`payload` envelope that only your platform can decrypt with the matching private key.
* **No registered public key** (`matched: false`): when plaintext mode is enabled server-side and gateway response redaction is live, Clink returns the DPAN and dynamic CVV in plaintext in the response. The card can be used directly, but these fields reach your backend in plaintext.

Register a public key with Clink to use the encrypted mode; this is recommended for production.

```http theme={null}
POST {API_BASE_URL}/agent/platform/credential
Authorization: Bearer {PORTAL_TOKEN}
Content-Type: application/json
```

### Public key registration

Contact Clink to register your platform's RSA public key in each environment; it is mapped to your platform client name. The registered key selects the response mode: with a key the credential is sealed to it; without one, Clink returns plaintext card fields only when plaintext mode is enabled server-side and gateway response redaction is live (the MCP-equivalent fallback). With the default gate off, the endpoint fails closed instead. Register the key before production use.

> [!WARNING]
>
> Without a registered public key, when plaintext mode is enabled server-side, the response returns the DPAN and dynamic CVV in plaintext to your backend. Keep these fields out of logs, model input, and ordinary storage, and do not keep them beyond the checkout. Register a public key with Clink (recommended for production) to switch to the sealed `key`/`payload` mode. This plaintext fallback matches MCP `get_credential`. Do not enable plaintext mode in environments where gateway response redaction is not yet active: it is config-gated server-side by `portal-plaintext-enabled` (default off), and with the gate off the endpoint fails closed with `71211012` (`credential_public_key_not_configured`) instead of returning plaintext card fields.

### Request fields

| Field | Required | Meaning |
| - | - | - |
| `instruction_id` | Yes | `ACTIVE` instruction owned by the token's customer. |
| `mandate_id` | Yes | Mandate under that instruction that covers the purchase. |
| `client_reference_id` | Yes | Platform-side reference and idempotency key for this purchase. Store it; the result report uses the same value. |
| `amount` | Yes | Transaction amount, decimal string in major currency units. |
| `currency_code` | Yes | Three-letter currency code. |
| `merchant_url` | Yes | Merchant checkout URL. Must use HTTPS. |
| `merchant_name` | No | Merchant display name. Defaults to the host of `merchant_url`. |
| `merchant_country_code` | No | Merchant country, ISO 3166-1 alpha-2. Defaults to `US`. |
| `merchant_order_id` | No | Merchant-side order ID, when available. |
| `products` | No | Line items for the purchase; fields below. |
| `shipping_address` | No | Shipping address object with the same fields as `shipping_address` in [Create instruction](#create-instruction). |

Each product in `products` contains:

| Field | Meaning |
| - | - |
| `product_id` | Product identifier. |
| `product_name` | Product name. |
| `quantity` | Quantity. |
| `unit_price` | Unit price, decimal string in major currency units. |
| `currency_code` | Three-letter currency code. |
| `product_url` | Product page URL. |

### Example request

```json theme={null}
{
  "instruction_id": "inst_...",
  "mandate_id": "mand_...",
  "client_reference_id": "platform-ref-001",
  "amount": "12.50",
  "currency_code": "USD",
  "merchant_url": "https://shop.example.com/checkout",
  "merchant_name": "Example Store",
  "merchant_country_code": "US",
  "merchant_order_id": "shop-order-1",
  "products": [
    {
      "product_id": "item-001",
      "product_name": "Example item",
      "quantity": 1,
      "unit_price": "12.50",
      "currency_code": "USD",
      "product_url": "https://shop.example.com/item-001"
    }
  ],
  "shipping_address": {
    "name": "Example User",
    "line1": "1 Market Street",
    "city": "San Francisco",
    "state": "CA",
    "zip": "94105",
    "country_code": "US"
  }
}
```

### Response

`matched` selects the response shape.

> [tab] Sealed (matched: true)
>
> Clink seals the credential to your registered public key. The card fields are inside `payload`.
>
> ```json theme={null}
> {
>   "matched": true,
>   "key": "<RSA-OAEP wrapped AES key, Base64>",
>   "payload": "<AES-256-GCM ciphertext, Base64>",
>   "expires_at": "2026-11-09T10:10:00Z"
> }
> ```

> [tab] Plaintext (matched: false)
>
> No public key is registered and plaintext mode is enabled server-side, so the DPAN and dynamic CVV are returned in plaintext and can be used directly.
>
> ```json theme={null}
> {
>   "matched": false,
>   "expires_at": "<ISO-8601>",
>   "order_id": "order_...",
>   "payment_execution_detail_id": "ped_...",
>   "client_reference_id": "...",
>   "card": {
>     "number": "<DPAN>",
>     "securityCode": "<dynamic CVV>",
>     "expiryMonth": "12",
>     "expiryYear": "2028",
>     "expiry": "12/2028",
>     "dynamicDataType": "DAVV",
>     "dynamicDataExpiration": 1792380808,
>     "eci": "05",
>     "paymentAccountReference": "PAR-1"
>   },
>   "display": {
>     "brand": "VISA",
>     "last4": "3797"
>   }
> }
> ```

| Field | Meaning |
| - | - |
| `matched` | `true` when Clink sealed the credential with your registered public key; `false` when no key is registered and plaintext mode is enabled server-side, so the card fields are returned in plaintext. |
| `key` | Present only when `matched` is `true`; omitted when `matched` is `false`. RSA-OAEP-wrapped AES key, Base64. |
| `payload` | Present only when `matched` is `true`; omitted when `matched` is `false`. AES-256-GCM ciphertext of the credential payload, Base64. |
| `order_id` | Present only when `matched` is `false`; omitted when `matched` is `true`, where the ID is inside the decrypted payload. Order created for this credential. |
| `payment_execution_detail_id` | Present only when `matched` is `false`; omitted when `matched` is `true`, where the ID is inside the decrypted payload. Payment execution detail created for this credential. |
| `client_reference_id` | Present only when `matched` is `false`; omitted when `matched` is `true`, where the ID is inside the decrypted payload. The `client_reference_id` you sent; store it for result reporting. |
| `card` | Present only when `matched` is `false`; omitted when `matched` is `true`. Plaintext card fields (DPAN and dynamic CVV), described below. |
| `display` | Present only when `matched` is `false`; omitted when `matched` is `true`. Card display metadata: `brand` (card scheme) and `last4` (last four digits of the credential card number). |
| `expires_at` | Estimated validity window as an ISO-8601 timestamp, based on Clink's default credential TTL. The authoritative validity is in `card` (plaintext mode) or the decrypted payload (sealed mode). |

The sealed response contains only `matched`, `key`, `payload`, and `expires_at`; in that mode `card`, `display`, and the ID fields are omitted and the IDs are inside the decrypted payload. The plaintext response contains no `key` or `payload`.

Plaintext `card` fields:

In plaintext mode, `number` and `securityCode` are always present; the remaining fields are optional and omitted when not provided.

| Field | Meaning |
| - | - |
| `number` | Required in plaintext mode. DPAN (the credential card number); it is never the underlying PAN. |
| `securityCode` | Required in plaintext mode. Dynamic security code (CVV) for the credential. |
| `expiryMonth` | Expiry month as `MM`. Optional; all expiry fields may be absent. |
| `expiryYear` | Expiry year as four digits. A two-digit network value is expanded to `20YY`. Optional. |
| `expiry` | Raw expiry string from the network, either `MM/YYYY` or `MM/YY`; it is not normalized. Optional. |
| `dynamicDataType` | How the dynamic data must be used at checkout (for example, `DAVV`). Optional. |
| `dynamicDataExpiration` | Dynamic data expiry as a number in Unix epoch seconds. Optional; omitted when the network does not provide it. |
| `eci` | Electronic commerce indicator. Optional. |
| `paymentAccountReference` | Payment account reference. Optional. |

### Decrypt the sealed response

When `matched` is `true`, decrypt `key` and `payload` with your platform private key. The decrypted payload contains `orderId`, `paymentExecutionDetailId`, `clientReferenceId`, `status`, the network credential (DPAN, dynamic CVV, and expiry), and cardholder and address fields. The envelope format and a decryption example are in [Decryption](https://docs.clinkbill.com/guides/agent/browser_pay/decryption.md).

Keep decrypted card fields out of logs, model input, and ordinary records, and use the validity information in the decrypted payload to decide when to use the credential; `expires_at` is only an estimate. Save `client_reference_id`; [Report credential result](#report-credential-result) uses it.

### Idempotency

`client_reference_id` is the idempotency key. Retrying with the same `client_reference_id` returns the same credential: the same sealed envelope when `matched` is `true`, and in plaintext mode the same card from the server-side encrypted cache, with no second issuance. Reusing it with different parameters returns `409` with `code` `71211011` (`portal_conflict`).

## Report credential result

Reports the outcome observed on the merchant page after using a credential. Requires `portal:write`. Clink uses the report to close the order and the instruction usage. A report records what the merchant page showed; it does not execute or verify the payment.

> [!NOTE]
>
> This is the payment result confirmation endpoint. It is the only entry point for confirming or reporting a transaction result; there is no separate confirm-transaction API. Use it to report the final outcome, including after an `UNKNOWN` report, within the 24-hour reporting window described below.

```http theme={null}
POST {API_BASE_URL}/agent/platform/credential-results
Authorization: Bearer {PORTAL_TOKEN}
Content-Type: application/json
```

### Request fields

| Field | Required | Meaning |
| - | - | - |
| `client_reference_id` | Yes | Value used when requesting the credential. Clink finds the usage by this value for the token's customer. |
| `outcome` | Yes | `SUCCEEDED`, `FAILED`, `NOT_SUBMITTED`, or `UNKNOWN`; see the outcomes below. |
| `paid_amount` | No | Amount shown by the merchant, decimal string in major currency units. Must be paired with `currency_code`. |
| `currency_code` | No | Three-letter currency code. Must be paired with `paid_amount`. |
| `failure_reason` | No | Reason for a failed or unsubmitted payment. Accepted only with `FAILED` or `NOT_SUBMITTED`. |

| Outcome | Meaning |
| - | - |
| `SUCCEEDED` | The merchant explicitly confirmed that the payment succeeded. |
| `FAILED` | The merchant explicitly showed failure after the card was submitted. |
| `NOT_SUBMITTED` | The card was never submitted. |
| `UNKNOWN` | The card was submitted, but the result cannot be determined. Clink does not publish a result and returns `PENDING` with `report_deadline`; report the final outcome before that time. |

`failure_reason` accepts `CARD_DECLINED`, `AUTHENTICATION_REQUIRED`, `AMOUNT_CHANGED`, `CREDENTIAL_EXPIRED`, `CREDENTIAL_LOST`, `MERCHANT_ERROR`, `BUYER_CANCELLED`, or `OTHER`.

### Example request

```json theme={null}
{
  "client_reference_id": "platform-ref-001",
  "outcome": "SUCCEEDED",
  "paid_amount": "12.50",
  "currency_code": "USD",
  "failure_reason": null
}
```

### Response

```json theme={null}
{
  "status": "SUCCEEDED",
  "report_deadline": "2026-11-10T09:00:00Z",
  "paid_amount_mismatch": false
}
```

| Field | Meaning |
| - | - |
| `status` | Recorded outcome. For an `UNKNOWN` report it is `PENDING`. |
| `report_deadline` | Latest time a terminal result can be reported. |
| `paid_amount_mismatch` | `true` when `paid_amount` differs from the authorized amount. The report does not overwrite the authorized amount. |

### Reporting rules

* Clink accepts a terminal result until `report_deadline`, 24 hours after the credential is issued. A report after the window returns `410` with `code` `71211018` (`report_window_expired`).
* A usage that does not exist, or does not belong to the token's customer, returns `404`.
* Clink compares terminal results in two classes: `SUCCEEDED` is success; `FAILED` and `NOT_SUBMITTED` are both failure. Repeating a result in the same class returns the recorded result with HTTP `200`; for example, reporting `FAILED` after `NOT_SUBMITTED` does not conflict. Reporting the other class after a terminal result is recorded returns `409` with `code` `71211017` (`result_already_reported`).
* `SUCCEEDED`, `FAILED`, and `NOT_SUBMITTED` close the usage and are delivered to Clink's existing payment-result pipeline.
* `UNKNOWN` records no terminal result, publishes no result message, and returns `PENDING`; report the final outcome before `report_deadline`.
* `paid_amount` and `currency_code` must be sent together. When `paid_amount` differs from the authorized amount, Clink records `paid_amount_mismatch: true` and keeps the authorized amount.

## Risk rules

Read and replace the user's global default risk rules. The update is a full replacement of the settings object.

```http theme={null}
GET {API_BASE_URL}/agent/platform/risk-rules
PUT {API_BASE_URL}/agent/platform/risk-rules
Authorization: Bearer {PORTAL_TOKEN}
Content-Type: application/json
```

| Field | Type | Required on PUT | Meaning |
| - | - | - | - |
| `singleRechargeLimit` | number | Yes | Single top-up limit in USD; minimum 1. |
| `dailyTotalLimit` | number | Yes | Daily top-up limit in USD; minimum 1. |
| `dailyMaxCount` | integer | Yes | Maximum top-ups per day; minimum 1. |
| `rechargeInterval` | string | Yes | Cooldown such as `5m`, `10m`, `30m`, or `1h`. |
| `manualApprovalThreshold` | number | No | Manual-approval threshold in USD. |
| `manualApprovalEnabled` | boolean | Yes | Whether large top-ups need manual approval. |
| `updatedAt` | string | Response only | Last update time. |

```json theme={null}
{
  "singleRechargeLimit": 500,
  "dailyTotalLimit": 2000,
  "dailyMaxCount": 5,
  "rechargeInterval": "30m",
  "manualApprovalThreshold": 1000,
  "manualApprovalEnabled": true,
  "autoSuspendEnabled": true
}
```

`autoSuspendEnabled` is a fixed safety behaviour in the current version: Clink always returns `true`, and the value is ignored on write. It is present in the request schema for compatibility, but it cannot disable auto-suspend. The PUT response returns the saved settings. A `portal:read`-only token can read this endpoint but receives `403` on PUT.

## Billing address

Read or replace the user's billing address.

```http theme={null}
GET {API_BASE_URL}/agent/platform/billing-address
PUT {API_BASE_URL}/agent/platform/billing-address
Authorization: Bearer {PORTAL_TOKEN}
Content-Type: application/json
```

| Field | Type | Required on PUT | Meaning |
| - | - | - | - |
| `country` | string | Yes | Country code/name. |
| `region` | string | Yes | State, province, or region. |
| `city` | string | Yes | City. |
| `line1` | string | Yes | Street address. |
| `line2` | string | No | Additional address line. |
| `postal_code` | string | Yes | Postal code. |
| `status` | string | Response only | Address status; ignored on write. |

```json theme={null}
{
  "country": "US",
  "region": "CA",
  "city": "San Francisco",
  "line1": "1 Market Street",
  "line2": "Suite 100",
  "postal_code": "94105"
}
```

The PUT response returns the updated address. Clink validates the required fields and reuses the existing wallet billing-address rules (including the configured country list and city/line1 requirements for those countries). A `portal:read`-only token receives `403` on PUT.

## Errors

Business errors use the standard envelope without `data`: `code` is a numeric business code and `msg` is a human-readable description. Judge an error by the HTTP status together with `code`, not by a string error code.

| HTTP status | Meaning |
| - | - |
| `400` | Invalid query parameters, request body, or billing address. |
| `401` | Missing, expired, or invalid portal token. |
| `403` | Wrong token audience, missing scope, or a platform client token used instead of a portal token. |
| `404` | Record not found or not owned by the token's customer. |
| `409` | Conflict: an idempotent `request_id` or `client_reference_id` was reused with different parameters (`portal_conflict`, `code` `71211011`), or a terminal result of the other class was already reported (`result_already_reported`, `code` `71211017`). |
| `410` | The credential result reporting window has expired (`report_window_expired`, `code` `71211018`). |
| `429` | Rate limit exceeded. |


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