# Transfer API

- 服务地址（示例，真实 Host 由接入团队提供）：`https://transfer-api.psp.example.com`
- 认证：Ed25519 签名
- 版本：1.0.0
- OpenAPI 规范：https://docs.potabank.com/openapi/transfer.openapi.yaml

Transfer service surface for merchant disbursement write flows. This service
only accepts transfer account secrets and rejects invoice/query/webhook/proof
traffic.
A missing X-API-ID header returns 400 INVALID_REQUEST. A missing X-API-Key
header or invalid credentials return 401 UNAUTHORIZED. A credential store
failure returns 500 INTERNAL_ERROR before the request handler runs.

## Transfers

### Create a transfer intent

**POST** `/api/v1/payments/transfers`

Requires a transfer account secret and `transfers:write` scope.

Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`;
at least one is required. When both are sent they must be equal,
otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519
signature covers `X-Idempotency-Key` when it is sent, otherwise
`Idempotency-Key`.

The same header key is also the transfer's business idempotency key:
reusing it for a different transfer request is rejected with
409 CONFLICT. The request body has no separate idempotency field.

**请求头**

| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `X-API-Key` | `string` | 必填 | — |
| `X-Timestamp` | `string` | 必填 | — |
| `X-Signature` | `string` | 必填 | — |
| `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 |
| `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 |

**请求体** (application/json · 必填)

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `payment_method` | `enum` | 必填 | 可选值：`pix`、`spei`、`upi`、`america_ach`、`paypal`、`chime`、`zelle`、`cash_app`、`venmo`、`emt`、`apple_pay` |
| `rail_variant` | `string` | 必填 | Canonical rail variant paired with payment_method. |
| `amount` | `integer<int64>` | 必填 | ≥ 1 |
| `currency` | `string` | 必填 | 最短 3 · 最长 3 |
| `beneficiary` | `object` | 可选 | — |
| `beneficiary.name` | `string` | 可选 | 最长 200 |
| `beneficiary.tax_id` | `string` | 可选 | 收款人证件号。账户 transfer_beneficiary_validation_mode 为 optional 或 required 且提交了本字段时，须符合账户市场格式：巴西 CPF（11 位数字含校验位）或 CNPJ （14 位，数字或字母数字，含官方校验位，例如 00.000.000/E08G-12）；墨西哥 RFC （12 或 13 位，日期段月份 01–12、日期 01–31）。格式错误返回 INVALID_REQUEST。 required 档未提交亦返回 INVALID_REQUEST。 · 最长 50 |
| `beneficiary.email` | `string<email>` | 可选 | 最长 200 |
| `beneficiary.phone` | `string` | 可选 | 最长 50 |
| `beneficiary.account` | `string` | 可选 | 最长 200 |
| `beneficiary.bank_code` | `string` | 可选 | 最长 20 |
| `beneficiary.branch_code` | `string` | 可选 | 最长 20 |
| `instruction` | `object` | 可选 | PIX QR may omit beneficiary; other transfer modes require beneficiary name and account. PIX and SPEI consume `instruction.tax_id`. It must be a string; a number or object returns INVALID_REQUEST. UPI, ACH, and hosted methods ignore this field and do not persist it as beneficiary.tax_id. When transfer_beneficiary_validation_mode is optional or required and the value is non-empty, it must match the account market format (Brazil CPF, numeric or alphanumeric CNPJ, or Mexico RFC) and must match `beneficiary.tax_id` when that field is also set. Off mode does not send the value upstream. PIX keeps an instruction-only tax_id off beneficiary.tax_id so older Gateway versions can replay the same request. SPEI likewise keeps an explicit RFC and an instruction-only alias off beneficiary.tax_id. Off mode does not backfill the channel RFC from beneficiary.tax_id or instruction.tax_id; an explicit instruction.beneficiary_rfc is still submitted unchanged. Optional and required modes persist the RFC decision for the channel and worker. SPEI `beneficiary_rfc` must be a string. When transfer_beneficiary_validation_mode is optional or required and this field is non-empty (including when beneficiary.tax_id is omitted), it must be a Mexican RFC (12 or 13 characters, month 01–12, day 01–31); otherwise the request returns INVALID_REQUEST. |
| `description` | `string` | 可选 | 最长 500 |
| `merchant_order_id` | `string` | 可选 | 最长 100 |
| `webhook_url` | `string<uri>` | 可选 | 最长 500 |
| `metadata` | `object` | 可选 | — |

**请求示例**

```bash
curl -X POST 'https://transfer-api.psp.example.com/api/v1/payments/transfers' \
  -H 'X-API-Key: <api-key>' \
  -H 'X-Timestamp: <timestamp>' \
  -H 'X-Signature: <signature>' \
  -H 'X-API-ID: <api-id>' \
  -H 'Content-Type: application/json' \
  -d '{
    "payment_method": "pix",
    "rail_variant": "pix_key",
    "amount": 1,
    "currency": "string",
    "beneficiary": {
      "name": "string",
      "tax_id": "string",
      "email": "user@example.com",
      "phone": "string",
      "account": "string",
      "bank_code": "string",
      "branch_code": "string"
    },
    "instruction": {},
    "description": "string",
    "merchant_order_id": "string",
    "webhook_url": "https://example.com",
    "metadata": {}
  }'
```

**响应**

#### 响应 201 Created

Transfer intent created

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | `string<uuid>` | 必填 | — |
| `transfer_number` | `string` | 必填 | — |
| `payment_method` | `string` | 必填 | — |
| `classification` | `object` | 必填 | — |
| `classification.classification_status` | `enum` | 必填 | 可选值：`resolved` |
| `classification.payment_category` | `string` | 必填 | — |
| `classification.payment_method` | `string` | 必填 | — |
| `classification.rail_variant` | `string` | 必填 | — |
| `amount` | `integer<int64>` | 必填 | — |
| `currency` | `string` | 必填 | — |
| `transferred_amount` | `integer<int64>` | 可选 | — |
| `returned_amount` | `integer<int64>` | 可选 | — |
| `status` | `enum` | 必填 | 可选值：`pending`、`processing`、`completed`、`failed`、`returned`、`canceled` |
| `state` | `string` | 可选 | — |
| `description` | `string` | 可选 | — |
| `merchant_order_id` | `string` | 可选 | — |
| `provider_transaction_id` | `string` | 可选 | — |
| `beneficiary` | `object` | 可选 | The beneficiary the merchant submitted and PSP sent upstream. Echoed back unchanged; a provider callback never overwrites it. |
| `beneficiary.name` | `string` | 可选 | 最长 200 |
| `beneficiary.tax_id` | `string` | 可选 | 收款人证件号。账户 transfer_beneficiary_validation_mode 为 optional 或 required 且提交了本字段时，须符合账户市场格式：巴西 CPF（11 位数字含校验位）或 CNPJ （14 位，数字或字母数字，含官方校验位，例如 00.000.000/E08G-12）；墨西哥 RFC （12 或 13 位，日期段月份 01–12、日期 01–31）。格式错误返回 INVALID_REQUEST。 required 档未提交亦返回 INVALID_REQUEST。 · 最长 50 |
| `beneficiary.email` | `string<email>` | 可选 | 最长 200 |
| `beneficiary.phone` | `string` | 可选 | 最长 50 |
| `beneficiary.account` | `string` | 可选 | 最长 200 |
| `beneficiary.bank_code` | `string` | 可选 | 最长 20 |
| `beneficiary.branch_code` | `string` | 可选 | 最长 20 |
| `actual_beneficiary` | `object` | 可选 | The beneficiary the provider reported back after settling the transfer. Absent until a callback carries one, and absent for good on providers that do not report it. Compare it against `beneficiary` to tell whether the money reached the requested account. |
| `actual_beneficiary.name` | `string` | 可选 | 最长 200 |
| `actual_beneficiary.tax_id` | `string` | 可选 | 收款人证件号。账户 transfer_beneficiary_validation_mode 为 optional 或 required 且提交了本字段时，须符合账户市场格式：巴西 CPF（11 位数字含校验位）或 CNPJ （14 位，数字或字母数字，含官方校验位，例如 00.000.000/E08G-12）；墨西哥 RFC （12 或 13 位，日期段月份 01–12、日期 01–31）。格式错误返回 INVALID_REQUEST。 required 档未提交亦返回 INVALID_REQUEST。 · 最长 50 |
| `actual_beneficiary.email` | `string<email>` | 可选 | 最长 200 |
| `actual_beneficiary.phone` | `string` | 可选 | 最长 50 |
| `actual_beneficiary.account` | `string` | 可选 | 最长 200 |
| `actual_beneficiary.bank_code` | `string` | 可选 | 最长 20 |
| `actual_beneficiary.branch_code` | `string` | 可选 | 最长 20 |
| `failure_reason` | `string` | 可选 | — |
| `failure_code` | `string` | 可选 | — |
| `created_at` | `string<date-time>` | 必填 | — |
| `updated_at` | `string<date-time>` | 必填 | — |
| `submitted_at` | `string<date-time>` | 可选 | — |
| `completed_at` | `string<date-time>` | 可选 | — |
| `failed_at` | `string<date-time>` | 可选 | — |
| `returned_at` | `string<date-time>` | 可选 | — |
| `authorization_url` | `string<uri>` | 可选 | Short-lived payer authorization URL, available after asynchronous channel submission. |
| `authorization_url_expires_at` | `string<date-time>` | 可选 | — |
| `metadata` | `object` | 可选 | — |


```json
{
  "id": "00000000-0000-7000-8000-000000000000",
  "transfer_number": "string",
  "payment_method": "string",
  "classification": {
    "classification_status": "resolved",
    "payment_category": "string",
    "payment_method": "string",
    "rail_variant": "string"
  },
  "amount": 0,
  "currency": "string",
  "transferred_amount": 0,
  "returned_amount": 0,
  "status": "pending",
  "state": "string",
  "description": "string",
  "merchant_order_id": "string",
  "provider_transaction_id": "string",
  "beneficiary": {
    "name": "string",
    "tax_id": "string",
    "email": "user@example.com",
    "phone": "string",
    "account": "string",
    "bank_code": "string",
    "branch_code": "string"
  },
  "actual_beneficiary": {
    "name": "string",
    "tax_id": "string",
    "email": "user@example.com",
    "phone": "string",
    "account": "string",
    "bank_code": "string",
    "branch_code": "string"
  },
  "failure_reason": "string",
  "failure_code": "string",
  "created_at": "2026-01-01T00:00:00Z",
  "updated_at": "2026-01-01T00:00:00Z",
  "submitted_at": "2026-01-01T00:00:00Z",
  "completed_at": "2026-01-01T00:00:00Z",
  "failed_at": "2026-01-01T00:00:00Z",
  "returned_at": "2026-01-01T00:00:00Z",
  "authorization_url": "https://example.com",
  "authorization_url_expires_at": "2026-01-01T00:00:00Z",
  "metadata": {}
}
```

#### 响应 400 Bad Request

The request body or path parameters are invalid.

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | `string` | 必填 | — |
| `message` | `string` | 必填 | — |


```json
{
  "code": "string",
  "message": "string"
}
```

#### 响应 401 Unauthorized

The credential kind or host does not match the transfer service.

#### 响应 403 Forbidden

The account secret lacks the required transfer scope.

#### 响应 404 Not Found

ACCOUNT_FEE_NOT_FOUND. No canonical transfer pricing policy matches this account, rail, currency and operation. The transfer is not created.

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | `string` | 必填 | — |
| `message` | `string` | 必填 | — |


```json
{
  "code": "string",
  "message": "string"
}
```

#### 响应 409 Conflict

The idempotency key or transfer state conflicts with the request.

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | `string` | 必填 | — |
| `message` | `string` | 必填 | — |


```json
{
  "code": "string",
  "message": "string"
}
```

#### 响应 500 Server Error

Credential lookup failed (INTERNAL_ERROR). The request handler was not executed.

### Cancel a transfer intent

**POST** `/api/v1/payments/transfers/{intent_id}/cancel`

Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`;
at least one is required. When both are sent they must be equal,
otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519
signature covers `X-Idempotency-Key` when it is sent, otherwise
`Idempotency-Key`.

**路径参数**

| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `intent_id` | `string` | 必填 | — |

**请求头**

| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `X-API-Key` | `string` | 必填 | — |
| `X-Timestamp` | `string` | 必填 | — |
| `X-Signature` | `string` | 必填 | — |
| `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 |
| `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 |

**请求体** (application/json)

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reason` | `string` | 可选 | 最长 500 |

**请求示例**

```bash
curl -X POST 'https://transfer-api.psp.example.com/api/v1/payments/transfers/{intent_id}/cancel' \
  -H 'X-API-Key: <api-key>' \
  -H 'X-Timestamp: <timestamp>' \
  -H 'X-Signature: <signature>' \
  -H 'X-API-ID: <api-id>' \
  -H 'Content-Type: application/json' \
  -d '{
    "reason": "string"
  }'
```

**响应**

#### 响应 200 OK

Transfer intent canceled

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `status` | `enum` | 必填 | 可选值：`canceled` |


```json
{
  "status": "canceled"
}
```

#### 响应 400 Bad Request

The request body or path parameters are invalid.

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | `string` | 必填 | — |
| `message` | `string` | 必填 | — |


```json
{
  "code": "string",
  "message": "string"
}
```

#### 响应 401 Unauthorized

The credential kind or host does not match the transfer service.

#### 响应 403 Forbidden

The account secret lacks the required transfer scope.

#### 响应 404 Not Found

The transfer does not exist for this account.

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | `string` | 必填 | — |
| `message` | `string` | 必填 | — |


```json
{
  "code": "string",
  "message": "string"
}
```

#### 响应 409 Conflict

The idempotency key or transfer state conflicts with the request.

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | `string` | 必填 | — |
| `message` | `string` | 必填 | — |


```json
{
  "code": "string",
  "message": "string"
}
```

#### 响应 500 Server Error

Credential lookup failed (INTERNAL_ERROR). The request handler was not executed.

### Request a transfer order sync

**POST** `/api/v1/orders/{order_id}/sync`

Requires a transfer account secret and `transfers:write` scope. A
non-terminal transfer is never advanced by this request: the gateway
delivers one workflow command derived from the idempotency key and
returns `action=update_transfer_workflow` with `synced=false` once the
Transfer workflow accepts it. The workflow observes the provider and
alone decides the terminal state and funds; read the outcome from
webhooks or the query service. Retrying with the same idempotency key
maps to the same workflow command. A transfer that is not yet submitted
or whose submission is unconfirmed (state `submission_unknown`, even when
a provider ID was saved) returns `action=none`. A completed
transfer only refreshes its channel fee (`action=query_channel_status`).
If the transfer's workflow run has failed, the request answers 503
until operations recover the workflow; merchant retries, with the same
or a new idempotency key, never restart it.

Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`;
at least one is required. When both are sent they must be equal,
otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519
signature covers `X-Idempotency-Key` when it is sent, otherwise
`Idempotency-Key`.

**路径参数**

| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `order_id` | `string<uuid>` | 必填 | — |

**请求头**

| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `X-API-Key` | `string` | 必填 | — |
| `X-Timestamp` | `string` | 必填 | — |
| `X-Signature` | `string` | 必填 | — |
| `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 |
| `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 |

**请求示例**

```bash
curl -X POST 'https://transfer-api.psp.example.com/api/v1/orders/{order_id}/sync' \
  -H 'X-API-Key: <api-key>' \
  -H 'X-Timestamp: <timestamp>' \
  -H 'X-Signature: <signature>' \
  -H 'X-API-ID: <api-id>'
```

**响应**

#### 响应 200 OK

Sync request handled; see `action` for what was done.

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `order_id` | `string<uuid>` | 必填 | — |
| `order_type` | `enum` | 必填 | 可选值：`transfer_intent`、`transfer_refund` |
| `action` | `enum` | 可选 | `update_transfer_workflow` means the Transfer workflow accepted the sync command; the gateway did not query the provider or write state. · 可选值：`none`、`query_channel_status`、`update_transfer_workflow` |
| `previous_state` | `string` | 必填 | — |
| `current_state` | `string` | 必填 | Local state when the response was built; workflow results arrive asynchronously. |
| `synced` | `boolean` | 必填 | Always false for `update_transfer_workflow`. |
| `synced_at` | `string<date-time>` | 必填 | — |
| `message` | `string` | 可选 | — |


```json
{
  "order_id": "00000000-0000-7000-8000-000000000000",
  "order_type": "transfer_intent",
  "action": "none",
  "previous_state": "string",
  "current_state": "string",
  "synced": false,
  "synced_at": "2026-01-01T00:00:00Z",
  "message": "string"
}
```

#### 响应 400 Bad Request

The request body or path parameters are invalid.

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | `string` | 必填 | — |
| `message` | `string` | 必填 | — |


```json
{
  "code": "string",
  "message": "string"
}
```

#### 响应 401 Unauthorized

The credential kind or host does not match the transfer service.

#### 响应 403 Forbidden

The account secret lacks the required transfer scope.

#### 响应 404 Not Found

The transfer does not exist for this account.

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | `string` | 必填 | — |
| `message` | `string` | 必填 | — |


```json
{
  "code": "string",
  "message": "string"
}
```

#### 响应 500 Server Error

Credential lookup failed (INTERNAL_ERROR). The request handler was not executed.

#### 响应 503 Unavailable

The Transfer workflow or provider cannot accept the request now; retry with the same idempotency key.

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | `string` | 必填 | — |
| `message` | `string` | 必填 | — |


```json
{
  "code": "string",
  "message": "string"
}
```
