{
  "openapi": "3.1.0",
  "info": {
    "title": "Jeeves Payment API",
    "version": "1.0.0",
    "summary": "Quote FX, create payments idempotently, and track them through signed webhooks.",
    "description": "Lock an FX rate, create a payment against it, and follow it to a terminal status.\nConventions, authentication and error handling are covered in the Jeeves Payment API\nIntegration Guide.\n\n**Getting credentials:** sandbox and production each need their own client credentials.\nEmail [support@tryjeeves.com](mailto:support@tryjeeves.com) to request them for either\nenvironment.\n\n**mTLS on every call:** all requests — including `POST /oauth/token` — require your\nclient certificate. The curl examples show the `--cert`/`--key` flags once, on the\ntoken call, and omit them elsewhere for brevity; add them to every request.\n\nLooking for cards, boletos, transactions, or webhook subscriptions? Those live in the\n[main API reference](/apis).\n",
    "contact": {
      "name": "Jeeves API Support"
    },
    "x-logo": {
      "altText": "Jeeves"
    }
  },
  "servers": [
    {
      "url": "https://public-sandbox.tryjeeves.com",
      "description": "Sandbox"
    },
    {
      "url": "https://public-api.jeev.es",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerToken": [
        "payments:read",
        "payments:write",
        "quotes:write",
        "beneficiaries:read"
      ]
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Exchange client credentials for a short-lived bearer token."
    },
    {
      "name": "Accounts",
      "description": "The accounts you can fund payments from."
    },
    {
      "name": "Beneficiaries",
      "description": "The people and businesses you pay, with their attached payment methods.\n"
    },
    {
      "name": "Quotes",
      "description": "Lock an FX rate before creating a payment."
    },
    {
      "name": "Payments",
      "description": "Create a payment against a quote and track it to a terminal status.\n"
    }
  ],
  "paths": {
    "/oauth/token": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Get an access token",
        "operationId": "createToken",
        "description": "Exchange your Client ID and Secret for a short-lived access token. Don't have them yet?\nEmail [support@tryjeeves.com](mailto:support@tryjeeves.com) — sandbox and production\neach need their own.\n\nCredentials go in the `Authorization: Basic base64(clientId:clientSecret)` header,\n**not** in the body. The client certificate is required on this call as on every other.\n\nRequest only the scopes the integration needs. A token cannot reach a capability\noutside its scopes.\n\n**Read `expires_in` from the response rather than hardcoding a lifetime** — it is\nconfigured platform-side and may change. Request a new token when the current one\nnears expiry; there is no refresh token.\n\nThe response is a standard **RFC 6749 §5.1** token response — `access_token`,\n`token_type`, `expires_in` and `scope`, in snake_case — so any off-the-shelf OAuth 2.0\nclient library can consume it unchanged. Note that `scope` reports the scopes actually\n**granted**, which may be narrower than the scopes you asked for; check it rather than\nassuming your request was honoured in full.\n",
        "security": [
          {
            "basicAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type",
                  "scope"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "client_credentials"
                    ],
                    "description": "Always `client_credentials`."
                  },
                  "scope": {
                    "type": "string",
                    "description": "Space-delimited list of scopes.",
                    "example": "accounts:read beneficiaries:read beneficiaries:write quotes:write payments:write payments:read"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                },
                "example": {
                  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
                  "token_type": "Bearer",
                  "expires_in": 300,
                  "scope": "accounts:read beneficiaries:read beneficiaries:write quotes:write payments:write payments:read"
                }
              }
            }
          },
          "401": {
            "description": "Client ID or Secret not recognised, or the credential was revoked. This endpoint\nfollows the OAuth 2.0 error format (RFC 6749 §5.2), **not** the Jeeves error envelope.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "examples": [
                        "invalid_client"
                      ]
                    },
                    "error_description": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "error": "invalid_client",
                  "error_description": "Invalid client: client is invalid"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/product-accounts": {
      "get": {
        "tags": [
          "Accounts"
        ],
        "summary": "List product accounts",
        "operationId": "listProductAccounts",
        "description": "List the accounts you can fund payments from.\n\nThe response body is a **JSON array with one entry per company**, each carrying its own\n`productAccounts`. It is a bare array, not an object with a `data` envelope. A\nsingle-company integration reads `[0].productAccounts`.\n\nTake two values from here: `productAccountId`, which you send as `productAccountId`\nwhen creating a payment, and `productAccountType`, which you send as\n`productAccountType` when creating a quote — except for a Bill Pay credit account,\nwhich is reported as `jeeves-pay-credit` and quoted as `credit-line`.\n\nOnly an account with `productAccountStatus: active` can fund a payment.\n",
        "security": [
          {
            "bearerToken": [
              "accounts:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Companies and their product accounts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "title": "Companies",
                  "description": "A JSON array with one entry per company.",
                  "items": {
                    "$ref": "#/components/schemas/CompanyProductAccounts"
                  }
                },
                "example": [
                  {
                    "companyId": "c8ddf33f-6244-4218-af7e-7a6fe0b13528",
                    "companyName": "Bluesky Inc.",
                    "productAccounts": [
                      {
                        "productAccountType": "self-funded",
                        "productAccountId": "2b6f0c94-7a15-4e83-9d20-6c8e3b5a1f79",
                        "productAccountCurrency": "USD",
                        "productAccountStatus": "active"
                      },
                      {
                        "productAccountType": "credit-line",
                        "productAccountId": "7c14a8d0-2e63-4f19-9b85-3d0f6a2c7e41",
                        "productAccountCurrency": "USD",
                        "productAccountStatus": "active"
                      },
                      {
                        "productAccountType": "crypto-wallet",
                        "productAccountId": "4e9b1d27-5c80-4a36-b1f4-8a2d6c0e7f13",
                        "productAccountCurrency": "USDC",
                        "productAccountStatus": "active"
                      }
                    ]
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/beneficiaries": {
      "get": {
        "tags": [
          "Beneficiaries"
        ],
        "summary": "List beneficiaries",
        "operationId": "listBeneficiaries",
        "description": "List your beneficiaries, each with the payment methods attached to it.\n\nUse `paymentMethods[].paymentMethodId` as `beneficiaryPaymentMethodId` when creating\na payment.\n\n**Pagination is page-based**, matching the rest of the Jeeves Public API. Pass `page`\nand `limit`; `count` in the response is the total matching the query across all pages,\nso stop once `page × limit` reaches it. The request `page` and `limit` are not echoed\nback — keep track of them yourself.\n\nThe envelope is `data` and `count`, and nothing else.\n\nDeleted beneficiaries and deleted payment methods are never returned, and there is no\n`status` field on either.\n",
        "security": [
          {
            "bearerToken": [
              "beneficiaries:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number. Defaults to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1,
              "examples": [
                1
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 50. Values above 200 are **clamped to 200** rather\nthan rejected.\n",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "examples": [
                50
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Free-text filter. Matches `name`, `firstName` and `lastName` as a substring, and a\npayment method's account number by suffix.\n",
            "schema": {
              "type": "string",
              "maxLength": 100,
              "examples": [
                "Acme"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of beneficiaries.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BeneficiaryListResponse"
                },
                "example": {
                  "data": [
                    {
                      "beneficiaryId": "3f1c9a52-6b7e-4d80-9a13-5c2e8f0b4d76",
                      "entityType": "individual",
                      "firstName": "Ana",
                      "lastName": "Gómez",
                      "name": null,
                      "bankCountry": "MX",
                      "bankCurrency": "MXN",
                      "createdAt": "2026-09-10T14:02:11Z",
                      "paymentMethods": [
                        {
                          "paymentMethodId": "b7d2e4a1-8c05-49f3-a6b2-1e7d9c3f5024",
                          "paymentMethod": "spei",
                          "accountNumber": "••••0001"
                        }
                      ]
                    },
                    {
                      "beneficiaryId": "8a4e7c13-2d95-4f6b-8e01-7b3a5d9c2f48",
                      "entityType": "business",
                      "firstName": null,
                      "lastName": null,
                      "name": "Tortillería del Norte S.A. de C.V.",
                      "bankCountry": "MX",
                      "bankCurrency": "MXN",
                      "createdAt": "2026-09-11T09:41:55Z",
                      "paymentMethods": [
                        {
                          "paymentMethodId": "c91a06b4-7f38-4e52-b8d7-0a5c4e1f9362",
                          "paymentMethod": "spei",
                          "accountNumber": "••••4417"
                        },
                        {
                          "paymentMethodId": "d04b18e7-5a29-4c63-9f81-2d6e7b0a4c15",
                          "paymentMethod": "wire",
                          "accountNumber": "••••1897"
                        }
                      ]
                    }
                  ],
                  "count": 42
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "post": {
        "tags": [
          "Beneficiaries"
        ],
        "summary": "Create a beneficiary",
        "operationId": "createBeneficiary",
        "description": "Create a person or a business you pay. The beneficiary is created **with no payment\nmethods**; read what the corridor needs with `GET /v1/beneficiary-schemas`, then attach\na payment method with `POST /v1/payment-methods`.\n\nSend `firstName` and `lastName` for an `individual`, or `name` for a `business`. Each\nname field is at most 255 characters. A required name field that is missing, or a name\nfield that belongs to the other `entityType`, is a `422` (see below). Unknown fields are\nrejected with `unknown_field`, and a JSON array body is a `400`.\n\n`bankCurrency` must be one of the supported currencies listed on the field; any other\nvalue is a `400` with `unsupported_value`. A `bankCountry` that is well formed but not\na known country is a `422` with `unsupported_value`.\n\n**Not idempotent.** No `Idempotency-Key` is read; one that is sent is ignored. The\nduplicate check is your retry protection: a beneficiary with the same name, bank\ncountry and bank currency returns `422` with `errors[].code: duplicate` on the name\nfield or fields (`name` for a business, `firstName` and `lastName` for an individual).\nThat check also matches a Boleto-source vendor that `GET /v1/beneficiaries` does not\nlist. Two simultaneous identical requests can both succeed, because nothing enforces\nuniqueness at the database.\n",
        "security": [
          {
            "bearerToken": [
              "beneficiaries:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateBeneficiaryRequest"
              },
              "examples": {
                "individual": {
                  "summary": "An individual",
                  "value": {
                    "entityType": "individual",
                    "firstName": "Ana",
                    "lastName": "Gómez",
                    "bankCountry": "MX",
                    "bankCurrency": "MXN"
                  }
                },
                "business": {
                  "summary": "A business",
                  "value": {
                    "entityType": "business",
                    "name": "Tortillería del Norte S.A. de C.V.",
                    "bankCountry": "MX",
                    "bankCurrency": "MXN"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Beneficiary created. It has no payment methods yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BeneficiaryCreated"
                },
                "example": {
                  "beneficiaryId": "3f1c9a52-6b7e-4d80-9a13-5c2e8f0b4d76",
                  "status": "active",
                  "createdAt": "2026-11-02T14:02:11Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`validation_failed`, in three shapes:\n\n- **Name fields that do not match `entityType`.** A required name field is\n  missing (`required`) or one that belongs to the other `entityType` was sent\n  (`unknown_field`). The offending fields are listed in **`errorData.errors[]`**,\n  not in a top-level `errors[]`.\n- **Duplicate beneficiary.** A beneficiary with the same name, bank country and\n  bank currency already exists. The top-level `errors[]` carries\n  `code: duplicate` on the name field or fields.\n- **Unknown bank country.** `errors[]` carries `unsupported_value` on\n  `bankCountry`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "entityTypeMismatch": {
                    "summary": "Name field for the wrong entity type",
                    "value": {
                      "message": "The name fields do not match entityType: individuals use firstName and lastName, businesses use name.",
                      "code": "validation_failed",
                      "statusCode": 422,
                      "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92",
                      "errorData": {
                        "errors": [
                          {
                            "field": "firstName",
                            "code": "required",
                            "message": "firstName is required."
                          },
                          {
                            "field": "lastName",
                            "code": "required",
                            "message": "lastName is required."
                          },
                          {
                            "field": "name",
                            "code": "unknown_field",
                            "message": "name is not allowed."
                          }
                        ]
                      }
                    }
                  },
                  "duplicate": {
                    "summary": "Beneficiary already exists (business)",
                    "value": {
                      "message": "A beneficiary with the same name, country and currency already exists",
                      "code": "validation_failed",
                      "statusCode": 422,
                      "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92",
                      "errors": [
                        {
                          "field": "name",
                          "code": "duplicate",
                          "message": "name already exists."
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/beneficiaries/{beneficiaryId}": {
      "get": {
        "tags": [
          "Beneficiaries"
        ],
        "summary": "Get a beneficiary",
        "operationId": "getBeneficiary",
        "description": "Fetch one beneficiary with its `contactInformation` and attached payment methods.\n\nThe response is the **bare beneficiary object**, identical to one entry of `data[]` in\n`GET /v1/beneficiaries` — there is no `data` or `count` envelope. Account numbers are\nmasked to the last four characters; an account of four characters or fewer is masked\ncompletely, and a method with no account number is `null`.\n\nAn unknown, deleted or malformed `beneficiaryId`, or one that belongs to another\ncustomer, returns the same `404 beneficiary_not_found` body. It is never a `403`.\n",
        "security": [
          {
            "bearerToken": [
              "beneficiaries:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "beneficiaryId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "3f1c9a52-6b7e-4d80-9a13-5c2e8f0b4d76"
          }
        ],
        "responses": {
          "200": {
            "description": "The beneficiary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Beneficiary"
                },
                "example": {
                  "beneficiaryId": "3f1c9a52-6b7e-4d80-9a13-5c2e8f0b4d76",
                  "entityType": "individual",
                  "firstName": "Ana",
                  "lastName": "Gómez",
                  "name": null,
                  "bankCountry": "MX",
                  "bankCurrency": "MXN",
                  "contactInformation": {
                    "streetAddress": null,
                    "city": null,
                    "state": null,
                    "postcode": null,
                    "countryCode": null,
                    "phoneNumber": null,
                    "emailAddress": null
                  },
                  "createdAt": "2026-09-10T14:02:11Z",
                  "paymentMethods": [
                    {
                      "paymentMethodId": "b7d2e4a1-8c05-49f3-a6b2-1e7d9c3f5024",
                      "paymentMethod": "spei",
                      "accountNumber": "••••0001"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`beneficiary_not_found` — unknown, deleted, malformed or other-customer id. The body\nis identical in every case.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "statusCode": 404,
                  "code": "beneficiary_not_found",
                  "message": "Beneficiary not found.",
                  "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/beneficiary-schemas": {
      "get": {
        "tags": [
          "Beneficiaries"
        ],
        "summary": "Get what a corridor needs",
        "operationId": "getBeneficiarySchemas",
        "description": "Ask what a corridor needs before you attach a payment method or create a payment. For\neach payment method on offer the response lists the `bankingInformation` fields, the\n`contactInformation` fields and the purpose codes.\n\n- `fields[].fieldName` values are the keys of `bankingInformation` on\n  `POST /v1/payment-methods`.\n- `contactFields[].fieldName` values are the keys of `contactInformation`. The list is\n  empty when the method needs none.\n- `purposeCodes[]` is the exact list that payment-time validation accepts.\n\nPatterns and `purposeCode` are enforced again when you create the payment.\n\n`entityType` is required because the form depends on it. Your company must hold the\nfunding source you name with `sourceCurrency` and `productAccountType`; the methods and\npurpose codes returned are those for that funding source. Pass `paymentMethod` to\nnarrow the response to one method.\n\n**Use the `paymentMethod` value from this response, unchanged**, when you call\n`POST /v1/payment-methods`. Reading a beneficiary\n(`GET /v1/beneficiaries`) reports the payment method in a normalised vocabulary\ninstead (`ach`, `spei`, `wire` and so on), so the two lists are not interchangeable.\n",
        "security": [
          {
            "bearerToken": [
              "beneficiaries:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "sourceCurrency",
            "in": "query",
            "required": true,
            "description": "The currency you pay from, as in `POST /v1/quotes`.",
            "schema": {
              "type": "string",
              "enum": [
                "USD",
                "MXN",
                "BRL",
                "COP",
                "EUR",
                "GBP",
                "CAD",
                "USDB",
                "USDC",
                "USDT",
                "EURC"
              ]
            },
            "example": "USD"
          },
          {
            "name": "productAccountType",
            "in": "query",
            "required": true,
            "description": "The kind of account you pay from.",
            "schema": {
              "type": "string",
              "enum": [
                "credit-line",
                "self-funded",
                "crypto-wallet"
              ]
            },
            "example": "self-funded"
          },
          {
            "name": "destinationCountry",
            "in": "query",
            "required": true,
            "description": "ISO 3166-1 alpha-2, uppercase.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{2}$"
            },
            "example": "MX"
          },
          {
            "name": "destinationCurrency",
            "in": "query",
            "required": true,
            "description": "The currency the beneficiary receives.",
            "schema": {
              "type": "string",
              "enum": [
                "USD",
                "MXN",
                "BRL",
                "COP",
                "EUR",
                "GBP",
                "CAD",
                "USDB",
                "USDC",
                "USDT",
                "EURC"
              ]
            },
            "example": "MXN"
          },
          {
            "name": "entityType",
            "in": "query",
            "required": true,
            "description": "Whether the beneficiary is a person or a business.",
            "schema": {
              "type": "string",
              "enum": [
                "individual",
                "business"
              ]
            },
            "example": "individual"
          },
          {
            "name": "paymentMethod",
            "in": "query",
            "required": false,
            "description": "Narrow the response to one payment method, using a `paymentMethod` value as this\nendpoint returns it.\n",
            "schema": {
              "type": "string"
            },
            "example": "local_bank_transfer"
          }
        ],
        "responses": {
          "200": {
            "description": "The payment methods on offer for the corridor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BeneficiarySchemaResponse"
                },
                "example": {
                  "paymentMethods": [
                    {
                      "paymentMethod": "local_bank_transfer",
                      "fields": [
                        {
                          "fieldName": "accountNumber",
                          "type": "string",
                          "required": true,
                          "pattern": "^\\d{10,18}$",
                          "description": "Account number"
                        },
                        {
                          "fieldName": "bankCode",
                          "type": "string",
                          "required": true,
                          "description": "Bank code"
                        }
                      ],
                      "contactFields": [],
                      "purposeCodes": [
                        {
                          "code": "purchase_of_services",
                          "description": "Purchase of services"
                        },
                        {
                          "code": "travel",
                          "description": "Travel"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`validation_failed` — a query parameter is missing or invalid. A missing parameter\nappears as `errors[].code: required`; there is no `missing_parameter` code. A\n`sourceCurrency` or `destinationCurrency` outside the supported list, or a\n`destinationCountry` that is not an ISO 3166-1 alpha-2 code, reports\n`unsupported_value`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Validation failed.",
                  "code": "validation_failed",
                  "statusCode": 400,
                  "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92",
                  "errors": [
                    {
                      "field": "entityType",
                      "code": "required",
                      "message": "entityType is required."
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "One of:\n\n| `code` | Meaning |\n| --- | --- |\n| `unsupported_corridor` | No payment method is available for this destination. |\n| `unsupported_payment_method` | The `paymentMethod` you asked for is not offered for this corridor. |\n| `validation_failed` | `errors[]` reports `unsupported_value` on `sourceCurrency` when your company does not hold that funding source. |\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "statusCode": 422,
                  "code": "unsupported_corridor",
                  "message": "This corridor is not supported.",
                  "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/payment-methods": {
      "post": {
        "tags": [
          "Beneficiaries"
        ],
        "summary": "Attach a payment method",
        "operationId": "createPaymentMethod",
        "description": "Attach a payment method to a beneficiary. Use the returned `paymentMethodId` as\n`beneficiaryPaymentMethodId` when you create a payment.\n\nThe corridor comes from the beneficiary's stored bank country and currency, so there\nare no corridor parameters. `paymentMethod` must be a value returned by\n`GET /v1/beneficiary-schemas`; the keys of `bankingInformation` must equal that\nresponse's `fields[].fieldName`, and each value is checked against its `pattern`.\n\n`contactInformation` is required when the method's `contactFields` is not empty,\nunless the beneficiary already has the value stored. When `contactFields` is empty,\nany contact key is rejected with `unknown_field`.\n\n**One payment method per rail per beneficiary.** The duplicate check looks at\n`paymentMethod` only, not at the account number. To replace a method you would have to\ndelete the old one first, and the public API has no delete.\n\nSend `paymentMethod` exactly as `GET /v1/beneficiary-schemas` returns it. The payment\nmethod on a beneficiary you read back is reported in a normalised vocabulary, so it\nmay not equal the value you sent.\n\n**Not idempotent.** No `Idempotency-Key` is read, and a retry after a success returns\n`409 payment_method_already_exists`.\n",
        "security": [
          {
            "bearerToken": [
              "beneficiaries:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentMethodRequest"
              },
              "example": {
                "beneficiaryId": "3f1c9a52-6b7e-4d80-9a13-5c2e8f0b4d76",
                "paymentMethod": "local_bank_transfer",
                "bankingInformation": {
                  "accountNumber": "0123456789",
                  "bankCode": "021000021"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payment method attached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentMethodCreated"
                },
                "example": {
                  "paymentMethodId": "b7d2e4a1-8c05-49f3-a6b2-1e7d9c3f5024",
                  "status": "active"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`beneficiary_not_found` — unknown, deleted, malformed or other-customer\n`beneficiaryId`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "statusCode": 404,
                  "code": "beneficiary_not_found",
                  "message": "Beneficiary not found.",
                  "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
                }
              }
            }
          },
          "409": {
            "description": "`payment_method_already_exists` — the beneficiary already has a payment method on\nthis rail, whatever its account number.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "statusCode": 409,
                  "code": "payment_method_already_exists",
                  "message": "This beneficiary already has a payment method on this rail.",
                  "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
                }
              }
            }
          },
          "422": {
            "description": "`validation_failed` with `errors[]`. Fields inside `bankingInformation` are named\n`bankingInformation.<field>`, `contactInformation` fields keep their\n`contactInformation.<field>` name, and `paymentMethod` is reported as\n`paymentMethod`. Reasons are `required`, `invalid_format` (the value fails its\n`pattern`, which the message names), `unsupported_value` (a `paymentMethod` the\ncorridor does not offer) and `unknown_field`. A wallet blocked by screening is also\na `422 validation_failed`, with no per-field detail.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Validation failed.",
                  "code": "validation_failed",
                  "statusCode": 422,
                  "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92",
                  "errors": [
                    {
                      "field": "bankingInformation.accountNumber",
                      "code": "invalid_format",
                      "message": "bankingInformation.accountNumber does not match the required pattern ^\\d{10,18}$."
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/quotes": {
      "post": {
        "tags": [
          "Quotes"
        ],
        "summary": "Create a quote",
        "operationId": "createQuote",
        "description": "Lock an FX rate for a payment.\n\n`amount` is the **destination** amount — what the beneficiary receives. The source\namount is derived from the rate and returned as `sourceAmount`.\n\n**A quote is single-use and expires.** It funds exactly one payment; reusing a consumed\nquote returns `422 quote_expired`. Read `expiresAt` from the response rather than\nassuming a fixed window — the TTL is configured per company.\n\nYour FX rate and fees depend on the kind of account you pay from, so pass the\n`productAccountType` of the account you will actually use — a quote priced for a\ndifferent account type does not reflect what you will pay.\n",
        "security": [
          {
            "bearerToken": [
              "quotes:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateQuoteRequest"
              },
              "example": {
                "sourceCurrency": "USD",
                "destinationCurrency": "MXN",
                "amount": "43035.00",
                "productAccountType": "self-funded"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Quote created and rate locked.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                },
                "example": {
                  "quoteId": "9f2c1b84-3e7a-4d1f-9c05-8b6e2a4f7d13",
                  "rate": "17.2140",
                  "sourceCurrency": "USD",
                  "sourceAmount": "2500.00",
                  "destinationAmount": "43035.00",
                  "destinationCurrency": "MXN",
                  "createdAt": "2026-09-10T15:04:05Z",
                  "expiresAt": "2026-09-10T15:09:05Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`unsupported_pair` — no rate is available for this currency pair on the selected\naccount type.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "statusCode": 422,
                  "code": "unsupported_pair",
                  "message": "No rate is available for this currency pair.",
                  "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/payments": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Create a payment",
        "operationId": "createPayment",
        "description": "Create a payment from a valid quote.\n\n## Idempotency\n\n`Idempotency-Key` is **required**. Generate a fresh UUID per payment attempt and store\nit before sending the request.\n\n- Retrying with the same key and the same body returns the **original result**, byte for\n  byte, and never creates a second payment.\n- Reusing the key with a **different** body returns `409 idempotency_conflict`.\n- Keys never expire. A key is permanently bound to its payment and cannot be reused\n  later for a different one.\n\nIf a request times out or you get a `5xx`, retry with the **same** key. That is the\nsafe path — generating a new key on retry is how duplicate payments happen.\n\n## Validation\n\nAll checks run before acceptance: quote freshness, beneficiary field patterns,\npurpose code, and available funds. A `422` means nothing was created.\n\n## Expired quotes\n\n`onQuoteExpiry` decides what happens if the quote expired before the payment arrived.\n`fail` (the default) returns `422 quote_expired`. `requote` prices a new quote at the\ncurrent rate and uses it — the destination amount is unchanged, the source amount moves.\n\nWhen `requote` is used, the response carries both `quoteId` (the quote that actually\npriced the payment) and `originalQuoteId` (the expired one you sent). **Compare them,\nand compare `sourceAmount` against what you expected** — a requote is the one case where\nyou pay a rate you were not previously shown, and no separate event announces it.\n\n## Scheduled payments\n\nSet `scheduledDate` for a future-dated payment. Then omit `quoteId` and send `amount`\ninstead — the rate is locked on the execution date, not at creation. `quoteId` and\n`scheduledDate` are mutually exclusive.\n",
        "security": [
          {
            "bearerToken": [
              "payments:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Customer-generated UUID, unique per payment and never reused.\n",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "1f8a6d90-4c37-4b52-9e81-0d5a7c2b6f34"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentRequest"
              },
              "examples": {
                "immediate": {
                  "summary": "Payment against a quote",
                  "value": {
                    "quoteId": "9f2c1b84-3e7a-4d1f-9c05-8b6e2a4f7d13",
                    "onQuoteExpiry": "requote",
                    "productAccountId": "2b6f0c94-7a15-4e83-9d20-6c8e3b5a1f79",
                    "beneficiaryId": "3f1c9a52-6b7e-4d80-9a13-5c2e8f0b4d76",
                    "beneficiaryPaymentMethodId": "b7d2e4a1-8c05-49f3-a6b2-1e7d9c3f5024",
                    "purposeCode": "purchase_of_services",
                    "reference": "Invoice 2026-0912"
                  }
                },
                "scheduled": {
                  "summary": "Future-dated payment (no quote)",
                  "value": {
                    "scheduledDate": "2026-10-15",
                    "amount": "43035.00",
                    "productAccountId": "2b6f0c94-7a15-4e83-9d20-6c8e3b5a1f79",
                    "beneficiaryId": "3f1c9a52-6b7e-4d80-9a13-5c2e8f0b4d76",
                    "beneficiaryPaymentMethodId": "b7d2e4a1-8c05-49f3-a6b2-1e7d9c3f5024",
                    "purposeCode": "purchase_of_services",
                    "reference": "Invoice 2026-0912"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payment accepted for processing. Also returned on a replayed request carrying a\npreviously used `Idempotency-Key` with an identical body.\n",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentCreated"
                },
                "example": {
                  "paymentId": "7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6",
                  "status": "accepted",
                  "quoteId": "4c81de07-15b2-4a93-8f6d-2e7c09ab5431",
                  "originalQuoteId": "9f2c1b84-3e7a-4d1f-9c05-8b6e2a4f7d13",
                  "rate": "17.1980",
                  "sourceAmount": "2502.33",
                  "destinationAmount": "43035.00",
                  "createdAt": "2026-09-10T15:06:11Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "`idempotency_conflict` — the same `Idempotency-Key` was reused with a different\nrequest body. Nothing was created. Use a new key for a new payment.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "statusCode": 409,
                  "code": "idempotency_conflict",
                  "message": "This idempotency key was already used with a different request.",
                  "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
                }
              }
            }
          },
          "422": {
            "description": "Well-formed but rejected. Nothing was created.\n\n| `code` | Meaning |\n| --- | --- |\n| `quote_expired` | Quote already consumed, or expired with `onQuoteExpiry: fail`. |\n| `validation_failed` | Well-formed but not valid for the corridor — e.g. `purposeCode` not in the corridor's list, or a beneficiary detail failing the corridor's rules. |\n\nA **missing** or malformed field, such as no `purposeCode`, is a `400` with no `code`\nand the field listed in `errors`.\n| `insufficient_funds` | Source account balance is below the quoted amount plus fees. |\n| `invalid_schedule` | `quoteId` and `scheduledDate` are mutually exclusive; scheduled payments need `amount`. |\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "quoteExpired": {
                    "summary": "Expired quote with onQuoteExpiry=fail",
                    "value": {
                      "statusCode": 422,
                      "code": "quote_expired",
                      "message": "The quote has expired or was already consumed.",
                      "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
                    }
                  },
                  "validationFailed": {
                    "summary": "Purpose code not valid for this corridor",
                    "value": {
                      "message": "purposeCode is not valid for this corridor.",
                      "statusCode": 422,
                      "code": "validation_failed",
                      "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/payments/{paymentId}": {
      "get": {
        "tags": [
          "Payments"
        ],
        "summary": "Get a payment",
        "operationId": "getPayment",
        "description": "Fetch a payment and its current lifecycle status.\n\nThis is the authoritative view of a payment. Webhooks tell you *when* something changed;\nthis endpoint tells you what is true now. Reconcile against it if you miss a delivery or\nreceive events out of order.\n\n`originalQuoteId` is non-null only when the payment was requoted. In that case `quoteId`\nis the quote that actually priced it and `rate` matches that quote.\n\nA payment belonging to another customer returns `404`, not `403` — we never confirm the\nexistence of a resource you cannot access.\n",
        "security": [
          {
            "bearerToken": [
              "payments:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "paymentId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6"
          }
        ],
        "responses": {
          "200": {
            "description": "The payment.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Payment"
                },
                "example": {
                  "paymentId": "7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6",
                  "status": "in-progress",
                  "quoteId": "4c81de07-15b2-4a93-8f6d-2e7c09ab5431",
                  "originalQuoteId": "9f2c1b84-3e7a-4d1f-9c05-8b6e2a4f7d13",
                  "rate": "17.1980",
                  "sourceAmount": "2502.33",
                  "destinationAmount": "43035.00",
                  "productAccountId": "2b6f0c94-7a15-4e83-9d20-6c8e3b5a1f79",
                  "beneficiaryId": "3f1c9a52-6b7e-4d80-9a13-5c2e8f0b4d76",
                  "beneficiaryPaymentMethodId": "b7d2e4a1-8c05-49f3-a6b2-1e7d9c3f5024",
                  "purposeCode": "purchase_of_services",
                  "reference": "Invoice 2026-0912",
                  "scheduledDate": null,
                  "failureReason": null,
                  "createdAt": "2026-09-10T15:06:11Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`payment_not_found` — unknown `paymentId`, or it belongs to another customer.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "statusCode": 404,
                  "code": "payment_not_found",
                  "message": "Payment not found.",
                  "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/payments/{paymentId}/cancel": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Cancel a payment",
        "operationId": "cancelPayment",
        "description": "Cancel a payment that has not been released yet. There is no request body.\n\nA payment can be cancelled only while its status is `in-review` or `scheduled`,\nincluding a payment held by an approval policy. An `accepted` payment is released too\nquickly to cancel, and a payment that is `in-progress` or later cannot be cancelled.\n\n**Not idempotent.** A retry returns `422 not_cancelable` because the first call\nsucceeded, and so does cancelling a payment that is already `canceled`. After a\ntimeout, read `GET /v1/payments/{paymentId}` before you retry.\n\nSide effects of a successful cancel:\n\n- A `canceled` `payment.status.updated` webhook is sent.\n- `beneficiaryPaymentMethodId` becomes `null` when you read the payment.\n- The quote stays spent; it cannot fund another payment.\n- A pending approval workflow for the payment is removed.\n",
        "security": [
          {
            "bearerToken": [
              "payments:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "paymentId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6"
          }
        ],
        "responses": {
          "200": {
            "description": "Payment cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentCanceled"
                },
                "example": {
                  "paymentId": "7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6",
                  "status": "canceled",
                  "canceledAt": "2026-11-02T16:20:45Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`payment_not_found` — unknown `paymentId`, an id that is not a UUID, a payment\nthat belongs to another customer, or a payment that was not created through the\nAPI.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "statusCode": 404,
                  "code": "payment_not_found",
                  "message": "Payment not found.",
                  "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
                }
              }
            }
          },
          "422": {
            "description": "`not_cancelable` — the payment is not in a status that can be cancelled, including\na payment that was already cancelled.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "statusCode": 422,
                  "code": "not_cancelable",
                  "message": "The payment can no longer be cancelled.",
                  "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/payments/{paymentId}/receipts": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Upload a receipt for a payment",
        "operationId": "createPaymentReceipt",
        "description": "Upload a receipt for a payment.\n\nThe only `multipart/form-data` endpoint in this API: send a single `file` part.\n\nOne receipt per request. Receipts **append**: posting again adds another rather than\nreplacing the first.\n\nThe file type is determined from the file's **content**, not from its extension or the\n`Content-Type` you send. Supported: `pdf`, `jpeg`, `jpg`, `png`, `tiff`, up to 5 MB.\n\nReceipts are stored as documents only — they are never read, parsed or put through OCR.\n\nNo `Idempotency-Key` is required, and one that is sent is ignored — a repeated\nupload creates a duplicate receipt, not a duplicate payment.\n",
        "security": [
          {
            "bearerToken": [
              "payments:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "paymentId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The receipt. Type is validated from its content; max 5 MB."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Receipt uploaded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentReceipt"
                },
                "example": {
                  "receiptId": "0f9e8a05-12b1-438e-a95f-edf39969e884",
                  "paymentId": "7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6",
                  "fileName": "payment-receipt-8821.pdf",
                  "contentType": "application/pdf",
                  "sizeBytes": 184320,
                  "createdAt": "2026-10-07T09:14:33Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`payment_not_found` — unknown `paymentId`, or it belongs to another customer.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large` — the file is above the 5 MB limit.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "`unsupported_media_type` — the file type is outside the supported list.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`validation_failed` — the payment's status does not accept receipts.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    }
  },
  "webhooks": {
    "paymentStatusUpdated": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "payment.status.updated",
        "operationId": "paymentStatusUpdatedWebhook",
        "description": "Jeeves sends this to your registered callback URL on **every** payment status change.\n\nYour callback URL, signing secret, optional mTLS and IP allowlist are configured in the\nJeeves web app. There is no registration API.\n\n## Verify before you trust\n\n1. Read `X-Jeeves-Timestamp` and reject the delivery if the skew exceeds **5 minutes**.\n2. Compute `HMAC-SHA256(secret, timestamp + \".\" + rawBody)`, hex-encoded, and compare it\n   to `X-Jeeves-Signature` using a **constant-time** comparison.\n3. Compute the HMAC over the **raw request body**, before any JSON parsing or\n   re-serialisation. Re-encoding changes the bytes and breaks the signature.\n\n## Delivery semantics\n\n- **At-least-once.** The same event may arrive more than once — deduplicate on `eventId`.\n- **Order is not guaranteed.** Reconcile using `occurredAt`, and treat\n  `GET /v1/payments/{paymentId}` as the source of truth. Never assume the latest delivery\n  reflects the latest state.\n- Respond `2xx` within **10 seconds**, then process asynchronously. Any other response,\n  or a timeout, is retried with exponential backoff for for about **26 hours**.\n- Payloads carry the payment id, status and failure reason only — never beneficiary or\n  banking details.\n- Webhook deliveries do not count against your rate limits.\n\nTolerate unrecognised `status` values and unrecognised fields — both are added additively.\n",
        "parameters": [
          {
            "name": "X-Jeeves-Signature",
            "in": "header",
            "required": true,
            "description": "HMAC-SHA256 over `timestamp + \".\" + rawBody`, hex-encoded.",
            "schema": {
              "type": "string"
            },
            "example": "4f2b8c1e9a7d305f6b2e4c8a1d9f0b3e7c5a2d8f4b6e1c9a3d7f0b5e2c8a4d61"
          },
          {
            "name": "X-Jeeves-Timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds. Reject if the skew exceeds 5 minutes.",
            "schema": {
              "type": "integer"
            },
            "example": 1789045710
          },
          {
            "name": "X-Jeeves-Event-Id",
            "in": "header",
            "required": true,
            "description": "Deduplication key, matching `eventId` in the body.",
            "schema": {
              "type": "string"
            },
            "example": "evt_c91d20"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaymentStatusEvent"
              },
              "example": {
                "eventId": "evt_c91d20",
                "eventType": "payment.status.updated",
                "occurredAt": "2026-09-10T15:08:30Z",
                "data": {
                  "paymentId": "7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6",
                  "status": "in-progress",
                  "failureReason": null
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged. Return any 2xx within 10 seconds. The body is ignored.\n"
          },
          "default": {
            "description": "Any non-2xx response, or a timeout, triggers retry with exponential backoff for for\nabout 26 hours.\n"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "basicAuth": {
        "type": "http",
        "scheme": "basic",
        "description": "`base64(clientId:clientSecret)`. Used **only** on `POST /oauth/token`.\n"
      },
      "bearerToken": {
        "type": "oauth2",
        "description": "OAuth 2.0 client credentials. The token carries your company identity and the granted\nscopes. A client certificate is required alongside it on every request.\n",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "/oauth/token",
            "scopes": {
              "accounts:read": "Read product accounts.",
              "beneficiaries:read": "Read beneficiaries and their payment methods.",
              "beneficiaries:write": "Create beneficiaries and attach payment methods.",
              "quotes:write": "Create FX quotes.",
              "payments:read": "Read payments and their status.",
              "payments:write": "Create and cancel payments."
            }
          }
        }
      }
    },
    "headers": {
      "XRateLimitLimit": {
        "description": "Requests allowed in the current window.",
        "schema": {
          "type": "integer"
        },
        "example": 300
      },
      "XRateLimitRemaining": {
        "description": "Requests left in the current window.",
        "schema": {
          "type": "integer"
        },
        "example": 297
      },
      "XRateLimitReset": {
        "description": "Unix time at which the window resets.",
        "schema": {
          "type": "integer"
        },
        "example": 1789045760
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer"
        },
        "example": 12
      }
    },
    "responses": {
      "BadRequest": {
        "description": "`400` — the request could not be accepted. Two codes are returned here and they mean\ndifferent things: `invalid_request_body` is a body that could not be parsed at all\n(malformed JSON, or a JSON value that is not an object), and `validation_failed` is a\nwell-formed body whose field values were rejected — the offending fields are listed in\n`errors`.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "message": "Validation failed.",
              "code": "validation_failed",
              "statusCode": 400,
              "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92",
              "errors": [
                {
                  "field": "amount",
                  "code": "invalid_format",
                  "message": "amount has an invalid format."
                }
              ]
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`401` — missing, invalid, or expired bearer token. Carries `code: unauthorized`. Request a\nnew token and retry.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "statusCode": 401,
              "code": "unauthorized",
              "message": "Unauthorized request.",
              "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
            }
          }
        }
      },
      "Forbidden": {
        "description": "`403` — the token lacks the required scope, the client certificate is missing or invalid,\nor your source IP is not on the allowlist. Carries `code: forbidden`.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "statusCode": 403,
              "code": "forbidden",
              "message": "The token does not carry the required scope.",
              "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
            }
          }
        }
      },
      "RateLimited": {
        "description": "`429` — rate limit exceeded. **Nothing was executed**, not even partially. Back off\nexponentially with jitter, honouring `Retry-After`.\n",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/XRateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/XRateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "statusCode": 429,
              "code": "rate_limit",
              "message": "API credential limit of 300 requests per minute exceeded.",
              "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
            }
          }
        }
      },
      "ServerError": {
        "description": "`500` — unexpected error. Idempotent requests are safe to retry; for\n`POST /v1/payments` retry with the **same** `Idempotency-Key`.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "statusCode": 500,
              "code": "service_error",
              "message": "An unexpected error occurred.",
              "requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "message",
          "code",
          "statusCode",
          "requestId"
        ],
        "description": "Every error carries the same four fields: `message`, `code`, `statusCode` and\n`requestId`. `errors` is added **only** on field-level validation failures.\n\nBranch on `statusCode` first, then refine on `code` — it is always there, so you never\nhave to test for its presence.\n",
        "properties": {
          "message": {
            "type": "string",
            "description": "Human-readable description. Localised; may be reworded without notice."
          },
          "code": {
            "type": "string",
            "description": "Stable error identifier, present on every error. Safe to branch on — these strings\nare a published contract and are not reworded.\n",
            "examples": [
              "quote_expired"
            ]
          },
          "statusCode": {
            "type": "integer",
            "description": "HTTP status code, repeated in the body for convenience.",
            "examples": [
              422
            ]
          },
          "requestId": {
            "type": "string",
            "description": "Unique id for this request. Quote it to Jeeves support — it is the fastest way to\nfind what happened. Echoes your `X-Request-Id` when you send one.\n",
            "examples": [
              "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92"
            ]
          },
          "errorData": {
            "type": "object",
            "additionalProperties": true,
            "description": "Extra structured detail, present on a few errors only. For example, the `422` on\n`POST /v1/beneficiaries` for a name field that does not match `entityType` lists\nthe offending fields in `errorData.errors[]`, each with the same `field`, `code`\nand `message` as a top-level `errors[]` entry. Do not rely on it elsewhere.\n"
          },
          "errors": {
            "type": "array",
            "description": "Field-level validation failures, one entry per offending field. Present on\n`validation_failed`, alongside the top-level `code`.\n",
            "items": {
              "type": "object",
              "required": [
                "field",
                "code",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string",
                  "description": "The offending field. Nested fields are dot-joined.",
                  "examples": [
                    "purposeCode"
                  ]
                },
                "code": {
                  "type": "string",
                  "enum": [
                    "required",
                    "invalid_format",
                    "unsupported_value",
                    "out_of_range",
                    "unknown_field",
                    "duplicate"
                  ],
                  "description": "Why this field was rejected. `unknown_field` means the field is not part of\nthe request at all — unrecognised fields are rejected, not ignored.\n`duplicate` means a record with the same value already exists, for example a\nbeneficiary with the same name, bank country and bank currency.\n",
                  "examples": [
                    "required"
                  ]
                },
                "message": {
                  "type": "string",
                  "description": "Human-readable description of this field's failure. Localised.",
                  "examples": [
                    "purposeCode is required."
                  ]
                }
              }
            }
          }
        }
      },
      "TokenResponse": {
        "type": "object",
        "description": "A standard RFC 6749 §5.1 token response. Field names are snake_case, as the\nspecification requires.\n",
        "required": [
          "access_token",
          "token_type",
          "expires_in"
        ],
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Send as `Authorization: Bearer <access_token>`."
          },
          "token_type": {
            "type": "string",
            "enum": [
              "Bearer"
            ]
          },
          "expires_in": {
            "type": "integer",
            "description": "Lifetime in seconds. Read this value rather than hardcoding it — it is configured\nplatform-side and is currently far shorter than an hour.\n",
            "examples": [
              300
            ]
          },
          "scope": {
            "type": "string",
            "description": "Space-delimited list of the scopes actually **granted**. This may be narrower than\nthe `scope` you requested — unknown or ungranted scopes are dropped rather than\nrejected — so check it before relying on a capability.\n",
            "examples": [
              "accounts:read beneficiaries:read"
            ]
          }
        }
      },
      "AccountType": {
        "type": "string",
        "description": "The kind of account funding the payment. Take this from the `productAccountType` of the\naccount you intend to pay from, via `GET /v1/product-accounts` — with one exception, see\nthe `credit-line` row.\n\n| Value | Plain meaning |\n| --- | --- |\n| `self-funded` | Your own pre-funded balance at Jeeves. |\n| `credit-line` | Your Jeeves credit line. Also the value to send for a Bill Pay credit account, which `GET /v1/product-accounts` reports as `jeeves-pay-credit`. |\n| `crypto-wallet` | Your stablecoin balance. Its currency is reported as a stablecoin ticker (`USDB`, `USDC`, `USDT` or `EURC`) rather than an ISO 4217 code, and it changes the purpose-code corridors. |\n",
        "enum": [
          "self-funded",
          "crypto-wallet",
          "credit-line"
        ]
      },
      "ProductAccountType": {
        "type": "string",
        "description": "The kind of account, as `GET /v1/product-accounts` reports it. A quote accepts only\n`self-funded`, `credit-line` and `crypto-wallet`: send `credit-line` for a\n`jeeves-pay-credit` account, and never send `jeeves-pay-external`. See\n[Funding accounts](/payments/funding/product-accounts#account-types) for what each\nvalue means.\n\nTreat this enum as open — tolerate values you do not recognise.\n",
        "enum": [
          "self-funded",
          "crypto-wallet",
          "credit-line",
          "jeeves-pay-external",
          "jeeves-pay-credit"
        ]
      },
      "PaymentStatus": {
        "type": "string",
        "description": "Payment lifecycle status.\n\n| Status | Meaning |\n| --- | --- |\n| `accepted` | Passed all validations and accepted for processing. |\n| `in-review` | Held for compliance or manual review before release. |\n| `scheduled` | Accepted and queued for a future execution date. |\n| `in-progress` | Released for processing; funds are moving. |\n| `completed` | Funds delivered to the beneficiary. |\n| `rejected` | **Terminal.** Declined before processing; no funds moved. |\n| `failed` | **Terminal.** Processing started but could not complete; funds returned. |\n| `canceled` | **Terminal.** Stopped while held; no funds moved. |\n| `refunded` | **Terminal.** A completed payment was returned in full. |\n\nTerminal states are `completed`, `rejected`, `failed`, `canceled` and `refunded`.\nA payment never re-enters an earlier state.\n\nTreat this enum as open — tolerate values you do not recognise.\n",
        "enum": [
          "accepted",
          "in-review",
          "scheduled",
          "in-progress",
          "completed",
          "rejected",
          "failed",
          "canceled",
          "refunded"
        ]
      },
      "PaymentWebhookStatus": {
        "type": "string",
        "description": "Statuses that send a `payment.status.updated` webhook. A subset of `PaymentStatus` —\n`accepted` is never sent as a webhook.\nTreat as open — tolerate values you do not recognise.\n",
        "enum": [
          "in-review",
          "scheduled",
          "in-progress",
          "completed",
          "failed",
          "canceled",
          "rejected",
          "refunded"
        ]
      },
      "ProductAccountStatus": {
        "type": "string",
        "description": "Only `active` accounts can fund a payment — treat every other status\n(`pending`, `in-active`, `deactivate`, `closed`) as unusable for funding.\n",
        "enum": [
          "active",
          "pending",
          "in-active",
          "deactivate",
          "closed"
        ]
      },
      "ProductAccount": {
        "type": "object",
        "required": [
          "productAccountType",
          "productAccountId",
          "productAccountCurrency",
          "productAccountStatus"
        ],
        "properties": {
          "productAccountType": {
            "$ref": "#/components/schemas/ProductAccountType"
          },
          "productAccountId": {
            "type": "string",
            "description": "Send verbatim as `productAccountId` when creating a payment."
          },
          "productAccountCurrency": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 4217 alpha code, or one of the supported stablecoin tickers (`USDB`, `USDC`,\n`USDT`, `EURC`) — `crypto-wallet` accounts report a ticker. `null` when the stored\nvalue is not a recognised currency code. Must match the quote's `sourceCurrency`.\n",
            "examples": [
              "USD",
              "USDC"
            ]
          },
          "productAccountStatus": {
            "$ref": "#/components/schemas/ProductAccountStatus"
          }
        }
      },
      "CompanyProductAccounts": {
        "type": "object",
        "required": [
          "companyId",
          "companyName",
          "productAccounts"
        ],
        "properties": {
          "companyId": {
            "type": "string",
            "examples": [
              "c8ddf33f-6244-4218-af7e-7a6fe0b13528"
            ]
          },
          "companyName": {
            "type": "string",
            "examples": [
              "Bluesky Inc."
            ]
          },
          "productAccounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProductAccount"
            }
          }
        }
      },
      "PaymentMethodSummary": {
        "type": "object",
        "required": [
          "paymentMethodId",
          "paymentMethod",
          "accountNumber"
        ],
        "properties": {
          "paymentMethodId": {
            "type": "string",
            "format": "uuid",
            "description": "Pass as `beneficiaryPaymentMethodId` when creating a payment."
          },
          "paymentMethod": {
            "type": "string",
            "description": "The rail, from a stable public set. A rail with no dedicated member is reported as\n`local-clearing` (a country-specific domestic system) or `other`. Treat as open.\n",
            "examples": [
              "spei",
              "wire",
              "ach",
              "swift",
              "sepa",
              "faster-payments",
              "ted",
              "pix",
              "boleto",
              "rtgs",
              "bank-transfer",
              "local-clearing",
              "ethereum",
              "base",
              "solana",
              "tron",
              "polygon",
              "other"
            ]
          },
          "accountNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The destination account, masked: only the last four characters are shown. An account\nof four characters or fewer is masked completely, and a method with no account number\nis `null`.\n",
            "examples": [
              "••••0001"
            ]
          }
        }
      },
      "Beneficiary": {
        "type": "object",
        "required": [
          "beneficiaryId",
          "entityType",
          "bankCountry",
          "bankCurrency",
          "createdAt",
          "paymentMethods"
        ],
        "properties": {
          "beneficiaryId": {
            "type": "string",
            "format": "uuid"
          },
          "entityType": {
            "type": "string",
            "enum": [
              "individual",
              "business"
            ],
            "description": "Determines which name fields are populated: `individual` uses `firstName` and\n`lastName`; `business` uses `name`.\n"
          },
          "firstName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present for individuals."
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present for individuals."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Registered business name. Present for businesses."
          },
          "bankCountry": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2.",
            "examples": [
              "MX"
            ]
          },
          "bankCurrency": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 4217 alpha code, or one of the supported stablecoin tickers (`USDB`, `USDC`,\n`USDT`, `EURC`). `null` when the stored value is not a recognised currency code.\n",
            "examples": [
              "MXN"
            ]
          },
          "contactInformation": {
            "$ref": "#/components/schemas/ContactInformation"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "paymentMethods": {
            "type": "array",
            "description": "Every live payment method attached to this beneficiary. May be empty.",
            "items": {
              "$ref": "#/components/schemas/PaymentMethodSummary"
            }
          }
        }
      },
      "ContactInformation": {
        "type": "object",
        "description": "The beneficiary's contact details. Every key is always present and is `null` when the\nvalue is not set.\n",
        "properties": {
          "streetAddress": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": [
              "string",
              "null"
            ]
          },
          "postcode": {
            "type": [
              "string",
              "null"
            ]
          },
          "countryCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 3166-1 **alpha-3**.",
            "examples": [
              "MEX"
            ]
          },
          "phoneNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "emailAddress": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "CreateBeneficiaryRequest": {
        "type": "object",
        "required": [
          "entityType",
          "bankCountry",
          "bankCurrency"
        ],
        "additionalProperties": false,
        "properties": {
          "entityType": {
            "type": "string",
            "enum": [
              "individual",
              "business"
            ],
            "description": "`individual` takes `firstName` and `lastName`; `business` takes `name`. A name field\nthat belongs to the other type is rejected.\n"
          },
          "firstName": {
            "type": "string",
            "maxLength": 255,
            "description": "Required for an `individual`.",
            "examples": [
              "Ana"
            ]
          },
          "lastName": {
            "type": "string",
            "maxLength": 255,
            "description": "Required for an `individual`.",
            "examples": [
              "Gómez"
            ]
          },
          "name": {
            "type": "string",
            "maxLength": 255,
            "description": "Registered business name. Required for a `business`.",
            "examples": [
              "Tortillería del Norte S.A. de C.V."
            ]
          },
          "bankCountry": {
            "type": "string",
            "pattern": "^[A-Z]{2}$",
            "description": "ISO 3166-1 alpha-2, uppercase.",
            "examples": [
              "MX"
            ]
          },
          "bankCurrency": {
            "type": "string",
            "enum": [
              "USD",
              "MXN",
              "BRL",
              "COP",
              "EUR",
              "GBP",
              "CAD",
              "USDB",
              "USDC",
              "USDT",
              "EURC"
            ],
            "description": "ISO 4217 alpha code, or a supported stablecoin ticker.",
            "examples": [
              "MXN"
            ]
          }
        }
      },
      "BeneficiaryCreated": {
        "type": "object",
        "required": [
          "beneficiaryId",
          "status",
          "createdAt"
        ],
        "properties": {
          "beneficiaryId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "active"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SchemaField": {
        "type": "object",
        "required": [
          "fieldName",
          "type",
          "required",
          "description"
        ],
        "properties": {
          "fieldName": {
            "type": "string",
            "description": "The key to send in the request object this field belongs to.",
            "examples": [
              "accountNumber"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "string",
              "enum",
              "number"
            ]
          },
          "required": {
            "type": "boolean"
          },
          "pattern": {
            "type": "string",
            "description": "Regular expression the value must match. Present when the field has one."
          },
          "allowedValues": {
            "type": "array",
            "description": "The values the field accepts. Present when `type` is `enum`.",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "number"
                }
              ]
            }
          },
          "description": {
            "type": "string"
          }
        }
      },
      "PurposeCodeOption": {
        "type": "object",
        "required": [
          "code",
          "description"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Send as `purposeCode` when creating a payment.",
            "examples": [
              "purchase_of_services"
            ]
          },
          "description": {
            "type": "string"
          }
        }
      },
      "BeneficiarySchemaPaymentMethod": {
        "type": "object",
        "required": [
          "paymentMethod",
          "fields",
          "contactFields",
          "purposeCodes"
        ],
        "properties": {
          "paymentMethod": {
            "type": "string",
            "description": "Send exactly this value as `paymentMethod` on `POST /v1/payment-methods`. It is not\nthe normalised vocabulary used when you read a beneficiary.\n",
            "examples": [
              "local_bank_transfer"
            ]
          },
          "fields": {
            "type": "array",
            "description": "The keys of `bankingInformation`.",
            "items": {
              "$ref": "#/components/schemas/SchemaField"
            }
          },
          "contactFields": {
            "type": "array",
            "description": "The keys of `contactInformation`. Empty when the method needs none.",
            "items": {
              "$ref": "#/components/schemas/SchemaField"
            }
          },
          "purposeCodes": {
            "type": "array",
            "description": "The exact list of purpose codes that payment-time validation accepts.",
            "items": {
              "$ref": "#/components/schemas/PurposeCodeOption"
            }
          }
        }
      },
      "BeneficiarySchemaResponse": {
        "type": "object",
        "required": [
          "paymentMethods"
        ],
        "properties": {
          "paymentMethods": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BeneficiarySchemaPaymentMethod"
            }
          }
        }
      },
      "CreatePaymentMethodRequest": {
        "type": "object",
        "required": [
          "beneficiaryId",
          "paymentMethod",
          "bankingInformation"
        ],
        "additionalProperties": false,
        "properties": {
          "beneficiaryId": {
            "type": "string",
            "format": "uuid"
          },
          "paymentMethod": {
            "type": "string",
            "description": "A `paymentMethod` value, unchanged, from `GET /v1/beneficiary-schemas`.",
            "examples": [
              "local_bank_transfer"
            ]
          },
          "bankingInformation": {
            "type": "object",
            "description": "Keys equal the `fields[].fieldName` values from `GET /v1/beneficiary-schemas`; each\nvalue is checked against that field's `pattern`.\n",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "number"
                }
              ]
            }
          },
          "contactInformation": {
            "description": "Keys equal the `contactFields[].fieldName` values. Required when `contactFields`\nis not empty, unless the beneficiary already has the value stored. `phoneNumber`\ntakes the form `+<dialing code> <number>`, for example `+1 2125551234`; a\n`postcode` is at most 15 characters. Both rules are published as `pattern` in\n`contactFields`.\n",
            "allOf": [
              {
                "$ref": "#/components/schemas/ContactInformation"
              }
            ]
          }
        }
      },
      "PaymentMethodCreated": {
        "type": "object",
        "required": [
          "paymentMethodId",
          "status"
        ],
        "properties": {
          "paymentMethodId": {
            "type": "string",
            "format": "uuid",
            "description": "Send as `beneficiaryPaymentMethodId` when creating a payment."
          },
          "status": {
            "type": "string",
            "enum": [
              "active"
            ]
          }
        }
      },
      "PaymentCanceled": {
        "type": "object",
        "required": [
          "paymentId",
          "status",
          "canceledAt"
        ],
        "properties": {
          "paymentId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "canceled"
            ]
          },
          "canceledAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BeneficiaryListResponse": {
        "type": "object",
        "required": [
          "data",
          "count"
        ],
        "additionalProperties": false,
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Beneficiary"
            }
          },
          "count": {
            "type": "integer",
            "description": "Total beneficiaries matching the query, across all pages. Stop paging once\n`page × limit` reaches it. The request `page` and `limit` are not echoed back.\n",
            "examples": [
              42
            ]
          }
        }
      },
      "PaymentReceipt": {
        "type": "object",
        "required": [
          "receiptId",
          "paymentId",
          "fileName",
          "contentType",
          "sizeBytes",
          "createdAt"
        ],
        "properties": {
          "receiptId": {
            "type": "string",
            "format": "uuid"
          },
          "paymentId": {
            "type": "string",
            "format": "uuid"
          },
          "fileName": {
            "type": "string",
            "description": "The uploaded file name.",
            "examples": [
              "payment-receipt-8821.pdf"
            ]
          },
          "contentType": {
            "type": "string",
            "description": "Media type resolved from the file's content.",
            "examples": [
              "application/pdf"
            ]
          },
          "sizeBytes": {
            "type": "integer",
            "description": "Size in bytes, as received.",
            "examples": [
              184320
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateQuoteRequest": {
        "type": "object",
        "required": [
          "sourceCurrency",
          "destinationCurrency",
          "amount",
          "productAccountType"
        ],
        "additionalProperties": false,
        "properties": {
          "sourceCurrency": {
            "type": "string",
            "description": "The currency you pay from. One of `USD`, `MXN`, `BRL`, `COP`, `EUR`, `GBP`, `CAD`, or a\nstablecoin ticker: `USDB`, `USDC`, `USDT`, `EURC`. Any other code is rejected with\n`400 validation_failed`. Use the account's `productAccountCurrency` as returned by\n`GET /v1/product-accounts` — a `crypto-wallet` account is quoted in its ticker, for\nexample `USDC`.\n",
            "examples": [
              "USD",
              "USDC"
            ]
          },
          "destinationCurrency": {
            "type": "string",
            "description": "The currency the beneficiary receives. One of `USD`, `MXN`, `BRL`, `COP`, `EUR`, `GBP`,\n`CAD`, or a stablecoin ticker: `USDB`, `USDC`, `USDT`, `EURC`. Any other code is\nrejected with `400 validation_failed`.\n",
            "examples": [
              "MXN"
            ]
          },
          "amount": {
            "type": "string",
            "pattern": "^(?!0+(\\.0+)?$)\\d{1,12}(\\.\\d{1,2})?$",
            "description": "**Destination** amount as a decimal string — what the beneficiary receives, in\n`destinationCurrency`. The source amount is computed from the rate.\n\nGreater than zero, at most 2 decimal places, at most 12 integer digits. Anything\nelse returns `400 validation_failed`.\n",
            "examples": [
              "43035.00"
            ]
          },
          "productAccountType": {
            "$ref": "#/components/schemas/AccountType"
          }
        }
      },
      "Quote": {
        "type": "object",
        "required": [
          "quoteId",
          "rate",
          "sourceCurrency",
          "sourceAmount",
          "destinationAmount",
          "destinationCurrency",
          "createdAt",
          "expiresAt"
        ],
        "properties": {
          "quoteId": {
            "type": "string",
            "format": "uuid"
          },
          "rate": {
            "type": "string",
            "description": "Decimal string. Destination units per one source unit.",
            "examples": [
              "17.2140"
            ]
          },
          "sourceCurrency": {
            "type": "string",
            "description": "The currency you pay from, as sent in the request — one of the supported currencies\n(`USD`, `MXN`, `BRL`, `COP`, `EUR`, `GBP`, `CAD`) or a stablecoin ticker (`USDB`,\n`USDC`, `USDT`, `EURC`).\n",
            "examples": [
              "USD",
              "USDC"
            ]
          },
          "sourceAmount": {
            "type": "string",
            "description": "Decimal string. What you will be debited, before fees.",
            "examples": [
              "2500.00"
            ]
          },
          "destinationAmount": {
            "type": "string",
            "description": "Decimal string. What the beneficiary receives.",
            "examples": [
              "43035.00"
            ]
          },
          "destinationCurrency": {
            "type": "string",
            "description": "The currency the beneficiary receives, as sent in the request — one of the supported\ncurrencies (`USD`, `MXN`, `BRL`, `COP`, `EUR`, `GBP`, `CAD`) or a stablecoin ticker\n(`USDB`, `USDC`, `USDT`, `EURC`).\n",
            "examples": [
              "MXN"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the locked rate stops being valid. **Read this rather than assuming a window** —\nthe TTL is configured per company. `null` means the quote does not expire.\n"
          }
        }
      },
      "CreatePaymentRequest": {
        "type": "object",
        "required": [
          "productAccountId",
          "beneficiaryId",
          "beneficiaryPaymentMethodId",
          "purposeCode",
          "reference"
        ],
        "additionalProperties": false,
        "properties": {
          "quoteId": {
            "type": "string",
            "format": "uuid",
            "description": "An unconsumed quote. Required unless `scheduledDate` is set, in which case it must\nbe omitted.\n"
          },
          "onQuoteExpiry": {
            "type": "string",
            "enum": [
              "fail",
              "requote"
            ],
            "default": "fail",
            "description": "What to do if the quote expired before this request arrived.\n\n- `fail` — return `422 quote_expired`.\n- `requote` — price a new quote at the current rate and use it. The destination\n  amount is unchanged; `sourceAmount` moves. Check `quoteId` against\n  `originalQuoteId` in the response to detect this.\n\nDoes not apply to scheduled payments.\n"
          },
          "productAccountId": {
            "type": "string",
            "format": "uuid",
            "description": "The account to debit, from `GET /v1/product-accounts`. Its\n`productAccountCurrency` must match the quote's `sourceCurrency`.\n"
          },
          "beneficiaryId": {
            "type": "string",
            "format": "uuid"
          },
          "beneficiaryPaymentMethodId": {
            "type": "string",
            "format": "uuid",
            "description": "A `paymentMethodId` attached to that beneficiary."
          },
          "purposeCode": {
            "type": "string",
            "description": "**Mandatory on every payment.** A purpose code is a **string identifier**\nnaming why the payment is made (e.g. `purchase_of_services`) — never a number.\nWhich identifiers are valid depends on the corridor — the currency you pay\nfrom, the currency and country you pay to, and the kind of account funding\nthe payment.\n\nRead the exact list for your corridor from `purposeCodes[]` in the response of\n`GET /v1/beneficiary-schemas` and send one of its `code` values.\n\nNo default is ever applied. Omitting it returns `400`; sending an identifier\noutside the corridor's list returns `422 validation_failed`.\n",
            "examples": [
              "purchase_of_services"
            ]
          },
          "reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 140,
            "description": "Required on every payment. 1–140 characters; an empty string is rejected exactly as\nomitting it is. Shown to the recipient where the payment method supports it.\n",
            "examples": [
              "Invoice 2026-0912"
            ]
          },
          "scheduledDate": {
            "type": "string",
            "format": "date",
            "description": "Future date. The payment enters `scheduled` and the rate is locked on the execution\ndate. Mutually exclusive with `quoteId`.\n"
          },
          "amount": {
            "type": "string",
            "pattern": "^(?!0+(\\.0+)?$)\\d{1,12}(\\.\\d{1,2})?$",
            "description": "Destination amount as a decimal string. Required for scheduled payments, which are\nquoted on the execution date.\n\nGreater than zero, at most 2 decimal places, at most 12 integer digits. Anything\nelse returns `400 validation_failed`.\n"
          }
        }
      },
      "PaymentCreated": {
        "type": "object",
        "required": [
          "paymentId",
          "status",
          "quoteId",
          "rate",
          "sourceAmount",
          "destinationAmount",
          "createdAt"
        ],
        "properties": {
          "paymentId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "quoteId": {
            "type": "string",
            "format": "uuid",
            "description": "The quote that actually priced this payment."
          },
          "originalQuoteId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The expired quote you sent, when `onQuoteExpiry: requote` caused a new one to be\ncreated. `null` when no requote happened.\n"
          },
          "rate": {
            "type": "string",
            "description": "Decimal string. Matches the quote named by `quoteId`."
          },
          "sourceAmount": {
            "type": "string",
            "description": "Decimal string."
          },
          "destinationAmount": {
            "type": "string",
            "description": "Decimal string."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Payment": {
        "type": "object",
        "required": [
          "paymentId",
          "status",
          "quoteId",
          "rate",
          "sourceAmount",
          "destinationAmount",
          "productAccountId",
          "beneficiaryId",
          "beneficiaryPaymentMethodId",
          "purposeCode",
          "createdAt"
        ],
        "properties": {
          "paymentId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "quoteId": {
            "type": "string",
            "format": "uuid",
            "description": "The quote that actually priced this payment."
          },
          "originalQuoteId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The expired quote replaced by a requote, or `null`."
          },
          "rate": {
            "type": "string",
            "description": "Decimal string."
          },
          "sourceAmount": {
            "type": "string",
            "description": "Decimal string."
          },
          "destinationAmount": {
            "type": "string",
            "description": "Decimal string."
          },
          "productAccountId": {
            "type": "string",
            "format": "uuid"
          },
          "beneficiaryId": {
            "type": "string",
            "format": "uuid"
          },
          "beneficiaryPaymentMethodId": {
            "type": "string",
            "format": "uuid"
          },
          "purposeCode": {
            "type": "string"
          },
          "reference": {
            "type": [
              "string",
              "null"
            ]
          },
          "scheduledDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Set for future-dated payments, otherwise `null`."
          },
          "failureReason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Populated for `rejected`, `failed` and `canceled`. `null` otherwise.\n"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PaymentStatusEvent": {
        "type": "object",
        "required": [
          "eventId",
          "eventType",
          "occurredAt",
          "data"
        ],
        "properties": {
          "eventId": {
            "type": "string",
            "description": "Unique per event. **Deduplicate on this** — delivery is at-least-once.\n",
            "examples": [
              "evt_c91d20"
            ]
          },
          "eventType": {
            "type": "string",
            "enum": [
              "payment.status.updated"
            ],
            "description": "Treat as open — new event types are added additively."
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the status change happened. Order of delivery is not guaranteed — reconcile on\nthis, not on arrival order.\n"
          },
          "data": {
            "type": "object",
            "required": [
              "paymentId",
              "status"
            ],
            "properties": {
              "paymentId": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "$ref": "#/components/schemas/PaymentWebhookStatus"
              },
              "failureReason": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Set for `rejected`, `failed` and `canceled`; `null` otherwise."
              }
            }
          }
        }
      }
    }
  }
}