{
  "openapi": "3.1.0",
  "info": {
    "title": "FastPay client API",
    "version": "2026-10-28",
    "description": "Client integration reference for the FastPay API. Only Solana USDC client-signed payroll has production pilot traffic; client activation requires its own approvals."
  },
  "servers": [
    {
      "url": "https://relay.f-pay.com",
      "description": "Production. Live and sandbox requests use the same host; the key prefix (fp_live_ / fp_test_) selects the mode."
    }
  ],
  "paths": {
    "/v1/members/me": {
      "get": {
        "operationId": "getCurrentMember",
        "tags": [
          "Tenants and team"
        ],
        "summary": "The team member the calling API key is issued to",
        "description": "Returns the member the calling key is bound to (req.member, set at authentication), read again from the store. A key issued to no member (a legacy or memberless key) answers 404 resource_missing \"This API key is not issued to a team member.\". Read-only: no audit row, no webhook. Sandbox vs live: members belong to the tenant, not to a mode; a test key and a live key of the same member read the same object (takes no mode). Scope team:read.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The member object.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                },
                "examples": {
                  "TenantsTeamMemberOwner": {
                    "$ref": "#/components/examples/TenantsTeamMemberOwner"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- `invalid_json` (Shape B): A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header.\n- `invalid_request` (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "- `missing_api_key` (Shape A): No Authorization header, or not \"Bearer <key>\"; error_type authentication_error.\n- `invalid_api_key` (Shape A): An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first.\n- `api_key_expired` (Shape A): The key is past its expires_at.\n- `member_suspended` (Shape A): The member the key is issued to is suspended, or its member row is missing.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorMissingApiKey": {
                    "$ref": "#/components/examples/TenantsTeamErrorMissingApiKey"
                  }
                }
              }
            }
          },
          "403": {
            "description": "- `sandbox_host_requires_test_key` (Shape A): A live key on a sandbox-only host.\n- `ip_not_allowed` (Shape A): The key's IP allow-list excludes the caller.\n- `insufficient_scope` (Shape A): The key, narrowed to its member's role ceiling, lacks team:read.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInsufficientScope": {
                    "$ref": "#/components/examples/TenantsTeamErrorInsufficientScope"
                  }
                }
              }
            }
          },
          "404": {
            "description": "- `resource_missing` (Shape B): The key is issued to no member, a memberless key. The second lookup is defensive only: a key whose member row is missing is refused at authentication with 401 member_suspended, and no route deletes a member.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorKeyHasNoMember": {
                    "$ref": "#/components/examples/TenantsTeamErrorKeyHasNoMember"
                  }
                }
              }
            }
          },
          "413": {
            "description": "- `request_body_too_large` (Shape B): A body over the request body limit.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "- `invalid_request` (Shape B): A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header (body-parser 1.20.6).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "- `internal_error` (Shape B): An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInternal": {
                    "$ref": "#/components/examples/TenantsTeamErrorInternal"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is missing."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks team:read."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "The key is issued to no member, a memberless key. The second lookup is defensive only: a key whose member row is missing is refused at authentication with 401 member_suspended, and no route deletes a member."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "team:read"
      }
    },
    "/v1/members": {
      "post": {
        "operationId": "createMember",
        "tags": [
          "Tenants and team"
        ],
        "summary": "Create a team member in the calling key's tenant",
        "description": "Creates a member: the person API keys are issued to. Order: authentication, scope team:write and the tenant Idempotency-Key layer; the capability gate canManageTeam: a key of an owner or admin member passes, a key of a developer member is 403 capability_required (an approver's or a viewer's key never gets here: its role ceiling holds no write scope, so team:write is refused first with 403 insufficient_scope), a key of no member is 403 member_required, except while the tenant has no member at all, suspended ones included, when it acts as an owner (bootstrap); then the body, checked name, email, role, status; then the owner rule: only a caller with canAssignOwner (an owner member, or a bootstrap key) may create an owner, else 403 owner_role_required; then the insert, 400 email_in_use for an email already in the tenant. Effects: one member row, status active; then, as a separate statement, the audit row member.created: actor_type tenant, actor_id the tenant, actor_member_id and actor_api_key_id the caller's, ip; target_type member, target_id the new id; details {name, email, role, bootstrap}. No webhook. The member holds no key yet. Sandbox vs live: no mode check; a test (fp_test_) key creates a real member of the tenant, the same for live keys. Idempotency: the tenant layer stores every answer, 4xx and 5xx included, for 24 h, bound to the calling key, and replays it with X-Cache-Lookup: HIT. Stored answers are sealed with a key derived from the calling raw API key, which the relay does not store, so only that key opens them. Without a key a resend is naturally safe: the second create is 400 email_in_use.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MemberCreateRequest"
              },
              "examples": {
                "TenantsTeamMemberCreateMaker": {
                  "$ref": "#/components/examples/TenantsTeamMemberCreateMaker"
                },
                "TenantsTeamMemberCreateApprover": {
                  "$ref": "#/components/examples/TenantsTeamMemberCreateApprover"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new member.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                },
                "examples": {
                  "TenantsTeamMemberOwner": {
                    "$ref": "#/components/examples/TenantsTeamMemberOwner"
                  },
                  "TenantsTeamMemberApprover": {
                    "$ref": "#/components/examples/TenantsTeamMemberApprover"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- `invalid_json` (Shape B): A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header.\n- `invalid_name` (Shape B): name missing, not a string, empty after trim, or over 200 characters after trim.\n- `invalid_email` (Shape B): email missing, not a string, or not of the form x@y.z (at most 254 characters) after trim and lower-casing.\n- `invalid_role` (Shape B): role missing or not one of owner, admin, developer, approver, viewer after trim and lower-casing.\n- `invalid_status` (Shape B): A status field is present (any value, null included): a new member is always active.\n- `email_in_use` (Shape B): This tenant already has a member, active or suspended, with this (lower-cased) email.\n- `invalid_request` (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInvalidName": {
                    "$ref": "#/components/examples/TenantsTeamErrorInvalidName"
                  },
                  "TenantsTeamErrorEmailInUse": {
                    "$ref": "#/components/examples/TenantsTeamErrorEmailInUse"
                  }
                }
              }
            }
          },
          "401": {
            "description": "- `missing_api_key` (Shape A): No Authorization header, or not \"Bearer <key>\"; error_type authentication_error.\n- `invalid_api_key` (Shape A): An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first.\n- `api_key_expired` (Shape A): The key is past its expires_at.\n- `member_suspended` (Shape A): The member the key is issued to is suspended, or its member row is missing.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorMissingApiKey": {
                    "$ref": "#/components/examples/TenantsTeamErrorMissingApiKey"
                  }
                }
              }
            }
          },
          "403": {
            "description": "- `sandbox_host_requires_test_key` (Shape A): A live key on a sandbox-only host.\n- `ip_not_allowed` (Shape A): The key's IP allow-list excludes the caller.\n- `insufficient_scope` (Shape A): The key, narrowed to its member's role ceiling, lacks team:write.\n- `member_required` (Shape B): A key issued to no member, on a tenant that already has a member.\n- `capability_required` (Shape B): The caller's member is a developer. An approver's or viewer's key is refused earlier with insufficient_scope: its role ceiling holds no write scope.\n- `owner_role_required` (Shape B): role owner asked by a caller that is not an owner (an admin member).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInsufficientScope": {
                    "$ref": "#/components/examples/TenantsTeamErrorInsufficientScope"
                  },
                  "TenantsTeamErrorMemberRequired": {
                    "$ref": "#/components/examples/TenantsTeamErrorMemberRequired"
                  },
                  "TenantsTeamErrorCapabilityRequired": {
                    "$ref": "#/components/examples/TenantsTeamErrorCapabilityRequired"
                  },
                  "TenantsTeamErrorOwnerRoleRequired": {
                    "$ref": "#/components/examples/TenantsTeamErrorOwnerRoleRequired"
                  }
                }
              }
            }
          },
          "409": {
            "description": "- `idempotency_key_in_use` (Shape B): The Idempotency-Key was claimed by another API key of this tenant. Since the first 2026-10-28 deploy (10:46 UTC), also a repeat from the same API key whose stored answer does not open with that key: an answer stored unsealed before that deploy, until it expires 24 h after its first call. Its own message says the first request already ran; the request is not run again, so read the result back before resending under a new key.\n- `idempotency_parameter_mismatch` (Shape B): The same key, mode and path with another request hash.\n- `idempotency_key_in_progress` (Shape B): The first call under this key has not answered yet; after a relay restart mid-call it keeps answering this until the 24 h retention ends.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorIdempotencyMismatch": {
                    "$ref": "#/components/examples/TenantsTeamErrorIdempotencyMismatch"
                  },
                  "TenantsTeamErrorIdempotencyUnopened": {
                    "$ref": "#/components/examples/TenantsTeamErrorIdempotencyUnopened"
                  }
                }
              }
            }
          },
          "413": {
            "description": "- `request_body_too_large` (Shape B): A body over the request body limit.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "- `invalid_request` (Shape B): A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header (body-parser 1.20.6).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "- `internal_error` (Shape B): An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInternal": {
                    "$ref": "#/components/examples/TenantsTeamErrorInternal"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is missing."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks team:write."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was claimed by another API key of this tenant. Since the first 2026-10-28 deploy (10:46 UTC), also a repeat from the same API key whose stored answer does not open with that key: an answer stored unsealed before that deploy, until it expires 24 h after its first call. Its own message says the first request already ran; the request is not run again, so read the result back before resending under a new key."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same key, mode and path with another request hash."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first call under this key has not answered yet; after a relay restart mid-call it keeps answering this until the 24 h retention ends."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set."
          },
          {
            "status": 403,
            "code": "member_required",
            "envelope": "B",
            "when": "A key issued to no member, on a tenant that already has a member."
          },
          {
            "status": 403,
            "code": "capability_required",
            "envelope": "B",
            "when": "The caller's member is a developer. An approver's or viewer's key is refused earlier with insufficient_scope: its role ceiling holds no write scope."
          },
          {
            "status": 403,
            "code": "owner_role_required",
            "envelope": "B",
            "when": "role owner asked by a caller that is not an owner (an admin member)."
          },
          {
            "status": 400,
            "code": "invalid_name",
            "envelope": "B",
            "when": "name missing, not a string, empty after trim, or over 200 characters after trim."
          },
          {
            "status": 400,
            "code": "invalid_email",
            "envelope": "B",
            "when": "email missing, not a string, or not of the form x@y.z (at most 254 characters) after trim and lower-casing."
          },
          {
            "status": 400,
            "code": "invalid_role",
            "envelope": "B",
            "when": "role missing or not one of owner, admin, developer, approver, viewer after trim and lower-casing."
          },
          {
            "status": 400,
            "code": "invalid_status",
            "envelope": "B",
            "when": "A status field is present (any value, null included): a new member is always active."
          },
          {
            "status": 400,
            "code": "email_in_use",
            "envelope": "B",
            "when": "This tenant already has a member, active or suspended, with this (lower-cased) email."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "team:write"
      },
      "get": {
        "operationId": "listMembers",
        "tags": [
          "Tenants and team"
        ],
        "summary": "List the calling key's tenant's members",
        "description": "One page of the tenant's members, newest first by insertion order. Default page 20, at most 100. Filters: status, role, created[gte]/created[lte]. Every member of the tenant, whatever the key's mode. Read-only: no audit row.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "$ref": "#/components/parameters/CreatedGte"
          },
          {
            "$ref": "#/components/parameters/CreatedLte"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only members in this status. Exact, single value; an unknown or repeated value is 400 invalid_filter_parameter.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "suspended"
              ]
            }
          },
          {
            "name": "role",
            "in": "query",
            "required": false,
            "description": "Only members with this role. Exact, lower case, single value.",
            "schema": {
              "type": "string",
              "enum": [
                "owner",
                "admin",
                "developer",
                "approver",
                "viewer"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A list of member objects.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberList"
                },
                "examples": {
                  "TenantsTeamMemberList": {
                    "$ref": "#/components/examples/TenantsTeamMemberList"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- `invalid_json` (Shape B): A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header.\n- `invalid_pagination_parameter` (Shape B): limit not an integer from 1 to 100, or starting_after with ending_before.\n- `invalid_filter_parameter` (Shape B): An unknown or repeated status/role value, an unparseable created[gte]/created[lte], or gte later than lte.\n- `invalid_request` (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInvalidPagination": {
                    "$ref": "#/components/examples/TenantsTeamErrorInvalidPagination"
                  }
                }
              }
            }
          },
          "401": {
            "description": "- `missing_api_key` (Shape A): No Authorization header, or not \"Bearer <key>\"; error_type authentication_error.\n- `invalid_api_key` (Shape A): An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first.\n- `api_key_expired` (Shape A): The key is past its expires_at.\n- `member_suspended` (Shape A): The member the key is issued to is suspended, or its member row is missing.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorMissingApiKey": {
                    "$ref": "#/components/examples/TenantsTeamErrorMissingApiKey"
                  }
                }
              }
            }
          },
          "403": {
            "description": "- `sandbox_host_requires_test_key` (Shape A): A live key on a sandbox-only host.\n- `ip_not_allowed` (Shape A): The key's IP allow-list excludes the caller.\n- `insufficient_scope` (Shape A): The key, narrowed to its member's role ceiling, lacks team:read.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInsufficientScope": {
                    "$ref": "#/components/examples/TenantsTeamErrorInsufficientScope"
                  }
                }
              }
            }
          },
          "413": {
            "description": "- `request_body_too_large` (Shape B): A body over the request body limit.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "- `invalid_request` (Shape B): A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header (body-parser 1.20.6).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "- `internal_error` (Shape B): An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInternal": {
                    "$ref": "#/components/examples/TenantsTeamErrorInternal"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is missing."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks team:read."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set."
          },
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B",
            "when": "limit not an integer from 1 to 100, or starting_after with ending_before."
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B",
            "when": "An unknown or repeated status/role value, an unparseable created[gte]/created[lte], or gte later than lte."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "team:read"
      }
    },
    "/v1/members/{id}": {
      "get": {
        "operationId": "getMember",
        "tags": [
          "Tenants and team"
        ],
        "summary": "Retrieve one team member",
        "description": "One member of the calling key's tenant. An id of another tenant, or unknown, is 404 resource_missing \"No such member.\". Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A member id of the calling key's tenant (mem_ + 32 hex). A foreign or unknown id is 404 resource_missing.",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "mem_00000000000000000000000000000001"
          }
        ],
        "responses": {
          "200": {
            "description": "The member object.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                },
                "examples": {
                  "TenantsTeamMemberApprover": {
                    "$ref": "#/components/examples/TenantsTeamMemberApprover"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- `invalid_json` (Shape B): A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header.\n- `invalid_request` (Shape B): A path parameter that is not valid percent-encoding.\n- `invalid_request` (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "- `missing_api_key` (Shape A): No Authorization header, or not \"Bearer <key>\"; error_type authentication_error.\n- `invalid_api_key` (Shape A): An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first.\n- `api_key_expired` (Shape A): The key is past its expires_at.\n- `member_suspended` (Shape A): The member the key is issued to is suspended, or its member row is missing.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorMissingApiKey": {
                    "$ref": "#/components/examples/TenantsTeamErrorMissingApiKey"
                  }
                }
              }
            }
          },
          "403": {
            "description": "- `sandbox_host_requires_test_key` (Shape A): A live key on a sandbox-only host.\n- `ip_not_allowed` (Shape A): The key's IP allow-list excludes the caller.\n- `insufficient_scope` (Shape A): The key, narrowed to its member's role ceiling, lacks team:read.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInsufficientScope": {
                    "$ref": "#/components/examples/TenantsTeamErrorInsufficientScope"
                  }
                }
              }
            }
          },
          "404": {
            "description": "- `resource_missing` (Shape B): No such member in this tenant.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorMemberMissing": {
                    "$ref": "#/components/examples/TenantsTeamErrorMemberMissing"
                  }
                }
              }
            }
          },
          "413": {
            "description": "- `request_body_too_large` (Shape B): A body over the request body limit.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "- `invalid_request` (Shape B): A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header (body-parser 1.20.6).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "- `internal_error` (Shape B): An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInternal": {
                    "$ref": "#/components/examples/TenantsTeamErrorInternal"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A path parameter that is not valid percent-encoding. Also: The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is missing."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks team:read."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No such member in this tenant."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "team:read"
      },
      "post": {
        "operationId": "updateMember",
        "tags": [
          "Tenants and team"
        ],
        "summary": "Change a member's name, role or status",
        "description": "Changes the fields sent, and only those. The email can never change. Order: authentication, scope team:write, the tenant Idempotency-Key layer; the capability gate canManageTeam, bootstrap included (an approver's or viewer's key stops earlier, at 403 insufficient_scope); the target lookup, 404 resource_missing before any body check; the body; the owner rule: touching a member whose role is owner (a name change included) or granting owner needs canAssignOwner, which only an owner member (or a bootstrap key) holds, else 403 owner_role_required; the last-owner guard: demoting or suspending the only active owner is 409 last_owner. Effects: the fields and updated_at, only when a field was sent; then always the audit row member.updated, target_type member, details {patch, before {role, status}, after {role, status}}, an empty body included. No webhook. No key is revoked: a suspended member's keys answer 401 member_suspended from their next request and a reactivation revives every key not revoked or expired; a role change takes effect on each key's next request: the key exercises its minted scopes cut to the new role's ceiling, a scope outside it dropped rather than downgraded (a maker's payroll:write-only key keeps no scope at all after a demotion to approver), and the new role's capabilities, which a promotion widens; a key minted under an earlier, wider role regains its minted scopes when the member is promoted back. Revoke the old keys before the role write. Sandbox vs live: no mode check; a test key changes the tenant's live team. Idempotency: the tenant layer stores and replays this write for 24 h, unlike its admin twin, which is never replayed (against ADMIN_NOT_REPLAYED). Stored answers are sealed with a key derived from the calling raw API key, which the relay does not store, so only that key opens them. Read the member back rather than resending under the same key.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A member id of the calling key's tenant (mem_ + 32 hex). A foreign or unknown id is 404 resource_missing.",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "mem_00000000000000000000000000000001"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MemberUpdateRequest"
              },
              "examples": {
                "TenantsTeamMemberUpdateSuspend": {
                  "$ref": "#/components/examples/TenantsTeamMemberUpdateSuspend"
                },
                "TenantsTeamMemberUpdateRole": {
                  "$ref": "#/components/examples/TenantsTeamMemberUpdateRole"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The member after the change.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                },
                "examples": {
                  "TenantsTeamMemberSuspended": {
                    "$ref": "#/components/examples/TenantsTeamMemberSuspended"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- `invalid_json` (Shape B): A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header.\n- `invalid_request` (Shape B): A path parameter that is not valid percent-encoding.\n- `invalid_name` (Shape B): name sent but not a string, null, empty after trim, or over 200 characters.\n- `invalid_email` (Shape B): email sent, with any value: \"email cannot be changed - create a new member instead.\".\n- `invalid_role` (Shape B): role sent but not one of the five roles after trim and lower-casing, or null.\n- `invalid_status` (Shape B): status sent but not exactly active or suspended (case-sensitive, not trimmed; null refused).\n- `invalid_request` (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInvalidName": {
                    "$ref": "#/components/examples/TenantsTeamErrorInvalidName"
                  }
                }
              }
            }
          },
          "401": {
            "description": "- `missing_api_key` (Shape A): No Authorization header, or not \"Bearer <key>\"; error_type authentication_error.\n- `invalid_api_key` (Shape A): An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first.\n- `api_key_expired` (Shape A): The key is past its expires_at.\n- `member_suspended` (Shape A): The member the key is issued to is suspended, or its member row is missing.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorMissingApiKey": {
                    "$ref": "#/components/examples/TenantsTeamErrorMissingApiKey"
                  }
                }
              }
            }
          },
          "403": {
            "description": "- `sandbox_host_requires_test_key` (Shape A): A live key on a sandbox-only host.\n- `ip_not_allowed` (Shape A): The key's IP allow-list excludes the caller.\n- `insufficient_scope` (Shape A): The key, narrowed to its member's role ceiling, lacks team:write.\n- `member_required` (Shape B): A key issued to no member, on a tenant that has a member.\n- `capability_required` (Shape B): The caller's member is a developer. An approver's or viewer's key is refused earlier with insufficient_scope: its role ceiling holds no write scope.\n- `owner_role_required` (Shape B): An admin member changing an owner, or granting owner.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInsufficientScope": {
                    "$ref": "#/components/examples/TenantsTeamErrorInsufficientScope"
                  },
                  "TenantsTeamErrorMemberRequired": {
                    "$ref": "#/components/examples/TenantsTeamErrorMemberRequired"
                  },
                  "TenantsTeamErrorCapabilityRequired": {
                    "$ref": "#/components/examples/TenantsTeamErrorCapabilityRequired"
                  },
                  "TenantsTeamErrorOwnerRoleRequired": {
                    "$ref": "#/components/examples/TenantsTeamErrorOwnerRoleRequired"
                  }
                }
              }
            }
          },
          "404": {
            "description": "- `resource_missing` (Shape B): No such member in this tenant.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorMemberMissing": {
                    "$ref": "#/components/examples/TenantsTeamErrorMemberMissing"
                  }
                }
              }
            }
          },
          "409": {
            "description": "- `idempotency_key_in_use` (Shape B): The Idempotency-Key was claimed by another API key of this tenant. Since the first 2026-10-28 deploy (10:46 UTC), also a repeat from the same API key whose stored answer does not open with that key: an answer stored unsealed before that deploy, until it expires 24 h after its first call. Its own message says the first request already ran; the request is not run again, so read the result back before resending under a new key.\n- `idempotency_parameter_mismatch` (Shape B): The same key, mode and path with another request hash.\n- `idempotency_key_in_progress` (Shape B): The first call under this key has not answered yet; after a relay restart mid-call it keeps answering this until the 24 h retention ends.\n- `last_owner` (Shape B): The target is the tenant's only active owner and the patch demotes or suspends it.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorIdempotencyMismatch": {
                    "$ref": "#/components/examples/TenantsTeamErrorIdempotencyMismatch"
                  },
                  "TenantsTeamErrorLastOwner": {
                    "$ref": "#/components/examples/TenantsTeamErrorLastOwner"
                  },
                  "TenantsTeamErrorIdempotencyUnopened": {
                    "$ref": "#/components/examples/TenantsTeamErrorIdempotencyUnopened"
                  }
                }
              }
            }
          },
          "413": {
            "description": "- `request_body_too_large` (Shape B): A body over the request body limit.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "- `invalid_request` (Shape B): A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header (body-parser 1.20.6).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "- `internal_error` (Shape B): An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInternal": {
                    "$ref": "#/components/examples/TenantsTeamErrorInternal"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A path parameter that is not valid percent-encoding. Also: The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is missing."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks team:write."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was claimed by another API key of this tenant. Since the first 2026-10-28 deploy (10:46 UTC), also a repeat from the same API key whose stored answer does not open with that key: an answer stored unsealed before that deploy, until it expires 24 h after its first call. Its own message says the first request already ran; the request is not run again, so read the result back before resending under a new key."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same key, mode and path with another request hash."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first call under this key has not answered yet; after a relay restart mid-call it keeps answering this until the 24 h retention ends."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set."
          },
          {
            "status": 403,
            "code": "member_required",
            "envelope": "B",
            "when": "A key issued to no member, on a tenant that has a member."
          },
          {
            "status": 403,
            "code": "capability_required",
            "envelope": "B",
            "when": "The caller's member is a developer. An approver's or viewer's key is refused earlier with insufficient_scope: its role ceiling holds no write scope."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No such member in this tenant."
          },
          {
            "status": 400,
            "code": "invalid_name",
            "envelope": "B",
            "when": "name sent but not a string, null, empty after trim, or over 200 characters."
          },
          {
            "status": 400,
            "code": "invalid_email",
            "envelope": "B",
            "when": "email sent, with any value: \"email cannot be changed - create a new member instead.\"."
          },
          {
            "status": 400,
            "code": "invalid_role",
            "envelope": "B",
            "when": "role sent but not one of the five roles after trim and lower-casing, or null."
          },
          {
            "status": 400,
            "code": "invalid_status",
            "envelope": "B",
            "when": "status sent but not exactly active or suspended (case-sensitive, not trimmed; null refused)."
          },
          {
            "status": 403,
            "code": "owner_role_required",
            "envelope": "B",
            "when": "An admin member changing an owner, or granting owner."
          },
          {
            "status": 409,
            "code": "last_owner",
            "envelope": "B",
            "when": "The target is the tenant's only active owner and the patch demotes or suspends it."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "team:write"
      }
    },
    "/v1/payout-approval-policy": {
      "get": {
        "operationId": "getPayoutApprovalPolicy",
        "tags": [
          "Tenants and team"
        ],
        "summary": "Read the tenant's payout approval policy",
        "description": "The tenant's one policy. Never written: mode never, approvals_required 1, thresholds {}. An unreadable stored value reads the same default: it fails open. The policy decides, when a client-signed payroll batch is created, whether it starts awaiting approval and how many distinct approve-capable members other than its creator must approve it; the number is copied onto the batch, so a later change never alters one already created. always: everything waits; above: a currency whose summed amount reaches its threshold, or a currency with no threshold, waits. Tenant-wide: sandbox and live, every network. Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The policy.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutApprovalPolicy"
                },
                "examples": {
                  "TenantsTeamPolicyDefault": {
                    "$ref": "#/components/examples/TenantsTeamPolicyDefault"
                  },
                  "TenantsTeamPolicyAlwaysOne": {
                    "$ref": "#/components/examples/TenantsTeamPolicyAlwaysOne"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- `invalid_json` (Shape B): A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header.\n- `invalid_request` (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "- `missing_api_key` (Shape A): No Authorization header, or not \"Bearer <key>\"; error_type authentication_error.\n- `invalid_api_key` (Shape A): An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first.\n- `api_key_expired` (Shape A): The key is past its expires_at.\n- `member_suspended` (Shape A): The member the key is issued to is suspended, or its member row is missing.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorMissingApiKey": {
                    "$ref": "#/components/examples/TenantsTeamErrorMissingApiKey"
                  }
                }
              }
            }
          },
          "403": {
            "description": "- `sandbox_host_requires_test_key` (Shape A): A live key on a sandbox-only host.\n- `ip_not_allowed` (Shape A): The key's IP allow-list excludes the caller.\n- `insufficient_scope` (Shape A): The key, narrowed to its member's role ceiling, lacks team:read.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInsufficientScope": {
                    "$ref": "#/components/examples/TenantsTeamErrorInsufficientScope"
                  }
                }
              }
            }
          },
          "413": {
            "description": "- `request_body_too_large` (Shape B): A body over the request body limit.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "- `invalid_request` (Shape B): A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header (body-parser 1.20.6).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "- `internal_error` (Shape B): An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInternal": {
                    "$ref": "#/components/examples/TenantsTeamErrorInternal"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is missing."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks team:read."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "team:read"
      },
      "post": {
        "operationId": "setPayoutApprovalPolicy",
        "tags": [
          "Tenants and team"
        ],
        "summary": "Replace the tenant's payout approval policy",
        "description": "Replaces the whole policy: an omitted approvals_required is 1 and omitted thresholds are {}. Order: authentication, scope team:write, the tenant Idempotency-Key layer; the capability gate canSetApprovalPolicy, which only the owner role holds, or a memberless key while the tenant has no member (bootstrap): an admin or developer member is 403 capability_required (an approver's or viewer's key stops earlier at 403 insufficient_scope, its role ceiling holding no write scope), a memberless key 403 member_required; then the body. Effects: the stored policy; the audit row payout_approval_policy.updated {before, after}, target_type tenant, written even when nothing changed. No webhook. The policy decides, when a client-signed payroll batch is created, whether it starts awaiting approval and how many distinct approve-capable members other than its creator must approve it; the number is copied onto the batch, so a later change never alters one already created. always: everything waits; above: a currency whose summed amount reaches its threshold, or a currency with no threshold, waits. Sandbox vs live: no mode check; a test key sets the policy live batches read. Idempotency: the tenant layer stores and replays it for 24 h (the admin twin is never replayed). Stored answers are sealed with a key derived from the calling raw API key, which the relay does not store, so only that key opens them.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutApprovalPolicyRequest"
              },
              "examples": {
                "TenantsTeamPolicyRequestAlwaysOne": {
                  "$ref": "#/components/examples/TenantsTeamPolicyRequestAlwaysOne"
                },
                "TenantsTeamPolicyRequestNever": {
                  "$ref": "#/components/examples/TenantsTeamPolicyRequestNever"
                },
                "TenantsTeamPolicyRequestAbove": {
                  "$ref": "#/components/examples/TenantsTeamPolicyRequestAbove"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The policy as stored.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutApprovalPolicy"
                },
                "examples": {
                  "TenantsTeamPolicyAlwaysOne": {
                    "$ref": "#/components/examples/TenantsTeamPolicyAlwaysOne"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- `invalid_json` (Shape B): A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header.\n- `invalid_policy_mode` (Shape B): mode missing, not a string, or not never/always/above after trim and lower-casing.\n- `invalid_approvals_required` (Shape B): approvals_required present and not null, and Number of it is not an integer from 1 to 3.\n- `invalid_thresholds` (Shape B): thresholds not an object (an array included); more than 50 entries; a key not matching [A-Z0-9.]{2,20} after trim and upper-casing; a value not a plain non-negative decimal; or mode above with no threshold.\n- `invalid_request` (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInvalidPolicyMode": {
                    "$ref": "#/components/examples/TenantsTeamErrorInvalidPolicyMode"
                  }
                }
              }
            }
          },
          "401": {
            "description": "- `missing_api_key` (Shape A): No Authorization header, or not \"Bearer <key>\"; error_type authentication_error.\n- `invalid_api_key` (Shape A): An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first.\n- `api_key_expired` (Shape A): The key is past its expires_at.\n- `member_suspended` (Shape A): The member the key is issued to is suspended, or its member row is missing.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorMissingApiKey": {
                    "$ref": "#/components/examples/TenantsTeamErrorMissingApiKey"
                  }
                }
              }
            }
          },
          "403": {
            "description": "- `sandbox_host_requires_test_key` (Shape A): A live key on a sandbox-only host.\n- `ip_not_allowed` (Shape A): The key's IP allow-list excludes the caller.\n- `insufficient_scope` (Shape A): The key, narrowed to its member's role ceiling, lacks team:write.\n- `member_required` (Shape B): A key issued to no member, on a tenant that has a member.\n- `capability_required` (Shape B): The caller's member is an admin or a developer. An approver's or viewer's key is refused earlier with insufficient_scope.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInsufficientScope": {
                    "$ref": "#/components/examples/TenantsTeamErrorInsufficientScope"
                  },
                  "TenantsTeamErrorMemberRequired": {
                    "$ref": "#/components/examples/TenantsTeamErrorMemberRequired"
                  },
                  "TenantsTeamErrorCapabilityRequired": {
                    "$ref": "#/components/examples/TenantsTeamErrorCapabilityRequired"
                  }
                }
              }
            }
          },
          "409": {
            "description": "- `idempotency_key_in_use` (Shape B): The Idempotency-Key was claimed by another API key of this tenant. Since the first 2026-10-28 deploy (10:46 UTC), also a repeat from the same API key whose stored answer does not open with that key: an answer stored unsealed before that deploy, until it expires 24 h after its first call. Its own message says the first request already ran; the request is not run again, so read the result back before resending under a new key.\n- `idempotency_parameter_mismatch` (Shape B): The same key, mode and path with another request hash.\n- `idempotency_key_in_progress` (Shape B): The first call under this key has not answered yet; after a relay restart mid-call it keeps answering this until the 24 h retention ends.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorIdempotencyMismatch": {
                    "$ref": "#/components/examples/TenantsTeamErrorIdempotencyMismatch"
                  },
                  "TenantsTeamErrorIdempotencyUnopened": {
                    "$ref": "#/components/examples/TenantsTeamErrorIdempotencyUnopened"
                  }
                }
              }
            }
          },
          "413": {
            "description": "- `request_body_too_large` (Shape B): A body over the request body limit.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "- `invalid_request` (Shape B): A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header (body-parser 1.20.6).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "- `internal_error` (Shape B): An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "TenantsTeamErrorInternal": {
                    "$ref": "#/components/examples/TenantsTeamErrorInternal"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or an otherwise valid key of a suspended tenant: an expired key of a suspended tenant still answers api_key_expired and a suspended member's key member_suspended, both checked first."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is missing."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks team:write."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was claimed by another API key of this tenant. Since the first 2026-10-28 deploy (10:46 UTC), also a repeat from the same API key whose stored answer does not open with that key: an answer stored unsealed before that deploy, until it expires 24 h after its first call. Its own message says the first request already ran; the request is not run again, so read the result back before resending under a new key."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same key, mode and path with another request hash."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first call under this key has not answered yet; after a relay restart mid-call it keeps answering this until the 24 h retention ends."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a database error, or a failed audit insert after the write), masked; error.param null and error.request_id set."
          },
          {
            "status": 403,
            "code": "member_required",
            "envelope": "B",
            "when": "A key issued to no member, on a tenant that has a member."
          },
          {
            "status": 403,
            "code": "capability_required",
            "envelope": "B",
            "when": "The caller's member is an admin or a developer. An approver's or viewer's key is refused earlier with insufficient_scope."
          },
          {
            "status": 400,
            "code": "invalid_policy_mode",
            "envelope": "B",
            "when": "mode missing, not a string, or not never/always/above after trim and lower-casing."
          },
          {
            "status": 400,
            "code": "invalid_approvals_required",
            "envelope": "B",
            "when": "approvals_required present and not null, and Number of it is not an integer from 1 to 3."
          },
          {
            "status": 400,
            "code": "invalid_thresholds",
            "envelope": "B",
            "when": "thresholds not an object (an array included); more than 50 entries; a key not matching [A-Z0-9.]{2,20} after trim and upper-casing; a value not a plain non-negative decimal; or mode above with no threshold."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "team:write"
      }
    },
    "/v1/sandbox": {
      "get": {
        "operationId": "getSandbox",
        "tags": [
          "API keys and sandbox"
        ],
        "summary": "Count what the calling tenant's sandbox holds",
        "description": "Answers the row counts of the calling tenant's sandbox: every row its fp_test_ keys created, for 17 objects. Works with either key mode: the counts are always the sandbox's, and `mode` says which kind of key asked. Read only: no audit row, no webhook, no Idempotency-Key handling (a GET), but a successful authentication writes the key's last_used_at when it is null or at least 5 minutes old (5 min default, no production override). Needs sandbox:read (or sandbox:write, or *) after role narrowing; every member role reaches it. What the sandbox keeps on purpose (API keys, members, the approval policy, tenant wallets, webhook endpoints) and the integration settings, which have no mode, are never counted.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body sent with a JSON content-type that does not parse. The body parser answers before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser refused an aborted upload, or a body whose length does not match its Content-Length, before every other check, with request_id null."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\" (the word Bearer is case-sensitive)."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "expires_at has passed."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host; production sets no such host."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The client address is outside the key ip_allowlist."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "No sandbox:read after role narrowing."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit, before every other check, with request_id null."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not a UTF one (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped error (a database failure), message masked."
          }
        ],
        "responses": {
          "200": {
            "description": "The sandbox summary.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SandboxSummary"
                },
                "examples": {
                  "withTestKey": {
                    "$ref": "#/components/examples/SandboxSummaryExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json` or the body parser's `invalid_request` (envelope B; request_id null, no X-Request-ID).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalidJson": {
                    "$ref": "#/components/examples/ErrInvalidJsonExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiKeysTenantUnauthorized"
          },
          "403": {
            "description": "`insufficient_scope`, `ip_not_allowed` or `sandbox_host_requires_test_key` (all envelope A, error_type permission_error).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "insufficientScope": {
                    "$ref": "#/components/examples/ErrInsufficientScopeSandboxReadExample"
                  },
                  "ipNotAllowed": {
                    "$ref": "#/components/examples/ErrIpNotAllowedExample"
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/ApiKeysBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/ApiKeysUnsupportedBody"
          },
          "500": {
            "$ref": "#/components/responses/ApiKeysInternalError"
          }
        },
        "x-fastpay-scope": "sandbox:read"
      }
    },
    "/v1/sandbox/reset": {
      "post": {
        "operationId": "resetSandbox",
        "tags": [
          "API keys and sandbox"
        ],
        "summary": "Delete every sandbox row of the calling tenant (fp_test_ key only)",
        "description": "Deletes the calling tenant's sandbox data in one SQLite transaction, all or nothing, and answers the fresh counts (all zero unless a sandbox write landed meanwhile) plus `deleted`, the counts read inside the transaction just before the deletes. Only an fp_test_ key may call it: a live key gets 403 sandbox_key_required after the scope check and the Idempotency-Key layer.\n\nDeleted (children first): payment attempts and status history, refunds, channel deposits, payments, checkout sessions, payment requests, payment links, payment channels and their addresses, customers, every sandbox payroll row (tx watches, reserved outpoints, payload versions, output claims, fee shortfalls and settlements, authorizations, items, batch approvals, batches, payer wallets), recipients and their addresses, processed allocation ids, sandbox jobs with their approvals and payments, sandbox ledger accounts and entries, sandbox webhook deliveries, and every sandbox-mode Idempotency-Key row of the tenant. Kept: API keys, members, the approval policy, tenant wallets, webhook endpoints, the integration settings, the payroll lanes, audit rows, every live row. A deleted sandbox payroll batch or payer wallet is gone for good: a rehearsal's evidence rows do not survive a reset. What survives is the audit log: this reset's own row with its counts, and every audit row the rehearsal's payroll calls wrote, whose ids no longer resolve.\n\nSide effects: one audit row `sandbox.reset` (actor_type tenant, actor_api_key_id the calling key, target the tenant, details {deleted}) written after the transaction; if that insert fails the answer is 500 with the reset already committed. No webhook.\n\nIdempotency-Key: the tenant layer claims the key before the handler runs, but the reset deletes every `test:` idempotency row of the tenant, its own pending row included, so a successful reset's answer is never stored (the capture's UPDATE finds no row): a retry under the same key runs the reset again (harmless: it deletes what was created in between). Any sandbox request in flight at that moment also loses its stored answer, and every earlier sandbox answer of the tenant stops replaying. A reset that fails inside the transaction rolls that delete back with the rest, so its 500 IS stored (sealed with the calling key) and replayed for 24 h; a 500 from the audit insert after the commit is not. A live key's 403 sandbox_key_required is stored under the `live:` scope like any refusal.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "description": "No field is read; any JSON body (or none) is accepted.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-key-mode": "fp_test_ only",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that does not parse, before every other check, with request_id null."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser refused an aborted upload or a Content-Length mismatch, with request_id null."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host: answered at authentication, before and instead of sandbox_key_required. Production sets no such host."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "No sandbox:write after role narrowing (an approver or viewer key never has it)."
          },
          {
            "status": 403,
            "code": "sandbox_key_required",
            "envelope": "B",
            "when": "A live (fp_live_) key. Nothing is deleted."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was first used by another API key of the tenant, or its stored answer does not open with the calling key (one stored unsealed before the 2026-10-28 deploy), until it expires."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "Same key, another body or query."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first call under the key has not answered."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "An unsupported charset or Content-Encoding on a JSON body, with request_id null."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "A failed delete rolls the whole transaction back (this 500 is stored under an Idempotency-Key); or the audit insert failed after the commit (not stored; the reset is done)."
          }
        ],
        "responses": {
          "200": {
            "description": "Reset done; `counts` read after the transaction, `deleted` what it removed.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SandboxResetResult"
                },
                "examples": {
                  "reset": {
                    "$ref": "#/components/examples/SandboxResetExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json` or the body parser's `invalid_request` (envelope B; request_id null).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalidJson": {
                    "$ref": "#/components/examples/ErrInvalidJsonExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiKeysTenantUnauthorized"
          },
          "403": {
            "description": "`sandbox_key_required` (envelope B: a live key) or the authentication layer's `insufficient_scope`, `ip_not_allowed`, `sandbox_host_requires_test_key` (envelope A).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "liveKey": {
                    "$ref": "#/components/examples/ErrSandboxKeyRequiredExample"
                  },
                  "insufficientScope": {
                    "$ref": "#/components/examples/ErrInsufficientScopeSandboxExample"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The tenant Idempotency-Key layer's `idempotency_key_in_use`, `idempotency_parameter_mismatch`, `idempotency_key_in_progress` (envelope B, request_id inside `error`).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "mismatch": {
                    "$ref": "#/components/examples/ErrIdempotencyMismatchExample"
                  },
                  "unopened": {
                    "$ref": "#/components/examples/ApiKeysErrIdempotencyUnopenedExample"
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/ApiKeysBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/ApiKeysUnsupportedBody"
          },
          "500": {
            "$ref": "#/components/responses/ApiKeysInternalError"
          }
        },
        "x-fastpay-scope": "sandbox:write"
      }
    },
    "/v1/members/{id}/api-keys": {
      "post": {
        "operationId": "mintMemberApiKey",
        "tags": [
          "API keys and sandbox"
        ],
        "summary": "A tenant owner or admin mints an API key for one of its members (raw key shown once)",
        "description": "The tenant's own mint, with a client key: the calling key needs `team:write` and a member whose role has canManageTeam, i.e. owner or admin. A key bound to no member gets 403 member_required, except while the tenant has no member at all, when it passes the gate (bootstrap) but then finds no member (404).\n\nOrder after authentication and the Idempotency-Key layer: the capability 403; the path member must be this tenant's, else 404 resource_missing; then: the key's mode first, `test` a JSON boolean when sent (400 invalid_test), the calling key's mode when absent or null, and false from a sandbox key 403 live_key_required; a suspended member 409 member_suspended; the body's policy fields in this order: name, scopes, ip_allowlist, expiry, payroll_networks; the scopes (the role's ceiling when absent or null) must fit the role ceiling, else 400 scopes_exceed_role; payroll_networks judged again against those scopes; finally a live key needs an approved tenant, else 409 tenant_not_approved and nothing is written.\n\nDefaults here differ from the admin mint, which requires test, scopes and expires_in_days and refuses expires_at: here nothing is required, `test` defaults to the calling key's mode and `scopes` to the role's ceiling; a live key may mint a sandbox key, never the reverse. No lifetime means the key never expires; `expires_at` (any future instant, no upper bound) wins over `expires_in_days`.\n\nSide effects: one api_keys row (a SHA-256 hash, never the raw key) and one audit row `api_key.created` (actor_type tenant with the calling key and member, target_type member, details {apiKeyId, prefix, test, scopes, expiresAt, ipAllowlist, memberId, payrollNetworks, and for a live key tenant_approval}) written after the insert. Unknown body fields (member_id among them) are ignored.\n\nIdempotency-Key: the tenant layer stores the 201, raw key included, sealed with a key derived from the calling raw API key (AES-GCM, HKDF; the relay never stores that key, so the database and its backups alone open nothing) for 24 h and replays it only to the same calling key; each replay first writes an audit row `api_key.replayed` (actor the caller, target the member, details {apiKeyId}), and a failed write fails the replay with 500 and hands nothing out. 409 tenant_not_approved is never stored. Without a key a retry mints a second key.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiKeyMemberMintRequest"
              },
              "examples": {
                "developerSandboxKey": {
                  "$ref": "#/components/examples/ApiKeyMemberMintRequestExample"
                }
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that does not parse, before every other check, with request_id null."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A path id that is not valid percent-encoding, answered before authentication; the body parser's refusal of an aborted upload or a Content-Length mismatch; or payroll_networks not a non-empty array, repeating a network, or on a key whose scopes reach no payroll scope."
          },
          {
            "status": 400,
            "code": "invalid_test",
            "envelope": "B",
            "when": "test present, not null and not a JSON boolean (the string \"false\", 0 and 1 included)."
          },
          {
            "status": 400,
            "code": "invalid_name",
            "envelope": "B",
            "when": "name not a string, or over 100 characters."
          },
          {
            "status": 400,
            "code": "invalid_scopes",
            "envelope": "B",
            "when": "scopes not a non-empty array of known scopes."
          },
          {
            "status": 400,
            "code": "invalid_ip_allowlist",
            "envelope": "B",
            "when": "Not an array, over 50 entries, an entry that is not an address or address/prefix with a 1-3 digit prefix within the family's width, a /0, or an IPv6 range holding every IPv4 client."
          },
          {
            "status": 400,
            "code": "invalid_expires_at",
            "envelope": "B",
            "when": "expires_at not a future ISO-8601 instant, or expires_in_days not in (0, 3650]."
          },
          {
            "status": 400,
            "code": "unsupported_network",
            "envelope": "B",
            "when": "A payroll_networks entry that is no payroll network; error.details.networks lists them."
          },
          {
            "status": 400,
            "code": "scopes_exceed_role",
            "envelope": "B",
            "when": "A scope above the target member role ceiling."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The CALLING key member is suspended."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host; production sets none."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "No team:write after role narrowing."
          },
          {
            "status": 403,
            "code": "member_required",
            "envelope": "B",
            "when": "The calling key is bound to no member and the tenant has members."
          },
          {
            "status": 403,
            "code": "capability_required",
            "envelope": "B",
            "when": "The calling member role is not owner or admin."
          },
          {
            "status": 403,
            "code": "live_key_required",
            "envelope": "B",
            "when": "test is false and the calling key is a sandbox key. Nothing minted."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No such member in this tenant."
          },
          {
            "status": 409,
            "code": "member_suspended",
            "envelope": "B",
            "when": "The TARGET member is suspended (checked after the key mode, before every other body field)."
          },
          {
            "status": 409,
            "code": "tenant_not_approved",
            "envelope": "B",
            "when": "A live key for a tenant not approved yet; nothing minted; never stored by the Idempotency-Key layer."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "Another key of the tenant used the Idempotency-Key first, or the stored answer does not open with this key."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "An unsupported charset or Content-Encoding on a JSON body, with request_id null."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "For example the audit insert failed after the key row was written (the key exists and nobody received it), or the api_key.replayed write failed on a replay (nothing handed out)."
          }
        ],
        "responses": {
          "201": {
            "description": "The api_key object plus `apiKey`, the raw key, shown once, and the member route's own warning.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyMinted"
                },
                "examples": {
                  "memberSandboxKey": {
                    "$ref": "#/components/examples/ApiKeyMemberMinted201Example"
                  }
                }
              }
            }
          },
          "400": {
            "description": "A policy field refused, `invalid_test`, `scopes_exceed_role`, or a body-parser or path refusal (envelope B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "scopesExceedRole": {
                    "$ref": "#/components/examples/ErrScopesExceedRoleExample"
                  },
                  "unsupportedNetwork": {
                    "$ref": "#/components/examples/ErrUnsupportedNetworkExample"
                  },
                  "invalidTest": {
                    "$ref": "#/components/examples/ApiKeysErrInvalidTestExample"
                  },
                  "ipAllowlist": {
                    "$ref": "#/components/examples/ApiKeysErrInvalidIpAllowlistExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiKeysTenantUnauthorized"
          },
          "403": {
            "description": "`member_required`, `capability_required`, `live_key_required` (envelope B) or the authentication layer's 403s (envelope A).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "capabilityRequired": {
                    "$ref": "#/components/examples/ErrCapabilityRequiredExample"
                  },
                  "liveKeyRequired": {
                    "$ref": "#/components/examples/ApiKeysErrLiveKeyRequiredExample"
                  }
                }
              }
            }
          },
          "404": {
            "description": "`resource_missing`: no such member (envelope B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "noMember": {
                    "$ref": "#/components/examples/ErrNoMemberTenantRouteExample"
                  }
                }
              }
            }
          },
          "409": {
            "description": "`member_suspended`, `tenant_not_approved` or an Idempotency-Key conflict (envelope B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "memberSuspended": {
                    "$ref": "#/components/examples/ErrMemberSuspendedMintExample"
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/ApiKeysBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/ApiKeysUnsupportedBody"
          },
          "500": {
            "$ref": "#/components/responses/ApiKeysInternalError"
          }
        },
        "x-fastpay-scope": "team:write"
      },
      "get": {
        "operationId": "listMemberApiKeys",
        "tags": [
          "API keys and sandbox"
        ],
        "summary": "List one member's API keys (never their material)",
        "description": "One page of the member's keys, both modes and every status, newest first by insertion order. Without `limit` the page is the largest one (100). Only `limit`, `starting_after` and `ending_before` are read; any other query parameter is ignored. The cursor is looked up within the tenant, not the member: another member's key id still places the page, and an unknown id answers the first page.\n\nNeeds only `team:read` and checks no capability: any member key whose role reaches team:read (owner, admin, developer, approver and viewer all do) may list any member's keys, including their ip_allowlist and last_used_at. Keys are not partitioned by mode (api_keys is in NO_MODE_TABLES): an fp_test_ key with team:read lists the member's live keys too. A path member that is not this tenant's: 404 resource_missing, checked before the query. Read only: no audit row.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKeyListLimit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B",
            "when": "limit not an integer from 1 to 100, or both cursors."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A path id that is not valid percent-encoding (before authentication), or the body parser's refusal of an aborted upload or a Content-Length mismatch."
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A JSON body sent with the GET that does not parse."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host; production sets none."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "No team:read after role narrowing."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No such member in this tenant."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "An unsupported charset or Content-Encoding on a JSON body, with request_id null."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the member's api_key objects.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyList"
                },
                "examples": {
                  "onePage": {
                    "$ref": "#/components/examples/ApiKeyList200Example"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_pagination_parameter`, or a path or body-parser refusal (envelope B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "badLimit": {
                    "$ref": "#/components/examples/ErrInvalidPaginationExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiKeysTenantUnauthorized"
          },
          "403": {
            "description": "The authentication layer's 403s (envelope A).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`resource_missing`: no such member (envelope B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "noMember": {
                    "$ref": "#/components/examples/ErrNoMemberTenantRouteExample"
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/ApiKeysBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/ApiKeysUnsupportedBody"
          },
          "500": {
            "$ref": "#/components/responses/ApiKeysInternalError"
          }
        },
        "x-fastpay-scope": "team:read"
      }
    },
    "/v1/api-keys/me": {
      "get": {
        "operationId": "getCallingApiKey",
        "tags": [
          "API keys and sandbox"
        ],
        "summary": "Introspect the calling API key",
        "description": "Answers the calling key's api_key object plus `member`: {id, name, role, effective_scopes} for a member-bound key, null for an unbound one. `effective_scopes` is the key's scopes narrowed to the member's CURRENT role ceiling, so a demotion shows at once. Any key may call it: /v1/api-keys/* needs no scope, whatever the letter case of the path, but the IP allow-list and the sandbox-host rule still apply, so a key with an ip_allowlist answers 403 ip_not_allowed from any other address. No limiter, no audit row. Side effect: authentication writes last_used_at when it is null or at least 5 minutes (5 min) old, before the IP and scope checks: a portal's own check after a reveal sets the new key's last_used_at. Refusals: unknown, revoked, or the tenant suspended 401 invalid_api_key; expired 401 api_key_expired; member suspended 401 member_suspended, in the order revoked, expired, member, tenant.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A JSON body sent with the GET that does not parse."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser refusing an aborted upload or a length mismatch."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host; production sets none."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "The key row vanished between authentication and the read; keys are never deleted, so practically unreachable."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "An unsupported charset or Content-Encoding on a JSON body, with request_id null."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "responses": {
          "200": {
            "description": "The calling key and its member.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyIntrospection"
                },
                "examples": {
                  "makerLiveSolana": {
                    "$ref": "#/components/examples/ApiKeyIntrospection200Example"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json` or the body parser's `invalid_request` (envelope B), only if a body is sent with the GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalidJson": {
                    "$ref": "#/components/examples/ErrInvalidJsonExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiKeysTenantUnauthorized"
          },
          "403": {
            "description": "`ip_not_allowed` or `sandbox_host_requires_test_key` (envelope A); never insufficient_scope.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "ipNotAllowed": {
                    "$ref": "#/components/examples/ErrIpNotAllowedExample"
                  }
                }
              }
            }
          },
          "404": {
            "description": "`resource_missing` (envelope B), practically unreachable.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/ApiKeysBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/ApiKeysUnsupportedBody"
          },
          "500": {
            "$ref": "#/components/responses/ApiKeysInternalError"
          }
        },
        "x-fastpay-scope": "none"
      }
    },
    "/v1/api-keys/rotate": {
      "post": {
        "operationId": "rotateCallingApiKey",
        "tags": [
          "API keys and sandbox"
        ],
        "summary": "Rotate the calling key (a full-scope key only)",
        "description": "Self-service rotation with the admin rotate's semantics: a successor with the same mode, name, scopes, IP allow-list, member and payroll_networks, never expiring, written in one transaction with the calling key's change; the calling key lives `grace_hours` more (0-168, default 24; 0 revokes it at once). Only a key whose STORED scopes include \"*\" may call it, else 403 insufficient_scope (envelope A). The gate reads the stored scopes, not the role-narrowed ones.\n\nOrder: authentication (no scope needed at any letter case); the Idempotency-Key layer; the \"*\" gate; grace_hours (400 invalid_grace_hours, Number coercion as on the admin route); then: a live key of an unapproved tenant 409 tenant_not_approved, nothing rotated; a key no longer active 409 api_key_not_rotatable (practically a race: a revoked or expired key is refused at authentication).\n\nSide effects: as the admin rotate; the audit row `api_key.rotated` names the tenant actor (actor_type tenant, actor_api_key_id the calling key). Idempotency-Key: tenant layer; the 201 with the successor's raw key is stored for 24 h sealed with a key derived from the CALLING (old) key and replayed only to it, each replay first writing an `api_key.replayed` audit row (target the tenant, details {apiKeyId} of the successor). So whoever holds a leaked old key and a copy of the database or a backup of the window can open the successor offline: rotate a key believed leaked without an Idempotency-Key, or have the operator revoke it and mint another. With grace_hours 0 (or \"\", false, []) the calling key is revoked at once, so a retry after a lost answer is refused at authentication (401 invalid_api_key, before the replay layer) and the successor's raw key is lost for good while the successor stays active: keep a grace window.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiKeyRotationRequest"
              },
              "examples": {
                "oneHourGrace": {
                  "$ref": "#/components/examples/ApiKeyRotateRequestExample"
                }
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser refusing an aborted upload or a length mismatch."
          },
          {
            "status": 400,
            "code": "invalid_grace_hours",
            "envelope": "B",
            "when": "grace_hours not a number from 0 to 168 after Number coercion."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key (the old key after a grace-0 rotation included), or a key of a suspended tenant."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a sandbox-only host; production sets none."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key stored scopes do not include * (the route own check; the auth layer asks no scope here)."
          },
          {
            "status": 409,
            "code": "tenant_not_approved",
            "envelope": "B",
            "when": "A live key of an unapproved tenant; nothing rotated; never stored for replay."
          },
          {
            "status": 409,
            "code": "api_key_not_rotatable",
            "envelope": "B",
            "when": "The key is no longer active when the rotation runs."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "Another key of the tenant used the Idempotency-Key first, or the stored answer does not open with this key."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "An unsupported charset or Content-Encoding on a JSON body, with request_id null."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "A failure inside the rotation transaction (nothing changed); a failed api_key.rotated audit insert after the rotation committed (an active successor nobody received, the calling key on its grace window or revoked); or the api_key.replayed write failing on a replay (nothing handed out)."
          }
        ],
        "responses": {
          "201": {
            "description": "The successor and the calling key's fate.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyRotated"
                },
                "examples": {
                  "rotated": {
                    "$ref": "#/components/examples/ApiKeyRotated201Example"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_grace_hours`, or the body parser's `invalid_json` / `invalid_request` (envelope B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "grace": {
                    "$ref": "#/components/examples/ErrInvalidGraceHoursExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiKeysTenantUnauthorized"
          },
          "403": {
            "description": "`insufficient_scope` (envelope A), or the authentication layer's other 403s.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "notFullScope": {
                    "$ref": "#/components/examples/ErrInsufficientScopeRotateExample"
                  }
                }
              }
            }
          },
          "409": {
            "description": "`tenant_not_approved`, `api_key_not_rotatable` or an Idempotency-Key conflict (envelope B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "notRotatable": {
                    "$ref": "#/components/examples/ErrApiKeyNotRotatableExample"
                  },
                  "unopened": {
                    "$ref": "#/components/examples/ApiKeysErrIdempotencyUnopenedExample"
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/ApiKeysBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/ApiKeysUnsupportedBody"
          },
          "500": {
            "$ref": "#/components/responses/ApiKeysInternalError"
          }
        },
        "x-fastpay-scope": "none at the auth layer; the route requires the key's stored scopes to include *"
      }
    },
    "/health": {
      "get": {
        "operationId": "getRelayHealth",
        "tags": [
          "Payroll live gate and payer wallets"
        ],
        "summary": "Liveness, database check and the kill switch",
        "description": "Unauthenticated: no key, no operator and no rate limit (no global limiter is mounted between the body parser and the routes), so a platform health check can reach it (rationale, handler). Runs `SELECT 1` on the relay database and reads the kill switch. A payer-wallet registration, verification or revoke and every lane write are not stopped by it. It is the portal's only read of the kill switch.\n\nA database failure (or a kill-switch read that throws, which is the same database) answers a fixed 503 body; the driver's text goes to the operator log only. The 200 body is not error-shaped, so it carries no error_* fields; the 503 body carries `error` as a string (envelope A), so the res.json wrapper adds error_type null, error_slug null, error_message 'Database unavailable.' and request_id: branch on the status, never on error_slug. A request body sent with a JSON content type is still parsed, so an unreadable one gets the body parser's 400, 413 or 415. No audit row, no side effect.",
        "security": [],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body sent with a JSON content type that does not parse, or whose top-level value is not an object or an array. The body parser answers before routing: request_id null, no X-Request-ID header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of an aborted upload or of a body whose length does not match its Content-Length, before routing (request_id null)."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit, answered by the body parser before routing (request_id null)."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content type with an unsupported charset, or a Content-Encoding other than identity, gzip or deflate (the body parser's own message), before routing (request_id null)."
          }
        ],
        "responses": {
          "200": {
            "description": "The process is up and the database answers.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelayHealthOk"
                },
                "examples": {
                  "running": {
                    "summary": "Running",
                    "value": {
                      "status": "ok",
                      "paused": false
                    }
                  },
                  "killSwitchOn": {
                    "summary": "Paused by an operator",
                    "value": {
                      "status": "ok",
                      "paused": true
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified: a conditional GET whose If-None-Match holds the 200 body's ETag (Express's default weak ETag; the relay does not turn it off). No body."
          },
          "400": {
            "description": "Body parser refusals: invalid_json, or invalid_request (an aborted upload or a length mismatch); Shape B, request_id null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/LiveGateBodyTooLarge413"
          },
          "415": {
            "$ref": "#/components/responses/LiveGateBodyEncodingUnsupported415"
          },
          "503": {
            "description": "The database is unavailable.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelayHealthDatabaseUnavailable"
                },
                "examples": {
                  "databaseDown": {
                    "value": {
                      "status": "error",
                      "error": "Database unavailable.",
                      "error_type": null,
                      "error_slug": null,
                      "error_message": "Database unavailable.",
                      "request_id": "req_00000000-0000-4000-8000-000000000000"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/payroll-payer-wallets": {
      "post": {
        "operationId": "registerPayrollPayerWallet",
        "tags": [
          "Payroll live gate and payer wallets"
        ],
        "summary": "Register the client's own payer wallet on one payroll network",
        "description": "A client key registers the wallet it will pay payroll FROM and receives the control-proof challenge to sign. FastPay never holds a key for it; signing the challenge authorizes no payment.\n\nChecks, in this order (the first failure answers): the body parser; requireApiKey: the key, the sandbox host, the IP allow-list, the scope payroll:write narrowed by the member's role, then the tenant Idempotency-Key layer; the per-tenant per-minute limiter; a member-bound key whose role cannot manage the team (developer) is 403 team_capability_required (approver and viewer keys already stopped at 403 insufficient_scope because their role ceiling is read-only, a key bound to no member passes this check); the body; a key minted with payroll_networks that does not list the network: 403 api_key_network_not_allowed; then the network gate: a sandbox (fp_test_) key gets the mock adapter on every payroll network and is never gated; a live key needs the master switch and the family switch on, the network not held, the network inside the lane ceiling the lane ceiling, and this tenant's lane on the network reading exactly `enabled`, else 503 payroll_network_not_enabled. Its text says \"is not open for this account's live keys.\" for the tenant's lane or the ceiling, and \"is not open for live keys right now.\" for a switch that is off OR a network hold, so the text alone never tells a hold apart. Then no family config fault and a configured, startable deployment (503 payroll_network_not_configured) and no rotation pending (503 payroll_deployment_rotation_pending). Then the address is canonicalised by the network's adapter (400 invalid_address); chain_options are judged (only live Bitcoin takes them: a P2WSH vault must; every other network, and every network with a sandbox key, answers 400 invalid_chain_options); the address and any declared member key are screened against FastPay's held-key registry (409 payer_wallet_is_fastpay_key with one fixed text on purpose, or 503 payer_verification_unavailable while the registry is not ready); a live EVM opaque contract wallet is refused (409 payer_wallet_kind_unsupported); a live Bitcoin vault's addresses are sanctions-screened (403 payer_wallet_screening_refused / 503 payer_screening_unavailable); the challenge envelope is built (TON with no proof domain: 503 payroll_network_not_configured); and the row is inserted (409 payer_wallet_already_registered when a non-revoked row already exists for this tenant, network, canonical address and mode:, a pending row counts).\n\nSuccess: 201, the wallet in status pending_verification with its challenge. The response adds the challenge view to the wallet object: `challenge` (the exact text to sign), `challenge_format`, `challenge_extra`, `challenge_expires_at`, `chain_ref`, and for TON `nonce`, `ton_proof_payload` and `accepted_domains`. The challenge lives its lifetime (default 900 s); a Bitcoin vault gets the maximum lifetime (default and ceiling 24 h). Audit row payroll_payer_wallet.registered (details network, address, proof_timing, is_test; actor the tenant, its member and API key), written through the payroll audit sink, which never fails the request. A refusal by the held-key registry, the kind screen or the sanctions screen writes its own audit row: payroll_payer_wallet.fastpay_key_refused,.kind_refused (plus a notice to FastPay).screening_refused. No webhook. The kill switch does not stop a registration (no pause check).\n\nSandbox vs live: an fp_test_ key registers on the mock adapter (chain_ref `mock:<network>`), never needs a lane, is still screened against the held-key registry, takes no chain_options on any network (the mock has no registration hook), and its challenge_extra is `{}` where live Solana sends `{encoding, envelopes}`. Idempotency: optional tenant Idempotency-Key; every answer but a 429, 5xx included, is stored, sealed with the calling API key, and replayed for 24 h with X-Cache-Lookup: HIT: a 503 from the lane gate replays under the same key even after the lane opens, so retry with a new key. Without a key a repeat answers 409 payer_wallet_already_registered: list the wallets and use POST /{id}/challenge on the existing row. On TON (no address until the proof) a repeat makes another pending row: unconnected at_connect rows never collide on the unique index, and a second row proving the same address is refused only at verify (409 payer_wallet_already_registered).",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayerWalletRegistrationBody"
              },
              "examples": {
                "solanaLive": {
                  "summary": "A Solana payer (challenge_first)",
                  "value": {
                    "network": "solana",
                    "address": "EXAMPLEPayerWa11et1111111111111111111111111",
                    "label": "Treasury payer"
                  }
                },
                "tonAtConnect": {
                  "summary": "TON (at_connect) - the address comes from the proof later",
                  "value": {
                    "network": "ton",
                    "label": "TON payer"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:write",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body sent with a JSON content type that does not parse, or whose top-level value is not an object or an array. The body parser answers before routing: request_id null, no X-Request-ID header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of an aborted upload or of a body whose length does not match its Content-Length, before routing (request_id null); the body is a JSON array or not an object (\"The request body must be a JSON object.\"), or label is present but not a string of 1 to 100 characters."
          },
          {
            "status": 400,
            "code": "unsupported_network",
            "envelope": "B",
            "when": "network missing or not one of the 19 payroll networks (exact, lower case); details.networks lists them."
          },
          {
            "status": 400,
            "code": "invalid_address",
            "envelope": "B",
            "when": "address present but not a non-empty string; on every network but TON, missing or not an address of the network (\"address must be a valid <network> address.\")."
          },
          {
            "status": 400,
            "code": "invalid_chain_options",
            "envelope": "B",
            "when": "chain_options not an object or over 8192 bytes serialised (details.max_bytes); sent on a network, or with a sandbox key, that takes none; live Bitcoin: options on a single-key address, a P2WSH vault without chain_options.multisig, or a malformed vault."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a valid key of a suspended tenant (an expired key or a suspended member's key of that tenant answers its own slug first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is gone."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks payroll:write: neither payroll:write nor *, or an approver or viewer member's key (read-only ceiling)."
          },
          {
            "status": 403,
            "code": "team_capability_required",
            "envelope": "B",
            "when": "A member key whose role cannot manage the team (developer). An approver's or viewer's key never gets here: 403 insufficient_scope first."
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B",
            "when": "The key's payroll_networks does not list the wallet's network; details {network, key_networks}. Checked before the lane gate."
          },
          {
            "status": 403,
            "code": "payer_wallet_screening_refused",
            "envelope": "B",
            "when": "Live Bitcoin vault only: the vault's address or a member's address failed wallet sanctions screening (needs a screening provider; this build has none)."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was first used by another API key of this tenant; or the answer stored under it does not open with the calling key, which is every answer stored unsealed before the 2026-10-28 deploy, until it expires 24 h after it was stored (its own message: look up the first request's result before sending again under a new key).",
            "note": "Stored answers sealed with the calling key."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same key, mode and path with another body or query."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first request under this key has not answered yet."
          },
          {
            "status": 409,
            "code": "payer_wallet_is_fastpay_key",
            "envelope": "B",
            "when": "The address, or a vault's declared member key, is held by FastPay (sandbox keys too); one fixed message on purpose, the matched source in the audit row only. Not checked here on TON (no address yet)."
          },
          {
            "status": 409,
            "code": "payer_wallet_kind_unsupported",
            "envelope": "B",
            "when": "Live EVM key: an opaque contract wallet (details {network, kind: contract}). A node that cannot be read here lets the registration; verify reads again."
          },
          {
            "status": 409,
            "code": "payer_multisig_not_enabled",
            "envelope": "B",
            "when": "Live Bitcoin: a P2WSH vault address while multisig payer support is off (checked before the vault's chain_options are read)."
          },
          {
            "status": 409,
            "code": "payer_wallet_already_registered",
            "envelope": "B",
            "when": "A non-revoked row (pending or verified) exists for this tenant, network, canonical address and mode."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit, answered by the body parser before routing (request_id null)."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content type with an unsupported charset, or a Content-Encoding other than identity, gzip or deflate (the body parser's own message), before routing (request_id null)."
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A",
            "when": "Over the tenant's per-minute budget (its requests_per_minute, else 60 per minute by default, default 60); error_type rate_limit_error, Retry-After and RateLimit-* headers. Runs after the Idempotency-Key layer and is never stored."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a bug, a database error); its text goes to the operator log only. Never read it as a state."
          },
          {
            "status": 503,
            "code": "payroll_network_not_enabled",
            "envelope": "B",
            "when": "Live key only: the master or family switch off, the network held, outside the lane ceiling, or this tenant's lane not enabled; details.network. The text names a tenant reason (\"...for this account's live keys.\") or a network one (\"...for live keys right now.\")."
          },
          {
            "status": 503,
            "code": "payroll_network_not_configured",
            "envelope": "B",
            "when": "Live key only: a family config fault, a missing deployment setting, or an adapter that cannot start; details.network. Also TON with no ton_proof domain configured (neither the production host nor a TON proof domain list); nothing is stored."
          },
          {
            "status": 503,
            "code": "payroll_deployment_rotation_pending",
            "envelope": "B",
            "when": "Live key only: holding authorizations sit on a retired deployment of the network; details.network."
          },
          {
            "status": 503,
            "code": "payer_verification_unavailable",
            "envelope": "B",
            "when": "FastPay's held-key registry cannot answer yet (\"FastPay cannot complete its wallet safety check right now. Retry shortly.\"); details.network. Nothing was written: retry under a new Idempotency-Key."
          },
          {
            "status": 503,
            "code": "payer_screening_unavailable",
            "envelope": "B",
            "when": "Live Bitcoin vault while payer screening is true and the screen cannot run. Nothing was registered."
          }
        ],
        "responses": {
          "201": {
            "description": "Registered, pending verification, with the challenge to sign.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayerWalletWithChallenge"
                },
                "examples": {
                  "solanaLive": {
                    "$ref": "#/components/examples/PayerWalletRegisteredSolanaLive"
                  },
                  "solanaSandbox": {
                    "$ref": "#/components/examples/PayerWalletRegisteredSolanaSandbox"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_json, invalid_request (the body parser, a body that is not a JSON object, or a bad label), unsupported_network, invalid_address or invalid_chain_options; all Shape B.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                },
                "examples": {
                  "unsupportedNetwork": {
                    "$ref": "#/components/examples/UnsupportedNetworkError"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LiveGateTenantUnauthorized401"
          },
          "403": {
            "description": "sandbox_host_requires_test_key, ip_not_allowed or insufficient_scope (Shape A, before the handler); team_capability_required, api_key_network_not_allowed or payer_wallet_screening_refused (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "networkNotAllowed": {
                    "$ref": "#/components/examples/ApiKeyNetworkNotAllowedError"
                  }
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflicts and registration refusals (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                },
                "examples": {
                  "alreadyRegistered": {
                    "value": {
                      "error": {
                        "type": "invalid_request_error",
                        "code": "payer_wallet_already_registered",
                        "message": "This wallet is already registered on this network. Revoke the existing registration first."
                      },
                      "error_type": "invalid_request_error",
                      "error_slug": "payer_wallet_already_registered",
                      "error_message": "This wallet is already registered on this network. Revoke the existing registration first.",
                      "request_id": "req_00000000-0000-4000-8000-000000000000"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/LiveGateBodyTooLarge413"
          },
          "415": {
            "$ref": "#/components/responses/LiveGateBodyEncodingUnsupported415"
          },
          "429": {
            "$ref": "#/components/responses/LiveGateTenantRateLimited429"
          },
          "500": {
            "$ref": "#/components/responses/LiveGateInternalError500"
          },
          "503": {
            "description": "The live gate or a check that cannot run now (Shape B, details.network where the class sets it).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                },
                "examples": {
                  "laneNotOpen": {
                    "$ref": "#/components/examples/PayrollNetworkNotEnabledTenantError"
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listPayrollPayerWallets",
        "tags": [
          "Payroll live gate and payer wallets"
        ],
        "summary": "List this tenant's payer wallets in the key's mode",
        "description": "Newest first by insertion order, cursor-paged, only rows of the calling key's mode: a live key never sees sandbox rows. Filters: status, network, created[gte]/created[lte] (and the flat created_gte/created_lte), checked in the order limit, cursors, status, created, network. A cursor that is none of the tenant's wallets gives the first page; one of the tenant's wallets in the other mode, or outside the filters, still places the page at its position (the cursor is looked up by tenant only). Revoked rows stay listed. Any role with payroll:read (approver and viewer included); never limited by the key's payroll_networks (reads are not). No lane gate, no rate limiter, no audit row.\n\nEach item is the wallet object without the challenge view: `challenge` and `challenge_expires_at` show only while the wallet is pending, and challenge_format and challenge_extra are not served. A pending row whose `challenge` is null has no open challenge: a verify attempt consumed it or an intent or fee quote sent the wallet back to verification (reverification_reason set); either way POST /{id}/challenge comes next. verified_at, verified_by_member_id, verification_method and address_kind are NOT cleared when a wallet is sent back: decide on `status`, never on verified_at. A bad limit, cursor pair or filter is a Shape B 400; any other error goes through the payroll mapping.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "$ref": "#/components/parameters/CreatedGte"
          },
          {
            "$ref": "#/components/parameters/CreatedLte"
          },
          {
            "$ref": "#/components/parameters/LiveGateCreatedGteAlias"
          },
          {
            "$ref": "#/components/parameters/LiveGateCreatedLteAlias"
          },
          {
            "$ref": "#/components/parameters/PayerWalletStatusFilter"
          },
          {
            "$ref": "#/components/parameters/PayrollNetworkQueryFilter"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:read",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body sent with a JSON content type that does not parse, or whose top-level value is not an object or an array. The body parser answers before routing: request_id null, no X-Request-ID header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of an aborted upload or of a body whose length does not match its Content-Length, before routing (request_id null)."
          },
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B",
            "when": "limit not a whole number from 1 to 100, or both starting_after and ending_before."
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B",
            "when": "status not pending_verification, verified or revoked; a created bound unreadable, reversed or not in bracket form; network not a payroll network; or a filter parameter repeated."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a valid key of a suspended tenant (an expired key or a suspended member's key of that tenant answers its own slug first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is gone."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks payroll:read (any payroll:* or * scope passes)."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit, answered by the body parser before routing (request_id null)."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content type with an unsupported charset, or a Content-Encoding other than identity, gzip or deflate (the body parser's own message), before routing (request_id null)."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure reading the page; text in the operator log only."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of payer wallets.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayerWalletList"
                },
                "examples": {
                  "onePage": {
                    "value": {
                      "object": "list",
                      "has_more": false,
                      "data": [
                        {
                          "id": "pyw_00000000000000000000000000000001",
                          "object": "payroll_payer_wallet",
                          "network": "solana",
                          "chain_ref": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
                          "address": "EXAMPLEPayerWa11et1111111111111111111111111",
                          "address_kind": "solana_ed25519",
                          "proof_timing": "challenge_first",
                          "status": "verified",
                          "verification_method": "solana_sign_message",
                          "verified_at": "2026-10-24T09:05:00.000Z",
                          "revoked_at": null,
                          "reverification_reason": null,
                          "label": "Treasury payer",
                          "challenge": null,
                          "challenge_expires_at": null,
                          "created_by_member_id": "mem_00000000000000000000000000000001",
                          "verified_by_member_id": "mem_00000000000000000000000000000001",
                          "is_test": false,
                          "created_at": "2026-10-24T09:00:00.000Z",
                          "updated_at": "2026-10-24T09:05:00.000Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified: a conditional GET whose If-None-Match holds the 200 body's ETag (Express's default weak ETag; the relay does not turn it off). No body."
          },
          "400": {
            "description": "invalid_json or invalid_request (the body parser), invalid_pagination_parameter or invalid_filter_parameter; all Shape B.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LiveGateTenantUnauthorized401"
          },
          "403": {
            "$ref": "#/components/responses/LiveGateTenantReadForbidden403"
          },
          "413": {
            "$ref": "#/components/responses/LiveGateBodyTooLarge413"
          },
          "415": {
            "$ref": "#/components/responses/LiveGateBodyEncodingUnsupported415"
          },
          "500": {
            "$ref": "#/components/responses/LiveGateInternalError500"
          }
        }
      }
    },
    "/v1/payroll-payer-wallets/{id}": {
      "get": {
        "operationId": "getPayrollPayerWallet",
        "tags": [
          "Payroll live gate and payer wallets"
        ],
        "summary": "Retrieve one payer wallet",
        "description": "The calling tenant's wallet in the key's mode. A wallet of another tenant or of the other mode is 404 resource_missing (Shape B, \"No such payer wallet.\"). Any role with payroll:read; no team-capability check, no key-network check, no lane gate, no rate limiter. The body is the wallet object only: `challenge` and `challenge_expires_at` while pending, but no challenge_format, challenge_extra, nonce or accepted_domains. The challenge view is built only by the register and challenge routes, so a client that lost the 201 calls POST /{id}/challenge, which replaces the challenge. No audit row.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:read",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body sent with a JSON content type that does not parse, or whose top-level value is not an object or an array. The body parser answers before routing: request_id null, no X-Request-ID header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of an aborted upload or of a body whose length does not match its Content-Length, before routing (request_id null); a path parameter that is not valid percent-encoding (\"A path parameter is not valid percent-encoding.\", the global handler)."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a valid key of a suspended tenant (an expired key or a suspended member's key of that tenant answers its own slug first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is gone."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks payroll:read (any payroll:* or * scope passes)."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No payer wallet with this id for this tenant in the key's mode (\"No such payer wallet.\")."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit, answered by the body parser before routing (request_id null)."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content type with an unsupported charset, or a Content-Encoding other than identity, gzip or deflate (the body parser's own message), before routing (request_id null)."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a bug, a database error); its text goes to the operator log only. Never read it as a state."
          }
        ],
        "responses": {
          "200": {
            "description": "The wallet.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollPayerWallet"
                },
                "examples": {
                  "verified": {
                    "$ref": "#/components/examples/PayerWalletVerifiedSolanaLive"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified: a conditional GET whose If-None-Match holds the 200 body's ETag (Express's default weak ETag; the relay does not turn it off). No body."
          },
          "400": {
            "description": "invalid_json or invalid_request (the body parser, or a path parameter that is not valid percent-encoding); Shape B.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LiveGateTenantUnauthorized401"
          },
          "403": {
            "$ref": "#/components/responses/LiveGateTenantReadForbidden403"
          },
          "404": {
            "$ref": "#/components/responses/PayerWalletNotFound404"
          },
          "413": {
            "$ref": "#/components/responses/LiveGateBodyTooLarge413"
          },
          "415": {
            "$ref": "#/components/responses/LiveGateBodyEncodingUnsupported415"
          },
          "500": {
            "$ref": "#/components/responses/LiveGateInternalError500"
          }
        }
      },
      "delete": {
        "operationId": "revokePayrollPayerWallet",
        "tags": [
          "Payroll live gate and payer wallets"
        ],
        "summary": "Revoke a payer wallet (stops new intents at once)",
        "description": "Moves a pending or verified wallet to revoked. It stops every NEW intent at once; an authorization already issued stays executable by that wallet until its own expires_at and the sweep keeps watching it. The answer adds `live_authorizations`: how many of THIS tenant's holding authorizations (issuing, live or flagged) exist for that canonical address, network and mode, across any of the tenant's registrations of it (0 for an at_connect wallet that never connected). It does not cancel batches that name the wallet.\n\nChecks in order: requireApiKey and the tenant Idempotency-Key layer; 403 team_capability_required for a developer member key; 404 resource_missing; 403 api_key_network_not_allowed for a key limited to other networks; 409 payer_wallet_already_revoked. Not lane-gated and not rate-limited: a revoke works with the lane paused or closed and with the kill switch on. Any owner or admin key of the tenant may revoke any payer of the tenant. Audit row payroll_payer_wallet.revoked (details network, address, holding_authorizations), written through the sink that never fails the request. No webhook. Sandbox and live behave the same, each on its own mode's rows. Idempotency: every answer but a 429 is stored, sealed with the calling key, and replayed for 24 h.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:write",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body sent with a JSON content type that does not parse, or whose top-level value is not an object or an array. The body parser answers before routing: request_id null, no X-Request-ID header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of an aborted upload or of a body whose length does not match its Content-Length, before routing (request_id null); a path parameter that is not valid percent-encoding (\"A path parameter is not valid percent-encoding.\", the global handler)."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a valid key of a suspended tenant (an expired key or a suspended member's key of that tenant answers its own slug first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is gone."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks payroll:write: neither payroll:write nor *, or an approver or viewer member's key (read-only ceiling)."
          },
          {
            "status": 403,
            "code": "team_capability_required",
            "envelope": "B",
            "when": "A member key whose role cannot manage the team (developer). An approver's or viewer's key never gets here: 403 insufficient_scope first."
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B",
            "when": "The key's payroll_networks does not list the wallet's network; details {network, key_networks}. Checked before the lane gate."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No payer wallet with this id for this tenant in the key's mode (\"No such payer wallet.\")."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was first used by another API key of this tenant; or the answer stored under it does not open with the calling key, which is every answer stored unsealed before the 2026-10-28 deploy, until it expires 24 h after it was stored (its own message: look up the first request's result before sending again under a new key).",
            "note": "Stored answers sealed with the calling key."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same key, mode and path with another body or query."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first request under this key has not answered yet."
          },
          {
            "status": 409,
            "code": "payer_wallet_already_revoked",
            "envelope": "B",
            "when": "Already revoked, or revoked by a concurrent request (= done)."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit, answered by the body parser before routing (request_id null)."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content type with an unsupported charset, or a Content-Encoding other than identity, gzip or deflate (the body parser's own message), before routing (request_id null)."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a bug, a database error); its text goes to the operator log only. Never read it as a state."
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked; live_authorizations counts what stays executable until expiry.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayerWalletRevoked"
                },
                "examples": {
                  "revoked": {
                    "$ref": "#/components/examples/PayerWalletRevokedSolanaLive"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_json or invalid_request (the body parser, or a path parameter that is not valid percent-encoding); Shape B.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LiveGateTenantUnauthorized401"
          },
          "403": {
            "description": "sandbox_host_requires_test_key, ip_not_allowed or insufficient_scope (Shape A, before the handler); team_capability_required or api_key_network_not_allowed (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/PayerWalletNotFound404"
          },
          "409": {
            "description": "Idempotency conflicts or payer_wallet_already_revoked (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/LiveGateBodyTooLarge413"
          },
          "415": {
            "$ref": "#/components/responses/LiveGateBodyEncodingUnsupported415"
          },
          "500": {
            "$ref": "#/components/responses/LiveGateInternalError500"
          }
        }
      }
    },
    "/v1/payroll-payer-wallets/{id}/challenge": {
      "post": {
        "operationId": "refreshPayrollPayerWalletChallenge",
        "tags": [
          "Payroll live gate and payer wallets"
        ],
        "summary": "Issue a fresh control-proof challenge for a wallet pending verification",
        "description": "Replaces the stored challenge; the previous one stops working at once. Used after an expired challenge, after any failed verify (every verify past the proof parser consumes the challenge), and when an intent or a fee quote sent a verified wallet back to pending_verification (payer_wallet_reverification_required, which clears the challenge). The request body is not read.\n\nChecks in order: requireApiKey and the tenant Idempotency-Key layer; the per-tenant limiter (429); 403 team_capability_required; 404 resource_missing; 403 api_key_network_not_allowed; 409 payer_wallet_not_pending for a verified or revoked wallet; the network gate as on registration (live keys only: 503 payroll_network_not_enabled, payroll_network_not_configured, payroll_deployment_rotation_pending); the envelope (TON with no proof domain: 503 payroll_network_not_configured, nothing changed); 409 payer_wallet_not_pending if the wallet left pending meanwhile. 200: the wallet with the new challenge view, as on registration. The held-key and kind checks do not run here; verify runs them. No audit row. Idempotency: every answer but a 429 is stored, sealed with the calling key, and replayed for 24 h: a replay under the same Idempotency-Key returns the stored challenge text, which no longer verifies if another challenge was issued since (a verify against it consumes the current challenge and answers 400 invalid_payer_signature): use a new key for each fresh challenge. Sandbox keys are never gated.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Ignored (any JSON object, or none).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:write",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body sent with a JSON content type that does not parse, or whose top-level value is not an object or an array. The body parser answers before routing: request_id null, no X-Request-ID header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of an aborted upload or of a body whose length does not match its Content-Length, before routing (request_id null); a path parameter that is not valid percent-encoding (\"A path parameter is not valid percent-encoding.\", the global handler)."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a valid key of a suspended tenant (an expired key or a suspended member's key of that tenant answers its own slug first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is gone."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks payroll:write: neither payroll:write nor *, or an approver or viewer member's key (read-only ceiling)."
          },
          {
            "status": 403,
            "code": "team_capability_required",
            "envelope": "B",
            "when": "A member key whose role cannot manage the team (developer). An approver's or viewer's key never gets here: 403 insufficient_scope first."
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B",
            "when": "The key's payroll_networks does not list the wallet's network; details {network, key_networks}. Checked before the lane gate."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No payer wallet with this id for this tenant in the key's mode (\"No such payer wallet.\")."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was first used by another API key of this tenant; or the answer stored under it does not open with the calling key, which is every answer stored unsealed before the 2026-10-28 deploy, until it expires 24 h after it was stored (its own message: look up the first request's result before sending again under a new key).",
            "note": "Stored answers sealed with the calling key."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same key, mode and path with another body or query."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first request under this key has not answered yet."
          },
          {
            "status": 409,
            "code": "payer_wallet_not_pending",
            "envelope": "B",
            "when": "The wallet is verified or revoked, or left pending_verification while the new challenge was built (\"The payer wallet is no longer pending verification.\")."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit, answered by the body parser before routing (request_id null)."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content type with an unsupported charset, or a Content-Encoding other than identity, gzip or deflate (the body parser's own message), before routing (request_id null)."
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A",
            "when": "Over the tenant's per-minute budget (its requests_per_minute, else 60 per minute by default, default 60); error_type rate_limit_error, Retry-After and RateLimit-* headers. Runs after the Idempotency-Key layer and is never stored."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a bug, a database error); its text goes to the operator log only. Never read it as a state."
          },
          {
            "status": 503,
            "code": "payroll_network_not_enabled",
            "envelope": "B",
            "when": "Live key only: the master or family switch off, the network held, outside the lane ceiling, or this tenant's lane not enabled; details.network. The text names a tenant reason (\"...for this account's live keys.\") or a network one (\"...for live keys right now.\")."
          },
          {
            "status": 503,
            "code": "payroll_network_not_configured",
            "envelope": "B",
            "when": "Live key only: a family config fault, a missing deployment setting, or an adapter that cannot start; details.network. Also TON with no ton_proof domain configured; the old challenge stays."
          },
          {
            "status": 503,
            "code": "payroll_deployment_rotation_pending",
            "envelope": "B",
            "when": "Live key only: holding authorizations sit on a retired deployment of the network; details.network."
          }
        ],
        "responses": {
          "200": {
            "description": "The wallet with its new challenge.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayerWalletWithChallenge"
                },
                "examples": {
                  "solanaLive": {
                    "$ref": "#/components/examples/PayerWalletRegisteredSolanaLive"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_json or invalid_request (the body parser, or a path parameter that is not valid percent-encoding); Shape B.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LiveGateTenantUnauthorized401"
          },
          "403": {
            "description": "sandbox_host_requires_test_key, ip_not_allowed or insufficient_scope (Shape A, before the handler); team_capability_required or api_key_network_not_allowed (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/PayerWalletNotFound404"
          },
          "409": {
            "description": "Idempotency conflicts or payer_wallet_not_pending (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/LiveGateBodyTooLarge413"
          },
          "415": {
            "$ref": "#/components/responses/LiveGateBodyEncodingUnsupported415"
          },
          "429": {
            "$ref": "#/components/responses/LiveGateTenantRateLimited429"
          },
          "500": {
            "$ref": "#/components/responses/LiveGateInternalError500"
          },
          "503": {
            "description": "The live gate (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                },
                "examples": {
                  "laneNotOpen": {
                    "$ref": "#/components/examples/PayrollNetworkNotEnabledTenantError"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/payroll-payer-wallets/{id}/verify": {
      "post": {
        "operationId": "verifyPayrollPayerWallet",
        "tags": [
          "Payroll live gate and payer wallets"
        ],
        "summary": "Submit the wallet's signed proof of control",
        "description": "Proves the wallet controls the registered address and marks it verified. The body is `{proof: {...}}` or the proof's fields at the top level: when `proof` holds an object, that object is the proof, so a TON proof (TonConnect's `{account, proof}`) must be wrapped. The proof's shape is the network's (PayerWalletProof).\n\nChecks in this order: requireApiKey and the tenant Idempotency-Key layer; the per-tenant limiter; 403 team_capability_required; 404 resource_missing; 403 api_key_network_not_allowed; 409 payer_wallet_not_pending; the network gate (live keys: 503 payroll_network_not_enabled / payroll_network_not_configured / payroll_deployment_rotation_pending); 400 invalid_proof from the adapter's proof parser, the challenge NOT used up; Bitcoin only, a self-send proof not yet confirmed: 202 payer_proof_pending with details {retry_after_seconds, challenge_expires_at} and the challenge kept, or 503 payer_verification_unavailable (details.network) when the providers cannot say. THEN the challenge is consumed, single use whatever the outcome: 409 payer_challenge_missing (none open), 410 payer_challenge_expired. Then the chain check: a node that cannot answer is 503 payer_verification_unavailable, never \"invalid\" (from the core wrapper with details.network, from an adapter in its own words and without details, for example; live Tron also when the node's answer is not this account's: an object carrying Error, naming no account or naming another); an adapter may refuse inside its check (409 payer_wallet_delegated, e.g. an off-curve Solana address such as a Squads vault PDA, 409 payer_multisig_not_enabled for a Bitcoin vault while multisig payers are off, 503 payroll_network_not_configured for TON with no proof domain). A wallet of a refused kind: 409 payer_wallet_delegated, then 409 payer_wallet_is_fastpay_key (or 503 payer_verification_unavailable from the registry), then 409 payer_wallet_kind_unsupported; a proof that shows no control: 400 invalid_payer_signature with details.reason; a Bitcoin vault's declared keys are re-judged (400 invalid_chain_options / 409 payer_multisig_not_enabled); then a delegation, a FastPay-held controlling key or address; finally the store's re-check: 409 payer_address_mismatch, 400 invalid_address (an at_connect proof that names no address), 409 payer_wallet_already_registered (TON: the connected address is already registered), 409 payer_wallet_not_pending (lost a race). After any failure from the challenge step on, request a new challenge, sign it again and send it under a new Idempotency-Key.\n\n200: the wallet in status verified with verification_method (`sandbox` for an fp_test_ key), address_kind, verified_at and verified_by_member_id. Audit row payroll_payer_wallet.verified (method, kind, is_test); refusals write payroll_payer_wallet.fastpay_key_refused /.kind_refused, through the sink that never fails the request. No webhook. Sandbox: the mock adapter accepts any non-empty proof.signature except the literal `invalid` and needs no RPC. Idempotency: every answer but a 429 is stored, sealed with the calling key, and replayed for 24 h, the 202 and the 5xx included, so resending the same proof under the same key replays the 202 (use a new key per try).",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayerWalletVerifyBody"
              },
              "examples": {
                "solanaWrapped": {
                  "summary": "Solana, wrapped in proof",
                  "value": {
                    "proof": {
                      "signature": "EXAMPLEsignatureBase58xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
                    }
                  }
                },
                "solanaFlat": {
                  "summary": "Solana, at the top level",
                  "value": {
                    "signature": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
                  }
                },
                "sandbox": {
                  "summary": "Sandbox key (any non-empty signature but \"invalid\")",
                  "value": {
                    "proof": {
                      "signature": "sandbox-ok"
                    }
                  }
                }
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:write",
        "x-fastpay-error-codes": [
          {
            "status": 202,
            "code": "payer_proof_pending",
            "envelope": "B",
            "when": "Bitcoin self_send_opreturn proof not confirmed yet: the challenge stays open; details {retry_after_seconds (120, or less when the challenge expires sooner), challenge_expires_at}; no Retry-After header. Asked only while the wallet has an open, unexpired challenge: without one the request goes on to 409 payer_challenge_missing or 410 payer_challenge_expired.",
            "note": "A 2xx in the error envelope: check error_slug on a 202. Stored under an Idempotency-Key like any answer."
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body sent with a JSON content type that does not parse, or whose top-level value is not an object or an array. The body parser answers before routing: request_id null, no X-Request-ID header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of an aborted upload or of a body whose length does not match its Content-Length, before routing (request_id null); a path parameter that is not valid percent-encoding (\"A path parameter is not valid percent-encoding.\", the global handler)."
          },
          {
            "status": 400,
            "code": "invalid_proof",
            "envelope": "B",
            "when": "The network's proof parser refused the body (for example \"proof.signature is required.\"); the challenge is kept."
          },
          {
            "status": 400,
            "code": "invalid_payer_signature",
            "envelope": "B",
            "when": "The proof shows no control of this wallet; details.reason (default invalid_signature; the sandbox's literal \"invalid\" signature). The challenge is used up."
          },
          {
            "status": 400,
            "code": "invalid_address",
            "envelope": "B",
            "when": "An at_connect (TON) proof that names no address; a defensive check after the proof passed."
          },
          {
            "status": 400,
            "code": "invalid_chain_options",
            "envelope": "B",
            "when": "Bitcoin vault: its stored chain_options are refused when re-judged at verify."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a valid key of a suspended tenant (an expired key or a suspended member's key of that tenant answers its own slug first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended, or its member row is gone."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, lacks payroll:write: neither payroll:write nor *, or an approver or viewer member's key (read-only ceiling)."
          },
          {
            "status": 403,
            "code": "team_capability_required",
            "envelope": "B",
            "when": "A member key whose role cannot manage the team (developer). An approver's or viewer's key never gets here: 403 insufficient_scope first."
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B",
            "when": "The key's payroll_networks does not list the wallet's network; details {network, key_networks}. Checked before the lane gate."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No payer wallet with this id for this tenant in the key's mode (\"No such payer wallet.\")."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was first used by another API key of this tenant; or the answer stored under it does not open with the calling key, which is every answer stored unsealed before the 2026-10-28 deploy, until it expires 24 h after it was stored (its own message: look up the first request's result before sending again under a new key).",
            "note": "Stored answers sealed with the calling key."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same key, mode and path with another body or query."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first request under this key has not answered yet."
          },
          {
            "status": 409,
            "code": "payer_wallet_not_pending",
            "envelope": "B",
            "when": "The wallet is verified or revoked, or stopped being pending before the final write (a lost race)."
          },
          {
            "status": 409,
            "code": "payer_challenge_missing",
            "envelope": "B",
            "when": "No open challenge: an earlier attempt used it, it was never issued, or an intent or fee quote sent the wallet back to verification (which clears it)."
          },
          {
            "status": 409,
            "code": "payer_wallet_delegated",
            "envelope": "B",
            "when": "Live key: a delegation payroll refuses (for example a Solana off-curve address such as a Squads vault PDA, an EVM 7702 delegate or Safe module, a TON plugin); details.reason."
          },
          {
            "status": 409,
            "code": "payer_wallet_is_fastpay_key",
            "envelope": "B",
            "when": "A controlling key or the address is held by FastPay (sandbox keys too); a vault's declared keys are screened again."
          },
          {
            "status": 409,
            "code": "payer_wallet_kind_unsupported",
            "envelope": "B",
            "when": "Live EVM key: an opaque contract wallet or a Safe its opaque contract owners can operate alone; details {network, kind}. Answered after a delegation and a held key."
          },
          {
            "status": 409,
            "code": "payer_multisig_not_enabled",
            "envelope": "B",
            "when": "Live Bitcoin vault while multisig payer support is off."
          },
          {
            "status": 409,
            "code": "payer_address_mismatch",
            "envelope": "B",
            "when": "The proof is for another address than the registered one."
          },
          {
            "status": 409,
            "code": "payer_wallet_already_registered",
            "envelope": "B",
            "when": "At_connect (TON): the address the proof names already has a non-revoked row of this tenant in this mode."
          },
          {
            "status": 410,
            "code": "payer_challenge_expired",
            "envelope": "B",
            "when": "The consumed challenge was past its expiry; it is used up."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit, answered by the body parser before routing (request_id null)."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content type with an unsupported charset, or a Content-Encoding other than identity, gzip or deflate (the body parser's own message), before routing (request_id null)."
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A",
            "when": "Over the tenant's per-minute budget (its requests_per_minute, else 60 per minute by default, default 60); error_type rate_limit_error, Retry-After and RateLimit-* headers. Runs after the Idempotency-Key layer and is never stored."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a bug, a database error); its text goes to the operator log only. Never read it as a state."
          },
          {
            "status": 503,
            "code": "payroll_network_not_enabled",
            "envelope": "B",
            "when": "Live key only: the master or family switch off, the network held, outside the lane ceiling, or this tenant's lane not enabled; details.network. The text names a tenant reason (\"...for this account's live keys.\") or a network one (\"...for live keys right now.\")."
          },
          {
            "status": 503,
            "code": "payroll_network_not_configured",
            "envelope": "B",
            "when": "Live key only: a family config fault, a missing deployment setting, or an adapter that cannot start; details.network. Also TON with no ton_proof domain configured, raised inside the proof check (the challenge is used up)."
          },
          {
            "status": 503,
            "code": "payroll_deployment_rotation_pending",
            "envelope": "B",
            "when": "Live key only: holding authorizations sit on a retired deployment of the network; details.network."
          },
          {
            "status": 503,
            "code": "payer_verification_unavailable",
            "envelope": "B",
            "when": "A node, the Bitcoin providers or the held-key registry could not answer (live Tron also: a node answer that carries Error, names no account or names another account). Before the challenge is consumed only for Bitcoin's readiness check; otherwise the challenge is used up. details.network is set by the core and the registry, absent when an adapter raised it in its own words."
          }
        ],
        "responses": {
          "200": {
            "description": "Verified.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollPayerWallet"
                },
                "examples": {
                  "verified": {
                    "$ref": "#/components/examples/PayerWalletVerifiedSolanaLive"
                  }
                }
              }
            }
          },
          "202": {
            "description": "Bitcoin only: the proof is not final on chain yet; nothing failed and the challenge stays open. The body is error-shaped (Shape B, error_slug payer_proof_pending): a client must check error_slug on a 2xx here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                },
                "examples": {
                  "pending": {
                    "value": {
                      "error": {
                        "type": "invalid_request_error",
                        "code": "payer_proof_pending",
                        "message": "The proof is not final on chain yet. Send the same proof again later; the challenge stays open until it expires.",
                        "details": {
                          "retry_after_seconds": 120,
                          "challenge_expires_at": "2026-10-25T09:00:00.000Z"
                        }
                      },
                      "error_type": "invalid_request_error",
                      "error_slug": "payer_proof_pending",
                      "error_message": "The proof is not final on chain yet. Send the same proof again later; the challenge stays open until it expires.",
                      "request_id": "req_00000000-0000-4000-8000-000000000000"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_json, invalid_request (the body parser or the path), invalid_proof, invalid_payer_signature, invalid_address or invalid_chain_options; all Shape B.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                },
                "examples": {
                  "badSignature": {
                    "value": {
                      "error": {
                        "type": "invalid_request_error",
                        "code": "invalid_payer_signature",
                        "message": "The proof does not show control of this wallet. Request a new challenge and sign it with the wallet itself.",
                        "details": {
                          "reason": "invalid_signature"
                        }
                      },
                      "error_type": "invalid_request_error",
                      "error_slug": "invalid_payer_signature",
                      "error_message": "The proof does not show control of this wallet. Request a new challenge and sign it with the wallet itself.",
                      "request_id": "req_00000000-0000-4000-8000-000000000000"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LiveGateTenantUnauthorized401"
          },
          "403": {
            "description": "sandbox_host_requires_test_key, ip_not_allowed or insufficient_scope (Shape A, before the handler); team_capability_required or api_key_network_not_allowed (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/PayerWalletNotFound404"
          },
          "409": {
            "description": "Idempotency conflicts and verification refusals (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                },
                "examples": {
                  "challengeMissing": {
                    "value": {
                      "error": {
                        "type": "invalid_request_error",
                        "code": "payer_challenge_missing",
                        "message": "There is no open challenge for this wallet. Request one with POST /v1/payroll-payer-wallets/{id}/challenge."
                      },
                      "error_type": "invalid_request_error",
                      "error_slug": "payer_challenge_missing",
                      "error_message": "There is no open challenge for this wallet. Request one with POST /v1/payroll-payer-wallets/{id}/challenge.",
                      "request_id": "req_00000000-0000-4000-8000-000000000000"
                    }
                  }
                }
              }
            }
          },
          "410": {
            "description": "payer_challenge_expired (Shape B); the challenge is used up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/LiveGateBodyTooLarge413"
          },
          "415": {
            "$ref": "#/components/responses/LiveGateBodyEncodingUnsupported415"
          },
          "429": {
            "$ref": "#/components/responses/LiveGateTenantRateLimited429"
          },
          "500": {
            "$ref": "#/components/responses/LiveGateInternalError500"
          },
          "503": {
            "description": "The live gate, a chain or provider check that could not run, or the held-key registry (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollShapeBErrorBody"
                },
                "examples": {
                  "solanaNodeDown": {
                    "summary": "From the Solana adapter (no details)",
                    "value": {
                      "error": {
                        "type": "api_error",
                        "code": "payer_verification_unavailable",
                        "message": "The Solana node could not be asked about EXAMPLEPayerWa11et1111111111111111111111111. Retry shortly."
                      },
                      "error_type": "api_error",
                      "error_slug": "payer_verification_unavailable",
                      "error_message": "The Solana node could not be asked about EXAMPLEPayerWa11et1111111111111111111111111. Retry shortly.",
                      "request_id": "req_00000000-0000-4000-8000-000000000000"
                    }
                  },
                  "registryCold": {
                    "summary": "From the held-key registry (details.network)",
                    "value": {
                      "error": {
                        "type": "api_error",
                        "code": "payer_verification_unavailable",
                        "message": "FastPay cannot complete its wallet safety check right now. Retry shortly.",
                        "details": {
                          "network": "solana"
                        }
                      },
                      "error_type": "api_error",
                      "error_slug": "payer_verification_unavailable",
                      "error_message": "FastPay cannot complete its wallet safety check right now. Retry shortly.",
                      "request_id": "req_00000000-0000-4000-8000-000000000000"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/payroll-batches": {
      "post": {
        "operationId": "createPayrollBatch",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Create a client-signed payroll batch",
        "description": "Creates one payroll batch and all its items in one database transaction, paid later from the tenant's own verified payer wallet in transactions the payer signs itself. Nothing is signed and no chain write happens here.\n\n**Order of checks** (each only after the previous passed): the kill switch (503 service_paused, before the body is even parsed); the body shape and bounds (every item problem is reported at once in error.details.problems); a key limited by payroll_networks must cover `network` (403 api_key_network_not_allowed); `callback_url` must resolve to a public host (400 invalid_url); the network gate (a sandbox key always gets the sandbox adapter; a live key needs the master and family switches, the tenant's lane on the network `enabled` and the network not held, no config fault, a configured deployment that starts, and no rotation pending); the restore fence; the token (adapter.resolveToken); the payer wallet (this tenant, this key's mode, `verified`, same network); `recipient_id` items resolved to the recipient's registered address on the network; the deployment's own addresses read from the chain (chain_unavailable on a node error); every recipient canonicalised with the network's strict rule and refused when it is the batch's own payer, a static refusal of the adapter (zero address, disburser, token) or an address of the FastPay deployment; then the store: amounts converted to base units with the token's decimals (400 invalid_items with details {position} for too many decimals), a reference repeated inside the batch (409), the wallet re-checked inside the transaction and every item reference checked against the tenant's references still held (409 duplicate_item_reference).\n\n**Status at creation**: the tenant's payout approval policy decides. `never` (the default for a tenant that never wrote one) creates `open` with `approvals_required` 0; `always` creates `awaiting_approval`; `above` creates `awaiting_approval` when the summed amount reaches the currency's threshold or the currency has no threshold (fail closed). `approval: \"required\"` makes this one batch `awaiting_approval` whatever the policy. A waiting batch snapshots the policy's `approvals_required` (1 to 3) at creation; a later policy change never alters it. The batch lives the batch lifetime (default 7 days); past it the settlement sweep closes it.\n\n**References.** Every item `reference` is held tenant-wide and per key mode, on every network and across makers, while its item is `pending`, `authorized` or `completed` (unique partial index). It is released only when its item ends `failed` or `cancelled`; a completed item holds it for good. The batch's own `reference` is free text and is NOT unique (no index on it).\n\n**Side effects.** Audit row `payroll_batch.created` with network, currency, payer wallet, item count, total and status (sandbox rows are written too, only the INFO log line is skipped). Webhook `payroll_batch.awaiting_approval` when the batch waits, never awaited, to the subscribed account endpoints and to the batch's `callback_url` if one was set. The callback_url delivery is signed with the tenant webhook secret, an account endpoint's with that endpoint's own (x-webhook-signature); the raw tenant secret also rides in the clear in `x-webhook-secret`, but only on a LIVE batch's callback_url delivery: never set `callback_url` on a live batch.\n\n**Idempotency.** With `Idempotency-Key`, a replay of the same body answers the batch AS IT IS NOW, not the stored 201 snapshot (refreshOnReplay), so a batch rejected since reads `cancelled`. Its 5xx answers are never stored, so a retry under the same key runs again; 4xx answers are stored 24 h (except 429). A replay is answered before the rate limiter runs (then). Without a key, the reference guard (409 duplicate_item_reference) is the second net against a double create. A sandbox reset deletes the tenant's sandbox batches AND its sandbox Idempotency-Key answers in one transaction, and no other code deletes a batch: after a reset the same key on an `fp_test_` create runs the request again and makes a new batch (no stored answer is left to replay). The stored answers are sealed with a key derived from the calling API key: only that key opens them.\n\n**Sandbox vs live.** An `fp_test_` key creates a sandbox batch (`is_test` true) on the sandbox adapter: never lane-gated, no chain read, and on every family but Stacks and Bitcoin (which apply live's own token rule) ANY token symbol but the network's native coin is accepted with the network's real decimals or 6, while a live Solana key needs a configured mint. The approval policy is per tenant and applies to both modes alike. References, payer wallets and batches of the two modes never mix.\n\n**Body size.** This route has its own parser, registered before the app-wide one: 500 (items per batch) x 1024 + 16384 bytes = 528384 by default; above it 413 request_body_too_large before authentication.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:write",
        "x-fastpay-rate-limited": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayrollBatchCreateRequest"
              },
              "examples": {
                "solanaOnePerson": {
                  "summary": "Solana USDC, two items (a one-person client under `never` gets `open`)",
                  "value": {
                    "network": "solana",
                    "currency": "USDC",
                    "payer_wallet_id": "pyw_0123456789abcdef0123456789abcdef",
                    "reference": "example-payroll-2026-10",
                    "items": [
                      {
                        "reference": "example-2026-10-sol-emp-001",
                        "recipient_address": "Examp1eRecipientA11111111111111111111111111",
                        "amount": "1500.00"
                      },
                      {
                        "reference": "example-2026-10-sol-emp-002",
                        "recipient_id": "rcp_example0001",
                        "amount": "980.5"
                      }
                    ]
                  }
                },
                "approvalRequired": {
                  "summary": "One batch that must wait for an approver whatever the policy",
                  "value": {
                    "network": "solana",
                    "currency": "USDC",
                    "payer_wallet_id": "pyw_0123456789abcdef0123456789abcdef",
                    "approval": "required",
                    "items": [
                      {
                        "reference": "example-2026-10-sol-bonus-001",
                        "recipient_address": "Examp1eRecipientB11111111111111111111111111",
                        "amount": "250"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The batch, as created. `approvals` is empty and `counts.pending` equals `item_count`. No items array: read GET /v1/payroll-batches/{id}/items. A replay under the same Idempotency-Key answers 201 with the batch as it is now, with `X-Cache-Lookup: HIT`.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollBatch"
                },
                "examples": {
                  "open": {
                    "$ref": "#/components/examples/PayrollBatchOpenExample"
                  },
                  "awaitingApproval": {
                    "$ref": "#/components/examples/PayrollBatchAwaitingExample"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "Shape B unless noted. invalid_json (body parser, request_id null); invalid_request (body not an object, or the body parser's own 400: an aborted upload, or a length that does not match Content-Length); unsupported_network (details.networks); invalid_currency; invalid_payer_wallet; invalid_items in three shapes: no details at all when `items` is missing, not an array or empty, details.problems [{index, field, problem}] for every item or recipient problem at once, or details {position} from the store for the first amount with more decimal places than the token, trailing zeros counted; too_many_items (details.max_items); invalid_reference; invalid_url; invalid_approval; native_currency_not_supported_for_payroll (details {network, currency}); recipient_not_found / recipient_archived / recipient_missing_address / recipient_wallet_mismatch (details: a list of {index, recipient_id,...})."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A from authentication: sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope (an approver or viewer key). Shape B: api_key_network_not_allowed (details {network, key_networks})."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B payer_wallet_not_found (no wallet with that id in this tenant and key mode)."
          },
          "409": {
            "$ref": "#/components/responses/PayrollConflict",
            "description": "Shape B: payer_wallet_not_verified; payer_wallet_network_mismatch; currency_not_allowlisted (details {network, currency}); duplicate_item_reference (details.references: every reference refused, repeated in the batch or still held by another item); idempotency_key_in_use, idempotency_parameter_mismatch, idempotency_key_in_progress (request_id inside error)."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "429": {
            "$ref": "#/components/responses/PayrollRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          },
          "503": {
            "$ref": "#/components/responses/PayrollUnavailable",
            "description": "Shape B, never stored for replay (a retry under the same key runs again): service_paused; payroll_network_not_enabled (details.network; live keys only: switches, the tenant lane or a network hold); payroll_network_not_configured; payroll_deployment_rotation_pending; payroll_restore_reconciliation_pending (details.retryable false: do not retry); chain_unavailable."
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 503,
            "code": "service_paused",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "another API key of the tenant used this Idempotency-Key; or the answer stored under it was stored unsealed before the 2026-10-28 deploys and has not expired"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "the body is not a JSON object; or a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 400,
            "code": "unsupported_network",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_currency",
            "envelope": "B",
            "when": "not 1 to 16 letters or digits; or the network adapter refuses the symbol (Stacks pays SBTC only, Bitcoin BTC only)"
          },
          {
            "status": 400,
            "code": "invalid_payer_wallet",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_items",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "too_many_items",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_reference",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_url",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_approval",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_network_not_enabled",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_network_not_configured",
            "envelope": "B",
            "when": "a step of the live gate (config faults, missing settings, an adapter that cannot start); TON: the jetton has no pinned decimals (TON_JETTON_DECIMALS)"
          },
          {
            "status": 503,
            "code": "payroll_deployment_rotation_pending",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_restore_reconciliation_pending",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "native_currency_not_supported_for_payroll",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "currency_not_allowlisted",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "payer_wallet_not_found",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payer_wallet_not_verified",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payer_wallet_network_mismatch",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "recipient_not_found",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "recipient_archived",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "recipient_missing_address",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "recipient_wallet_mismatch",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "chain_unavailable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "duplicate_item_reference",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      },
      "get": {
        "operationId": "listPayrollBatches",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "List payroll batches",
        "description": "One page of the calling key's mode's batches, newest first by insertion order. Each batch carries its item `counts` (one GROUP BY for the page) and its approve/reject decisions. Filters: `status`, `network`, `created[gte]`/`created[lte]`; there is no reference or payer-wallet filter. Not limited by the key's payroll_networks: a Solana-only key reads every network's batches. No side effect, no audit row, not rate limited. A list parse error is Shape B 400; any other error goes through the payroll mapping.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:read",
        "x-fastpay-rate-limited": false,
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "$ref": "#/components/parameters/CreatedGte"
          },
          {
            "$ref": "#/components/parameters/CreatedLte"
          },
          {
            "$ref": "#/components/parameters/PayrollBatchStatusFilter"
          },
          {
            "$ref": "#/components/parameters/PayrollNetworkFilter"
          }
        ],
        "responses": {
          "200": {
            "description": "A list of batches.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollBatchList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "Shape B invalid_pagination_parameter or invalid_filter_parameter (an unknown status or network, a repeated parameter, a bad created bound); invalid_json; invalid_request from the body parser itself (an aborted upload, or a length that does not match Content-Length)."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}": {
      "get": {
        "operationId": "getPayrollBatch",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Retrieve one payroll batch",
        "description": "The batch with its item counts and approve/reject decisions. Scoped to the tenant and the key's mode: a batch of the other mode or another tenant is 404 resource_missing. Not limited by the key's payroll_networks. Not rate limited, no audit row.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:read",
        "x-fastpay-rate-limited": false,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "The batch.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollBatch"
                },
                "examples": {
                  "awaitingApproval": {
                    "$ref": "#/components/examples/PayrollBatchAwaitingExample"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "invalid_json (a malformed JSON body sent with the GET). Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding (for example %E0%A4%A): Express cannot decode it, so the global handler answers before any route step runs (no authentication); or a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}/items": {
      "get": {
        "operationId": "listPayrollBatchItems",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "List a batch's items",
        "description": "One page of the batch's items, newest first by insertion order. Items are inserted in position order in one transaction, so a page reads in REVERSE position order: sort by `position` yourself, and page until has_more is false (a 500-item batch is five pages at limit 100). The batch is loaded first (404 before any list parse error); then `status` and `created` filters. An item's `fee`, `tx_hash`, `chain_position` and `completed_at` are null until it completes. `recipient` is the network's display form of the canonical address, which may differ in spelling from what was sent. `amount` is the string as sent; the batch's `total_amount` is the normalised sum. No side effect; not rate limited.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:read",
        "x-fastpay-rate-limited": false,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "$ref": "#/components/parameters/CreatedGte"
          },
          {
            "$ref": "#/components/parameters/CreatedLte"
          },
          {
            "$ref": "#/components/parameters/PayrollItemStatusFilter"
          }
        ],
        "responses": {
          "200": {
            "description": "A list of items.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollItemList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "Shape B invalid_pagination_parameter, invalid_filter_parameter; invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing (the batch)."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding (for example %E0%A4%A): Express cannot decode it, so the global handler answers before any route step runs (no authentication); or a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}/approve": {
      "post": {
        "operationId": "approvePayrollBatch",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Approve a batch awaiting approval",
        "description": "Casts the calling member's approve vote on a batch in `awaiting_approval`. Needs only `payroll:read`: the member's capability decides, so an `approver` member (read scopes only) can approve but never create. Send the path without a trailing slash: the scope rule's capability pattern is anchored, so `.../approve/` still reaches the route but needs `payroll:write`, and an approver key gets 403 insufficient_scope; the same holds for reject. Letter case no longer matters: the rule reads the path in lower case, as Express routes it. Order: the batch (404; 409 batch_not_awaiting_approval); the key's payroll_networks; the approver rule and four eyes: a key bound to no member is 403 member_required (a suspended member's key is refused earlier, at authentication, with 401 member_suspended; the service's 403 member_suspended is defence in depth), a role without the approve capability (developer, viewer) 403 approval_not_permitted, and the member whose key created the batch 403 self_approval_forbidden; the batch inside its lifetime (409 payroll_batch_expired); then one vote per member in one transaction: a second vote by the same member is 409 already_approved. When the approve votes reach the batch's own `approvals_required` the batch moves to `open` at once. Any approve-capable member other than the creator counts: an `owner` or `admin` maker may approve another maker's batch. A batch created by a key with no member has no creator to exclude.\n\n**Side effects.** Audit row `payroll_batch.approved` {approvals, approvals_required, activated}. Webhook `payroll_batch.approved` {approvals} only when this vote opened the batch. No body is read. Not rate limited. With `Idempotency-Key`, a repeat replays the stored answer (2xx, 4xx or 5xx) for 24 h; without one, a repeat by the same member answers 409 already_approved (or batch_not_awaiting_approval once open).",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:read",
        "x-fastpay-rate-limited": false,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The batch after the vote; `status` is `open` when the quorum was reached, else still `awaiting_approval` with one more decision in `approvals`.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollBatch"
                },
                "examples": {
                  "opened": {
                    "$ref": "#/components/examples/PayrollBatchApprovedExample"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope. Shape B api_key_network_not_allowed, member_required, member_suspended, approval_not_permitted, self_approval_forbidden."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing."
          },
          "409": {
            "$ref": "#/components/responses/PayrollConflict",
            "description": "Shape B batch_not_awaiting_approval, payroll_batch_expired, already_approved; the three Idempotency-Key conflicts."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "batch_not_awaiting_approval",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "member_required",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "member_suspended",
            "envelope": "B",
            "note": "not reached over HTTP: authentication answers 401 member_suspended first"
          },
          {
            "status": 403,
            "code": "approval_not_permitted",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "self_approval_forbidden",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_batch_expired",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "already_approved",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "another API key of the tenant used this Idempotency-Key; or the answer stored under it was stored unsealed before the 2026-10-28 deploys and has not expired"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding (for example %E0%A4%A): Express cannot decode it, so the global handler answers before any route step runs (no authentication); or a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}/reject": {
      "post": {
        "operationId": "rejectPayrollBatch",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Reject a batch awaiting approval",
        "description": "Ends a batch in `awaiting_approval`: the batch and every item become `cancelled` in one transaction, so every item reference is released. Needs `payroll:read` plus the approve capability (owner, admin, approver) but, unlike approve, has NO creator rule: a maker with the approve capability may reject its own waiting batch. The reject wins: a member who approved before the quorum may still reject, and the reject replaces that member's approve vote. Order: `reason` parsed first (400 invalid_reason even for an unknown batch); the batch (404; 409 batch_not_awaiting_approval); the key's payroll_networks; the approver rule. No lifetime check: a waiting batch past its lifetime can still be rejected until the sweep closes it.\n\n**Side effects.** Audit row `payroll_batch.rejected` {reason, cancelled_items, replaced_approval}. Webhooks `payroll_batch.rejected` {reason}, then one `payroll_item.cancelled` per item, then `payroll_batch.closed`. Not rate limited. Idempotency: stored answers replay for 24 h; a repeat without a key answers 409 batch_not_awaiting_approval.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:read",
        "x-fastpay-rate-limited": false,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayrollReasonRequest"
              },
              "examples": {
                "withReason": {
                  "value": {
                    "reason": "Wrong payer wallet for this month"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The batch, `cancelled`, with `closed_at` set and the reject decision in `approvals`.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollBatch"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "Shape B invalid_reason (not a string, or over 500 characters; an all-space reason is stored as null); invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope. Shape B api_key_network_not_allowed, member_required, member_suspended, approval_not_permitted."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing."
          },
          "409": {
            "$ref": "#/components/responses/PayrollConflict",
            "description": "Shape B batch_not_awaiting_approval; the three Idempotency-Key conflicts."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_reason",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "batch_not_awaiting_approval",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "member_required",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "member_suspended",
            "envelope": "B",
            "note": "not reached over HTTP: authentication answers 401 member_suspended first"
          },
          {
            "status": 403,
            "code": "approval_not_permitted",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "another API key of the tenant used this Idempotency-Key; or the answer stored under it was stored unsealed before the 2026-10-28 deploys and has not expired"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding (for example %E0%A4%A): Express cannot decode it, so the global handler answers before any route step runs (no authentication); or a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}/cancel": {
      "post": {
        "operationId": "cancelPayrollBatch",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Cancel a batch (stops new co-signatures; cannot revoke one already issued)",
        "description": "Stops FastPay co-signing anything new for this batch at once. Any key with `payroll:write` of the tenant may cancel ANY batch of the tenant, whoever created it, in any non-terminal status (`awaiting_approval`, `open`, `closing`); no approver capability is needed. Not refused by the kill switch, the lane or a network hold.\n\n**Outcome.** With no holding authorization (issuing, live or flagged) the batch finalises now: pending items never attempted become `cancelled`, attempted ones `failed`, and the batch `cancelled` (nothing completed or failed), `completed` (every item completed) or `closed`. With one or more holding authorizations it becomes `closing`: an authorization already signed CANNOT be revoked and stays executable by the payer wallet until its expiry; whatever it pays before then is recorded as completed; GET intent serves no payload any more (a Bitcoin cancel payload excepted). The settlement sweep finalises the batch once each holding authorization settles, is proven expired or is resolved by FastPay. `closes_at` is the latest `expires_at` among the holding authorizations at the time of the call: a FLAGGED authorization never expires by itself, so with one the batch stays `closing` past `closes_at` until FastPay resolves it. A cancel during an intent's signing pass makes that intent go nowhere (no row goes live). A repeat on a `closing` batch answers 200 again (the first reason is kept); on a terminal batch 409 payroll_batch_already_closed.\n\n**Side effects.** Audit row `payroll_batch.cancelled` {reason, finalized, status}. When finalised now: webhooks `payroll_item.cancelled` / `payroll_item.failed` per item, then `payroll_batch.closed`. Not rate limited. Idempotency: stored answers replay for 24 h.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:write",
        "x-fastpay-rate-limited": false,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayrollReasonRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The batch after the cancel, plus `closes_at` and `note`.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollBatchCancelResult"
                },
                "examples": {
                  "closing": {
                    "$ref": "#/components/examples/PayrollBatchClosingExample"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "Shape B invalid_reason; invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope (an approver or viewer key). Shape B api_key_network_not_allowed."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing."
          },
          "409": {
            "$ref": "#/components/responses/PayrollConflict",
            "description": "Shape B payroll_batch_already_closed (the batch is completed, closed or cancelled); the three Idempotency-Key conflicts."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_reason",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_batch_already_closed",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "another API key of the tenant used this Idempotency-Key; or the answer stored under it was stored unsealed before the 2026-10-28 deploys and has not expired"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding (for example %E0%A4%A): Express cannot decode it, so the global handler answers before any route step runs (no authentication); or a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}/intents": {
      "post": {
        "operationId": "createPayrollIntent",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Issue an intent (reserve items, co-sign with the fund-free authorizer, serve unsigned payloads)",
        "description": "The console's **Pay**. Reserves the batch's next pending items (position order), has FastPay's fund-free payment authorizer co-sign exactly those items, amounts and the expiry, and returns the UNSIGNED payloads the payer wallet signs and broadcasts itself. FastPay holds no key over the payer's funds: each payload can be executed only by the payer wallet, only until `expires_at`, and FastPay cannot revoke a signature it handed out. No approval, approver or member-count check is made here: the status gate is `open`.\n\n**Gate order**: kill switch (503 service_paused); body; tenant active (403 tenant_not_active; a suspended tenant's key is normally refused earlier at authentication with 401 invalid_api_key); the batch (404); the key's payroll_networks (403 api_key_network_not_allowed); batch `open` (409 payroll_batch_not_open) and inside its lifetime (409 payroll_batch_expired); its payer wallet `verified` (409 payer_wallet_not_verified); `valid_for_seconds` within the payer kind's maximum; the network gate (live: switches, the tenant lane and holds, config, deployment, rotation); the restore fence; a FRESH re-read of the payer wallet's control (a change sends the wallet back to `pending_verification`, answers 409 payer_wallet_reverification_required and fires `payroll_payer_wallet.reverification_required`); I3, one holding authorization per (network, payer, token, mode) (409 payroll_intent_already_live; live it spans tenants and details.intent_id is null for another tenant's intent); a fee-mode network's debt gate (Bitcoin); fee mode (Bitcoin; any other network refuses `fee_quote_id` with 409 payroll_fee_mode_mismatch); `chain_options` (the adapter's own rule); the pending items in position order (409 no_pending_items), on Bitcoin trimmed to the value caps (in_tx_debt) or refused whole (prepaid: 409 payroll_value_cap_reached); chunk planning within the authorizations-per-intent limit (default 10) and the signatures-per-intent limit (default 100, production 7); one chain quote (paused disburser, allowlist, decimals, authorizer, fee, balances, recipients); the expiry: min(batch expires_at, now + valid_for_seconds), refused below 300 s; the tenant's lane once more right before the reserve; on a fee-mode network (Bitcoin) the value caps once more, never trimming (409 payroll_value_cap_reached); the reserve in one transaction; signing, with the lane re-read after every signature (a pause or hold mid-pass voids the intent and answers the lane's own 503 payroll_network_not_enabled); the signed bytes' preflight where the chain simulates (Solana: 409 invalid_recipient_state or invalid_payer_state); then every row goes live together, or none does. Any failure before live voids every issuing row and releases its items (I6: no signature of an issuing row ever left the process). The document is then built from the STORED rows exactly as GET builds it.\n\n**Solana in production**: `single` mode (one per-entry authorization signature per item), so at most 7 items per intent: a batch of N items takes ceil(N/7) intents, one after another (I3). `chain_options.mode` `batch` is refused with 503 payroll_network_not_configured while the Solana payroll mode is `single`. Each Solana payload carries a fresh blockhash that expires in about a minute: sign and send at once, or GET the intent again for fresh bytes.\n\n**A 5xx is not proof that nothing was issued.** After the rows went live, the document build can still rethrow a typed refusal. After any 5xx, list the batch's intents before concluding.\n\n**Side effects.** Audit row `payroll_authorization.issued` per live authorization {intent_id, chunk_index, network, deployment_key, auth_scheme, entry_match, fee_bps, expiry, item_ids, preimages, signing_wait_ms?}. Possible webhooks: `payroll_payer_wallet.reverification_required` (control change), `payroll_fee_settlement.paid` (Bitcoin prepaid). Items move `pending` -> `authorized`.\n\n**Idempotency.** 5xx never stored: a retry under the same key runs again, and a second live intent is refused by I3. A 2xx replay is the STORED snapshot (not refreshed): its payloads may be stale, expired or cancelled: always GET the intent before signing. A repeat while the first still runs (up to the 90 s signing deadline): 409 idempotency_key_in_progress.\n\nI3 is per tenant in the sandbox. Live refusals that need a relay setting are not simulated: the sandbox plans Solana `chain_options.mode: batch` whatever the Solana payroll mode says, while production answers 503 payroll_network_not_configured for it, so a rehearsal must use the default (`single`) mode.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:write",
        "x-fastpay-rate-limited": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayrollIntentCreateRequest"
              },
              "examples": {
                "default": {
                  "summary": "Defaults (1 h validity, as many items as the caps allow)",
                  "value": {}
                },
                "shortWindow": {
                  "value": {
                    "valid_for_seconds": 900,
                    "max_items": 5
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The payroll_intent document (the same shape GET returns, plus `balances` and `payer_balance` from this call's quote).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollIntent"
                },
                "examples": {
                  "solanaSingle": {
                    "$ref": "#/components/examples/PayrollIntentSolanaExample"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "Shape B invalid_request (body not an object), invalid_validity (details {min, max}, or the batch closes within 300 s), invalid_max_items, invalid_chain_options, invalid_fee_quote_id; invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PayrollPaymentRequired",
            "description": "Shape B payroll_fee_prepayment_required (Bitcoin stacks_prepaid tenants only)."
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope. Shape B tenant_not_active, api_key_network_not_allowed."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing (the batch, or the fee quote named by fee_quote_id)."
          },
          "409": {
            "$ref": "#/components/responses/PayrollConflict",
            "description": "Shape B, see x-fastpay-error-codes: the batch state, the payer wallet, I3, the quote's refusals (invalid_recipient_state with details.items [{item_id, reason}]: cancel and rebuild without them; insufficient_payer_balance with details {asset, required, balance}; invalid_payer_state with details.cause payer_account|fees|rent|token_balance), the reserve's re-checks, and the Idempotency-Key conflicts. A 409 invalid_chain_options comes from Stacks' refused `fee_payment: sponsored`."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "429": {
            "$ref": "#/components/responses/PayrollRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          },
          "503": {
            "$ref": "#/components/responses/PayrollUnavailable",
            "description": "Shape B, never stored for replay. Retry with the same key after Retry-After for payroll_signing_busy / payroll_signing_rate_limited; retry shortly for chain_unavailable and payroll_signing_unavailable unless details.retryable is false; never retry payroll_restore_reconciliation_pending or payroll_deployment_not_audited (STOP, sign nothing). List the batch's intents after any 5xx."
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 503,
            "code": "service_paused",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "also a path parameter that is not valid percent-encoding, answered before any route step; and a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 400,
            "code": "invalid_validity",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_max_items",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_chain_options",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "invalid_chain_options",
            "envelope": "B",
            "family": "stacks"
          },
          {
            "status": 400,
            "code": "invalid_fee_quote_id",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "tenant_not_active",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_batch_not_open",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_batch_expired",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payer_wallet_not_verified",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_network_not_enabled",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_network_not_configured",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_deployment_rotation_pending",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_restore_reconciliation_pending",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payer_wallet_reverification_required",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payer_wallet_delegated",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payer_wallet_is_fastpay_key",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payer_wallet_kind_unsupported",
            "envelope": "B",
            "family": "evm"
          },
          {
            "status": 503,
            "code": "payer_verification_unavailable",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "chain_unavailable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_intent_already_live",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_fee_outstanding",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 409,
            "code": "payroll_fee_mode_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_fee_mode_unavailable",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 402,
            "code": "payroll_fee_prepayment_required",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 409,
            "code": "payroll_fee_prepayment_pending",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 409,
            "code": "payroll_fee_settlement_anomaly",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 409,
            "code": "payroll_prepaid_plan_changed",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 409,
            "code": "payroll_value_cap_reached",
            "envelope": "B",
            "family": "bitcoin",
            "when": "the first check finds no item that fits (in_tx_debt trims to the cap first), or the re-check right before the reserve finds the intent no longer fits (never trims: a race with another intent refuses this one, nothing reserved or signed)"
          },
          {
            "status": 409,
            "code": "no_pending_items",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_signature_cap",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "invalid_recipient_state",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "insufficient_payer_balance",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "invalid_payer_state",
            "envelope": "B",
            "family": "solana"
          },
          {
            "status": 409,
            "code": "payer_token_account_unusable",
            "envelope": "B",
            "family": "solana"
          },
          {
            "status": 409,
            "code": "currency_not_allowlisted",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_disburser_paused",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payment_authorizer_mismatch",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payment_authorizer_not_fund_free",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "token_decimals_mismatch",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "authorizer_cluster_mismatch",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "authorizer_network_mismatch",
            "envelope": "B",
            "family": "aptos, sui, ton"
          },
          {
            "status": 503,
            "code": "payroll_deployment_not_audited",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_token_not_ready",
            "envelope": "B",
            "family": "evm, tron"
          },
          {
            "status": 409,
            "code": "payroll_ton_capacity",
            "envelope": "B",
            "family": "ton"
          },
          {
            "status": 409,
            "code": "payroll_fee_changed",
            "envelope": "B",
            "family": "aptos, ton"
          },
          {
            "status": 409,
            "code": "payroll_expiry_too_far",
            "envelope": "B",
            "family": "ton"
          },
          {
            "status": 503,
            "code": "payroll_disburser_version_mismatch",
            "envelope": "B",
            "family": "sui"
          },
          {
            "status": 409,
            "code": "payroll_authorization_expiring",
            "envelope": "B",
            "family": "tron"
          },
          {
            "status": 409,
            "code": "payroll_fee_address_quota",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 503,
            "code": "payroll_fee_address_unavailable",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 409,
            "code": "payroll_transaction_too_large",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 503,
            "code": "payroll_psbt_refused",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 409,
            "code": "payer_multisig_not_enabled",
            "envelope": "B",
            "family": "bitcoin",
            "when": "the batch payer wallet is a P2WSH vault while multisig payer support is off; nothing reserved"
          },
          {
            "status": 409,
            "code": "invalid_items",
            "envelope": "B",
            "family": "tron",
            "when": "one transaction would carry more items than the Tron items-per-transaction limit, or need a fee limit above its SUN cap"
          },
          {
            "status": 503,
            "code": "payroll_signing_busy",
            "envelope": "B",
            "retry_after": true
          },
          {
            "status": 503,
            "code": "payroll_signing_rate_limited",
            "envelope": "B",
            "retry_after": true
          },
          {
            "status": 503,
            "code": "payroll_signing_unavailable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_items_not_pending",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_chain_ref_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_fee_quote_open",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 409,
            "code": "payroll_authorization_flagged",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "deployment_not_current",
            "envelope": "B",
            "when": "an internal guard: a current-only adapter method reached a read-only (retired) deployment"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "another API key of the tenant used this Idempotency-Key; or the answer stored under it was stored unsealed before the 2026-10-28 deploys and has not expired"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      },
      "get": {
        "operationId": "listPayrollIntents",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "List a batch's intents (no payloads)",
        "description": "One page of the batch's intents, newest first, each with its authorizations in chunk order, their status and items, but NO payloads (that would be a chain call per authorization): GET the intent by id for them. Voided, expired and settled intents stay listed as the audit trail. `limit` and the cursors only (cursor = an intent id); no status or created filter. The query is parsed BEFORE the batch is loaded (a bad limit is 400 even for an unknown batch). Read-only, not limited by payroll_networks, not rate limited. Use it after any 5xx on POST.../intents to see whether an intent went live.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:read",
        "x-fastpay-rate-limited": false,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          }
        ],
        "responses": {
          "200": {
            "description": "A list of intent summaries.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollIntentList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "Shape B invalid_pagination_parameter; invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing (the batch)."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding (for example %E0%A4%A): Express cannot decode it, so the global handler answers before any route step runs (no authentication); or a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}/intents/{intentId}": {
      "get": {
        "operationId": "getPayrollIntent",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Retrieve one intent with its payloads (re-served from the stored rows; never signs)",
        "description": "The payroll_intent document rebuilt from the STORED authorization rows. It never signs. Rules: any flagged authorization of the intent is 409 payroll_authorization_flagged and nothing of it is served (do not resend any payload of it); payloads only for `live` authorizations on the CURRENT deployment and only for groups that still hold an `authorized` item; an authorization on a retired deployment shows status `deployment_retired` with no payload; a group a transaction still confirming has spent is withheld; once the batch's cancel was requested no payload and no pre-step is served (a warning says why; a Bitcoin `cancel_payload` still is). `pre_steps` and `allowance.required` (exact-allowance families: EVM, Tron) count only what is still left to pay.\n\n**Each GET rebuilds the payloads from the chain** (Solana: a fresh blockhash and the program pin each time): a node failure answers 503 chain_unavailable (the POST would have answered a warning instead), and a typed refusal of the build (for example 503 payroll_deployment_not_audited after a program upgrade) passes. A served predictable tx ref is recorded for the sweep to watch, and a Bitcoin cancel payload version may be stored: these are the only writes. No audit row; not rate limited; not limited by payroll_networks. The sandbox adapter builds synthetic payloads.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:read",
        "x-fastpay-rate-limited": false,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/PayrollIntentIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "The intent document (without `balances` / `payer_balance`, which only the POST's quote fills).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollIntent"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing (the batch, or no intent with that id on this batch)."
          },
          "409": {
            "$ref": "#/components/responses/PayrollConflict",
            "description": "Shape B payroll_authorization_flagged (details.authorizations [{id, flag_kind, flagged_reason}], details.note); Tron payroll_authorization_expiring."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          },
          "503": {
            "$ref": "#/components/responses/PayrollUnavailable",
            "description": "Shape B chain_unavailable; or a typed refusal of the payload build (payroll_deployment_not_audited, payroll_network_not_configured, payment_authorizer_mismatch)."
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_authorization_flagged",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "chain_unavailable",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_deployment_not_audited",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_network_not_configured",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payment_authorizer_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_authorization_expiring",
            "envelope": "B",
            "family": "tron",
            "when": "the authorization expires too soon after the Tron head block to serve a transaction that could still be signed and sent"
          },
          {
            "status": 500,
            "code": "deployment_not_current",
            "envelope": "B",
            "when": "an internal guard: a current-only adapter method reached a read-only (retired) deployment"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding (for example %E0%A4%A): Express cannot decode it, so the global handler answers before any route step runs (no authentication); or a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}/intents/{intentId}/requote": {
      "post": {
        "operationId": "requotePayrollIntent",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Bitcoin only - a new payload version over the same reserved inputs at another fee rate",
        "description": "Bitcoin (authScheme `none`: FastPay co-signs nothing) only: builds a new PSBT version over the IDENTICAL reserved inputs at `fee_rate_sat_vb`, the difference coming out of change, persisted before it is served. Order: kill switch; body (400 invalid_fee_rate unless a positive number at most 100000); the batch (404); the key's payroll_networks; the intent (404; 409 flagged); 409 payroll_requote_unavailable when the intent is not a single live `none` row on the current deployment, a transaction of it was already seen on chain, or it was requoted the requote limit (default 20) times; then the adapter's own fee-rate bounds (default 2 to 500 sat/vB). Every other network answers 409 payroll_requote_unavailable for a valid body on a real intent (a missing fee rate is 400 first, an unknown intent 404). It checks neither the tenant's lane nor the batch's cancel request: on a cancelled batch it still stores and audits a version whose payloads the answer then withholds. Audit row `payroll_authorization.requoted` {version, fee_rate_sat_vb, unsigned_txid}. Its 5xx IS stored for replay (only create, intents and fee-quotes are rerunnable). Bitcoin payroll is off in production.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:write",
        "x-fastpay-rate-limited": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/PayrollIntentIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayrollRequoteRequest"
              },
              "examples": {
                "faster": {
                  "value": {
                    "fee_rate_sat_vb": 12.5
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The intent document, serving the new payload version.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollIntent"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "Shape B invalid_request, invalid_fee_rate, invalid_address (an output not payable); invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope. Shape B api_key_network_not_allowed."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing (batch or intent)."
          },
          "409": {
            "$ref": "#/components/responses/PayrollConflict",
            "description": "Shape B payroll_requote_unavailable (details.max_requotes when the limit is reached), payroll_requote_insufficient_change, payroll_transaction_too_large, payroll_authorization_flagged; the Idempotency-Key conflicts."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "429": {
            "$ref": "#/components/responses/PayrollRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          },
          "503": {
            "$ref": "#/components/responses/PayrollUnavailable",
            "description": "Shape B service_paused, chain_unavailable, payroll_psbt_refused. Stored for replay under the same key."
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 503,
            "code": "service_paused",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "also a path parameter that is not valid percent-encoding, answered before any route step; and a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 400,
            "code": "invalid_fee_rate",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_authorization_flagged",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_requote_unavailable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_requote_insufficient_change",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 503,
            "code": "payroll_psbt_refused",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 409,
            "code": "payroll_transaction_too_large",
            "envelope": "B",
            "family": "bitcoin",
            "when": "the new version would exceed the input, size or PSBT limits (details.reason)"
          },
          {
            "status": 400,
            "code": "invalid_address",
            "envelope": "B",
            "family": "bitcoin",
            "when": "an output address is not payable on the network"
          },
          {
            "status": 503,
            "code": "chain_unavailable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "another API key of the tenant used this Idempotency-Key; or the answer stored under it was stored unsealed before the 2026-10-28 deploys and has not expired"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}/fee-quotes": {
      "post": {
        "operationId": "createPayrollFeeQuote",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Bitcoin stacks_prepaid only - quote the fee of the batch's next intent (nothing reserved)",
        "description": "For a tenant whose Bitcoin fee mode is `stacks_prepaid`: runs the intent's own gates and plan for ONE authorization, reserves nothing, and stores a `quoted` settlement whose Stacks `call` the payer pays in sBTC; then POST.../intents with its `fee_quote_id`. Order: kill switch; body; the intent gates (tenant active, batch, payroll_networks, open, lifetime, payer verified); the network gate; the restore fence; 409 payroll_fee_mode_mismatch when the network has no fee quotes (every network but Bitcoin); the adapter's `chain_options` rule; 409 payroll_fee_mode_unavailable when a prepaid fee is impossible; 409 payroll_fee_mode_mismatch when the tenant pays in_tx_debt; the payer gates (control re-check, I3, debt); 409 payroll_fee_quote_open (one open quote per batch); the plan (409 no_pending_items; Bitcoin value caps; 503 payroll_signature_cap) and the same quote as an intent. Audit row `payroll_fee_settlement.quoted`. Its 5xx is never stored. The quote expires after the quote lifetime (default 1 h). Bitcoin payroll is off in production: on Solana this answers 409 payroll_fee_mode_mismatch once the earlier gates pass (live: the lane must be enabled first).",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:write",
        "x-fastpay-rate-limited": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayrollFeeQuoteCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The payroll_fee_quote, with `required_confirmations` and `balances`.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollFeeQuoteCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "Shape B invalid_request (body, or a blank or over-200-character stacks_payer), invalid_max_items, invalid_chain_options; invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope. Shape B tenant_not_active, api_key_network_not_allowed."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing (the batch)."
          },
          "409": {
            "$ref": "#/components/responses/PayrollConflict",
            "description": "Shape B, see x-fastpay-error-codes."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "429": {
            "$ref": "#/components/responses/PayrollRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          },
          "503": {
            "$ref": "#/components/responses/PayrollUnavailable",
            "description": "Shape B, never stored for replay - service_paused, the gate's 503s, payroll_restore_reconciliation_pending, payer_verification_unavailable, chain_unavailable, payroll_signature_cap, the quote's 503s."
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 503,
            "code": "service_paused",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "also a path parameter that is not valid percent-encoding, answered before any route step; and a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 400,
            "code": "invalid_max_items",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_chain_options",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "tenant_not_active",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_batch_not_open",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_batch_expired",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payer_wallet_not_verified",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_network_not_enabled",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_network_not_configured",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_deployment_rotation_pending",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_restore_reconciliation_pending",
            "envelope": "B",
            "when": "checked before the plan and again before the quote"
          },
          {
            "status": 409,
            "code": "payroll_fee_mode_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_fee_mode_unavailable",
            "envelope": "B",
            "family": "bitcoin",
            "when": "the network has no prepaid fee mode or may split an intent; or the fee-settlement verifier is unavailable, misconfigured or paired with another chain (through)"
          },
          {
            "status": 409,
            "code": "payer_wallet_reverification_required",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payer_wallet_delegated",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payer_wallet_is_fastpay_key",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payer_verification_unavailable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_intent_already_live",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_fee_outstanding",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 409,
            "code": "payroll_fee_quote_open",
            "envelope": "B",
            "family": "bitcoin",
            "when": "the batch already has a quoted, paid or issuing quote (details.fee_quote_id); or another quote of the batch was stored meanwhile (details.fee_quote_id names it, or null)"
          },
          {
            "status": 409,
            "code": "no_pending_items",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_value_cap_reached",
            "envelope": "B",
            "family": "bitcoin"
          },
          {
            "status": 503,
            "code": "payroll_signature_cap",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "invalid_recipient_state",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "insufficient_payer_balance",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payroll_transaction_too_large",
            "envelope": "B",
            "family": "bitcoin",
            "when": "the quoted items do not fit one transaction within the input, size or PSBT limits (details.reason)"
          },
          {
            "status": 409,
            "code": "payer_multisig_not_enabled",
            "envelope": "B",
            "family": "bitcoin",
            "when": "the batch payer wallet is a P2WSH vault while multisig payer support is off"
          },
          {
            "status": 503,
            "code": "chain_unavailable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "another API key of the tenant used this Idempotency-Key; or the answer stored under it was stored unsealed before the 2026-10-28 deploys and has not expired"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}/fee-quotes/{feeQuoteId}": {
      "get": {
        "operationId": "getPayrollFeeQuote",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Retrieve one fee quote",
        "description": "One fee quote of this batch, tenant and key mode. A malformed id (not `pfs_` + 32 hex) is 404 before the batch is read. Read-only; not rate limited; no audit row.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:read",
        "x-fastpay-rate-limited": false,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/PayrollFeeQuoteIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "The fee quote.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollFeeQuote"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing (fee quote or batch)."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding (for example %E0%A4%A): Express cannot decode it, so the global handler answers before any route step runs (no authentication); or a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}/confirm": {
      "post": {
        "operationId": "confirmPayrollBatchTransaction",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Report the payer's broadcast transaction",
        "description": "The payer reports a transaction it broadcast (`tx_hash`, or a TON `boc`); the relay reads the chain and records what actually happened. A report is never the source of truth and never makes anything failed by itself: the same settleFromTx the 30 s settlement sweep runs decides, and the sweep finds payments nobody reported. A transaction the chain does not know yet answers `pending` and is remembered for the sweep. A repeated or racing report sends each webhook once. A tx_hash the batch's items already record answers `applied` from the store with no chain read, and so does one the sweep records while this report reads the chain. Works while the kill switch is on, is NOT lane-gated and is NOT limited by payroll_networks: it only records what already happened on chain, so it never strands an issued authorization. No tenant-active or member check. Order: the batch (404); the adapter that parses the report (live: the first holding authorization's, else the network's current one; a build failure is 503 chain_unavailable, none at all 503 payroll_network_not_configured); the report's format (400 invalid_tx_ref / invalid_report), before any settlement read of the chain.\n\n**Side effects.** Audit row `payroll_batch.tx_reported` {report_kind, tx_ref, status, authorization_ids} on every call, plus the verdicts' own rows. Webhooks from the verdict (item completed, attempt_failed, released; authorization fee_shortfall, flagged, fee_overcharge; batch closed when its last item completes).\n\n**Idempotency.** Its answers, a 503 included, ARE stored for 24 h: send no Idempotency-Key, or a new one per report; never resend a report under the key of one that answered 503. Read progress from the items, not from this answer.\n\n**Sandbox.** The outcome comes from the reported ref: a ref made of one repeated sentinel byte picks it (0xff every entry failed, 0xee not known yet, 0xdd first entry failed, 0xcc suspect -> flagged, 0xbb reverted, 0xaa node error -> 503); any other well-formed ref pays the next unpaid group; an item whose reference ends in `_fail` fails. Finality needs 0 confirmations.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:write",
        "x-fastpay-rate-limited": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayrollConfirmRequest"
              },
              "examples": {
                "solana": {
                  "value": {
                    "tx_hash": "5Examp1eSo1anaSignature1111111111111111111111111111111111111111111111111111111111111111"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The payroll_confirmation document.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollConfirmation"
                },
                "examples": {
                  "applied": {
                    "$ref": "#/components/examples/PayrollConfirmationAppliedExample"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "Shape B invalid_report (no tx_hash, or for TON no tx_hash nor boc, or a malformed boc), invalid_tx_ref (not this network's transaction reference); invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope (approver or viewer keys)."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing (the batch)."
          },
          "409": {
            "$ref": "#/components/responses/PayrollConflict",
            "description": "The Idempotency-Key conflicts."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "429": {
            "$ref": "#/components/responses/PayrollRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          },
          "503": {
            "$ref": "#/components/responses/PayrollUnavailable",
            "description": "Shape B chain_unavailable (the adapter could not be built or the chain read failed), payroll_network_not_configured (no adapter at all for the network). Stored for replay under the same key."
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "payroll_network_not_configured",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "chain_unavailable",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_report",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_tx_ref",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "another API key of the tenant used this Idempotency-Key; or the answer stored under it was stored unsealed before the 2026-10-28 deploys and has not expired"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding (for example %E0%A4%A): Express cannot decode it, so the global handler answers before any route step runs (no authentication); or a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/payroll-batches/{id}/sponsor": {
      "post": {
        "operationId": "sponsorPayrollBatchTransaction",
        "tags": [
          "Payroll batches and intents"
        ],
        "summary": "Stacks only - FastPay adds its fee-payer (SIP-005) signature and broadcasts",
        "description": "Fee sponsorship: FastPay's sponsor key adds its SIP-005 fee-payer signature to the group transaction the payer wallet signed, pays the STX fee and broadcasts it; tx-sender stays the payer and FastPay gains no authority over its funds. Order: the batch (404); the key's payroll_networks; a network with no sponsorship (every network but Stacks) or a closed Stacks sponsorship: 409 sponsorship_unavailable, or 503 sponsorship_screening_unavailable when only sanctions screening is missing; a sandbox key 409 sponsorship_unavailable; the kill switch (503 service_paused); tenant active; the body; the authorization (live, unexpired with margin, issued with `chain_options.fee_payment: sponsored`); the signed transaction byte-checked against the group; sanctions screening of the payer and recipients (fails closed); the payer's nonce; a preflight; the sponsor's float; the nonce lease with per-tenant and global caps; signing; broadcast; then the same confirm as POST.../confirm. Audit rows `payroll_sponsorship.signed` and `.broadcast`, `payroll_sponsorship.screening_refused` on a hit. Not lane-gated: a lane pause leaves the sponsor as it is until the Stacks flip commit adds its lane check. Its 503s are stored for replay on purpose: a sponsored transaction may have been broadcast. Stacks payroll is off in production: every batch there answers 409 sponsorship_unavailable.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "payroll:write",
        "x-fastpay-rate-limited": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/PayrollBatchIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayrollSponsorRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The payroll_sponsorship document (status `broadcast`), with the confirm document or null.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollSponsorship"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PayrollBadRequest",
            "description": "Shape B invalid_request (authorization_id, group_index, signed_tx, or a group the authorization lacks), sponsorship_tx_invalid; invalid_json. Shape B invalid_request also answers a path parameter that is not valid percent-encoding."
          },
          "401": {
            "$ref": "#/components/responses/PayrollUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PayrollForbidden",
            "description": "Shape A sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope. Shape B api_key_network_not_allowed, tenant_not_active, sponsorship_screening_refused."
          },
          "404": {
            "$ref": "#/components/responses/PayrollNotFound",
            "description": "Shape B resource_missing (the batch, or no such authorization on it)."
          },
          "409": {
            "$ref": "#/components/responses/PayrollConflict",
            "description": "Shape B, see x-fastpay-error-codes."
          },
          "413": {
            "$ref": "#/components/responses/PayrollPayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/PayrollUnsupportedMediaType"
          },
          "429": {
            "$ref": "#/components/responses/PayrollRateLimited",
            "description": "Shape A too_many_requests (the tenant limiter), or Shape B sponsorship_daily_cap. Neither is stored for replay."
          },
          "500": {
            "$ref": "#/components/responses/PayrollInternalError"
          },
          "503": {
            "$ref": "#/components/responses/PayrollUnavailable",
            "description": "Shape B; stored for replay under the same key. sponsorship_broadcast_unconfirmed (details.tx_hash): watch that hash, do not sign another transaction for the group."
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "api_key_network_not_allowed",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "sponsorship_unavailable",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "sponsorship_screening_unavailable",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "service_paused",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "tenant_not_active",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "also a path parameter that is not valid percent-encoding, answered before any route step; and a 400 from the body parser itself (an aborted upload, or a body whose length does not match its Content-Length), answered before authentication with request_id null"
          },
          {
            "status": 409,
            "code": "sponsorship_not_live",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "sponsorship_not_requested",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payer_wallet_not_verified",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "sponsorship_tx_invalid",
            "envelope": "B"
          },
          {
            "status": 403,
            "code": "sponsorship_screening_refused",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "sponsorship_origin_nonce_used",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "sponsorship_origin_nonce_future",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "sponsorship_origin_nonce_conflict",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "sponsorship_preflight_refused",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "sponsorship_fee_above_cap",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "sponsorship_capacity",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "sponsorship_pending",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "sponsorship_daily_cap",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "sponsorship_signing_unavailable",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "sponsorship_broadcast_unconfirmed",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "sponsorship_broadcast_rejected",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "chain_unavailable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "another API key of the tenant used this Idempotency-Key; or the answer stored under it was stored unsealed before the 2026-10-28 deploys and has not expired"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a JSON content-type whose charset does not start with utf- (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate: the body parser answers before every other check, with request_id null and no X-Request-ID header"
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ]
      }
    },
    "/v1/webhook-deliveries": {
      "get": {
        "operationId": "listWebhookDeliveries",
        "tags": [
          "Webhooks"
        ],
        "summary": "List the tenant's webhook deliveries (the outbox log) in the key's mode",
        "description": "One row per delivery target per event: the object's own callback_url delivery and one row per subscribed account endpoint, read. Only the key's mode: a fp_test_ key lists sandbox rows, a fp_live_ key live rows. Newest first by the outbox's INTEGER id, which is also the cursor: a non-integer starting_after or ending_before is ignored and the first page answers; starting_after=0 or a negative integer answers an empty page; an integer that matches no row (one pruned by retention included) still pages from that position. Filter: allocation_id only (exact, up to 256 characters); no status, created, webhook_endpoint_id or delivered filter, and any other query parameter is ignored. No payload on list rows. A superseded row (closed unsent: its endpoint was deleted or disabled, or a later payment.* event already reached the same target) reads delivered true with the reason in last_error; superseded_at is not exposed. Delivered and superseded rows are deleted the retention period (90) days after delivered_at; pending rows are never pruned. A sandbox reset deletes the sandbox rows. The operator's test events are never recorded. No audit row, no rate limiter.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter",
            "description": "The integer id (as text) of the previous page's last delivery. Non-integer: ignored (first page). 0 or negative: an empty page. An id pruned by retention still pages from its position."
          },
          {
            "$ref": "#/components/parameters/EndingBefore",
            "description": "The integer id of the previous page's first delivery; answers the newer rows, still newest first. Non-integer: ignored, the first page answers. 0 or a negative integer: the OLDEST page (the rows above it, read ascending and reversed)."
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryAllocationIdQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "One page of deliveries.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryList"
                },
                "examples": {
                  "page": {
                    "$ref": "#/components/examples/WebhookDeliveryListExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- invalid_json (Shape B): A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header. \n- invalid_request (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header. \n- invalid_pagination_parameter (Shape B): limit is not an integer from 1 to 100, or both starting_after and ending_before are sent. \n- invalid_filter_parameter (Shape B): allocation_id is repeated, bracketed or longer than 256 characters: \"allocation_id must be a single id string of at most 256 characters.\"",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "badLimit": {
                    "$ref": "#/components/examples/ErrInvalidPaginationExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/WebhooksTenantKeyUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WebhooksTenantKeyForbidden"
          },
          "413": {
            "$ref": "#/components/responses/WebhooksBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/WebhooksUnsupportedMediaType"
          },
          "500": {
            "description": "- internal_error (Shape B): An untyped failure (a database error), masked; error.param null and error.request_id set.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "$ref": "#/components/examples/ErrInternalExample"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant (an expired key and a suspended member's key are named first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended; error_type authentication_error."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a host listed in sandbox-only."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, holds neither webhooks:read, webhooks:write nor *. A path sent with capital letters is classified in lower case."
          },
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B",
            "when": "limit is not an integer from 1 to 100, or both starting_after and ending_before are sent."
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B",
            "when": "allocation_id is repeated, bracketed or longer than 256 characters: \"allocation_id must be a single id string of at most 256 characters.\""
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a database error), masked; error.param null and error.request_id set."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "webhooks:read"
      }
    },
    "/v1/webhook-deliveries/{id}": {
      "get": {
        "operationId": "retrieveWebhookDelivery",
        "tags": [
          "Webhooks"
        ],
        "summary": "Retrieve one webhook delivery, with the payload that was sent",
        "description": "The calling tenant's delivery row in the key's mode, with payload (the JSON body as recorded, its event_id included). A non-integer id, an unknown id, another tenant's row and the other mode's row all answer the same Shape B 404 resource_missing \"No such webhook delivery.\". The id goes through Number, so any text Number reads as an integer (\"1e3\", \"0x10\", \"1.0\") names that row. The payload of a checkout, refund, deposit or mass-payout delivery recorded since the 2026-10-28 deploy carries is_test, the object's mode; one recorded before does not (payroll payloads always did). A row whose stored payload does not parse answers 500 internal_error, since the serializer JSON.parses it. Read-only; no audit row; no rate limiter.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "The delivery with its payload.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryWithPayload"
                },
                "examples": {
                  "delivered": {
                    "$ref": "#/components/examples/WebhookDeliveryWithPayloadExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- invalid_json (Shape B): A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header. \n- invalid_request (Shape B): A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/WebhooksTenantKeyUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WebhooksTenantKeyForbidden"
          },
          "404": {
            "description": "- resource_missing (Shape B): Not an integer id, or no delivery of the tenant with this id in the key's mode (deleted by retention, another tenant's or the other mode's included): \"No such webhook delivery.\"",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing": {
                    "$ref": "#/components/examples/WebhookDeliveryMissingExample"
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/WebhooksBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/WebhooksUnsupportedMediaType"
          },
          "500": {
            "description": "- internal_error (Shape B): A row whose stored payload does not parse (the serializer JSON.parses it), or another untyped failure, masked.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "$ref": "#/components/examples/ErrInternalExample"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header)."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant (an expired key and a suspended member's key are named first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended; error_type authentication_error."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a host listed in sandbox-only."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, holds neither webhooks:read, webhooks:write nor *. A path sent with capital letters is classified in lower case."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "Not an integer id, or no delivery of the tenant with this id in the key's mode (deleted by retention, another tenant's or the other mode's included): \"No such webhook delivery.\""
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "A row whose stored payload does not parse (the serializer JSON.parses it), or another untyped failure, masked."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "webhooks:read"
      }
    },
    "/v1/webhook-deliveries/{id}/redeliver": {
      "post": {
        "operationId": "redeliverWebhookDelivery",
        "tags": [
          "Webhooks"
        ],
        "summary": "Send one recorded delivery again now, and answer with the row as it then reads",
        "description": "Claims the row's lease: a row whose lease is still running - its first attempt (leased 'inline' for its retry budget), the periodic sweep, or another redeliver - answers 409 webhook_delivery_redelivery_in_progress. An already delivered row can be redelivered. Then the sweep's own sequence: parse the stored payload; re-check that the row's recorded callback_url (not the endpoint's current url) still resolves only to public addresses (the SSRF check only: an http url stored before 2026-10-28 is still sent to); for an endpoint row, read the endpoint and sign with its current (and, in a rotation overlap, previous) secret; for a per-object row, the tenant's; send with a fresh x-webhook-timestamp and signature, the same body bytes (the same id and event_id), up to the attempt limit (5) in-process POSTs. x-webhook-secret rides only on a live per-object row whose stored body does not say is_test true, read from the row, never from the calling key: never on an endpoint row or a sandbox row. Each attempt waits at most the delivery timeout (15 s) for the status line and reads at most 64 KiB of the answer within the same 15 s; the status line alone decides. The call answers only after that: up to about 98 s at the default settings (about 120 s if every answer is a slow 429), plus any wait for one of the 20 process-wide delivery slots. It answers 200 with the row as it now reads whatever the send did, with two exceptions that answer 500 internal_error: a row whose stored payload does not parse (the sweep step returns without touching the row, so the 10-minute lease is kept, and the serializer throws), and a row deleted while the round ran (a sandbox reset of a sandbox row, or the retention prune), which reads back as null. Success is delivered_at later than the call and a 2xx response_status. 200 without a send: the endpoint was deleted or disabled (an undelivered row is closed as superseded and reads delivered true; an already delivered row is left as it was and keeps the 10-minute lease, so a repeat within the lease window answers 409); the recorded URL no longer resolves publicly (checked BEFORE the endpoint is read: attempts + 1, last_error set, response_status and response_body cleared); no signing secret is available (a tenant with no secret and no a platform-wide signing secret: a failed attempt). A failed send records attempts + 1, last_error, response_status, response_body and next_attempt_at, and leaves delivered_at as it was, so a delivered row still reads delivered true; a success stamps delivered_at again (its 90-day retention restarts) and clears last_error only when it was a supersede text. The payment.* supersede rule is the sweep's only: this route always sends. Sandbox key: sandbox rows only; live key: live rows only. Idempotency-Key (tenant layer, before the row is loaded): a repeat under the same key replays the stored answer, whatever it was - the 200, the 404, the 409 webhook_delivery_redelivery_in_progress or a 500 (this route answers none of the refusals the layer leaves unstored, a 429 or an approval or lane refusal) - for 24 h (24 hours) and sends nothing: to send again, or to retry after a 409, use a new key. The stored answer is sealed under the calling key. No audit row. Needs webhooks:write (or *).",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The row after the attempt (read success from delivered_at and response_status).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryWithPayload"
                },
                "examples": {
                  "redelivered": {
                    "$ref": "#/components/examples/WebhookDeliveryWithPayloadExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- invalid_json (Shape B): A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header. \n- invalid_request (Shape B): A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/WebhooksTenantKeyUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WebhooksTenantKeyForbidden"
          },
          "404": {
            "description": "- resource_missing (Shape B): Not an integer id, or no delivery of the tenant with this id in the key's mode: \"No such webhook delivery.\"",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing": {
                    "$ref": "#/components/examples/WebhookDeliveryMissingExample"
                  }
                }
              }
            }
          },
          "409": {
            "description": "- idempotency_key_in_use (Shape B): The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run. \n- idempotency_parameter_mismatch (Shape B): The same Idempotency-Key with another body or query. \n- idempotency_key_in_progress (Shape B): The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made. \n- webhook_delivery_redelivery_in_progress (Shape B): The row's lease is still running: its first attempt, another redelivery or the periodic sweep holds it, or a no-op redelivery of a delivered row left its 10-minute lease. A holder lets go when its round ends; a lease lasts at most the retry budget for a first attempt (about 5 min at the defaults, counted from its recording and again from when it gets a delivery slot) and the lease window (10 min) for a sweep or redeliver claim. Nothing is sent. Retry later under a new Idempotency-Key: this answer is stored under the key.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "inFlight": {
                    "$ref": "#/components/examples/WebhookRedeliveryInProgressExample"
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/WebhooksBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/WebhooksUnsupportedMediaType"
          },
          "500": {
            "description": "- internal_error (Shape B): A row whose stored payload does not parse, a row deleted while the round ran, or another untyped failure, masked. The round may have been sent: read the delivery back before retrying.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "$ref": "#/components/examples/ErrInternalExample"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header)."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant (an expired key and a suspended member's key are named first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended; error_type authentication_error."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a host listed in sandbox-only."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, holds neither webhooks:write nor *. A path sent with capital letters is classified in lower case."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same Idempotency-Key with another body or query."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "Not an integer id, or no delivery of the tenant with this id in the key's mode: \"No such webhook delivery.\""
          },
          {
            "status": 409,
            "code": "webhook_delivery_redelivery_in_progress",
            "envelope": "B",
            "when": "The row's lease is still running: its first attempt, another redelivery or the periodic sweep holds it, or a no-op redelivery of a delivered row left its 10-minute lease. A holder lets go when its round ends; a lease lasts at most the retry budget for a first attempt (about 5 min at the defaults, counted from its recording and again from when it gets a delivery slot) and the lease window (10 min) for a sweep or redeliver claim. Nothing is sent. Retry later under a new Idempotency-Key: this answer is stored under the key."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "A row whose stored payload does not parse, a row deleted while the round ran, or another untyped failure, masked. The round may have been sent: read the delivery back before retrying."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "webhooks:write"
      }
    },
    "/v1/webhooks/egress-ips": {
      "get": {
        "operationId": "listWebhookEgressIps",
        "tags": [
          "Webhooks"
        ],
        "summary": "The relay's outbound webhook IP addresses, when the operator configured them",
        "description": "the published egress list split on commas and read per call, answered as a list plus configured = at least one address. Unset: {object: list, data: [], has_more: false, configured: false}, never a guessed address. Each entry is trimmed and empty entries are dropped; the values are not validated as IP addresses. A receiver should allow-list FastPay only while this reads configured true. No credential, no rate limiter.",
        "security": [],
        "responses": {
          "200": {
            "description": "The egress list.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEgressIpList"
                },
                "examples": {
                  "unset": {
                    "$ref": "#/components/examples/WebhookEgressIpsUnsetExample"
                  },
                  "set": {
                    "$ref": "#/components/examples/WebhookEgressIpsSetExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- invalid_json (Shape B): A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header. \n- invalid_request (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/WebhooksBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/WebhooksUnsupportedMediaType"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          }
        ],
        "x-fastpay-credential": "public"
      }
    },
    "/v1/webhooks/secret/rotate": {
      "post": {
        "operationId": "rotateTenantWebhookSecret",
        "tags": [
          "Webhooks"
        ],
        "summary": "Rotate the tenant webhook secret that signs per-object callback_url deliveries (live key only)",
        "description": "A live key only: a sandbox (fp_test_) key answers 403 live_key_required, Shape B, error type permission_error, \"This action changes live data: use a live (fp_live_) API key.\", before the body is read and before anything changes. Then expires_in_hours (0-72, default 24; else 400 invalid_expires_in_hours). Replaces the tenant's single webhook_secret with a new whsec_ + 48 hex and returns it once. That secret signs every per-object callback_url delivery of BOTH modes (a payroll batch's, a payment request's, a checkout session's, a refund's underlying session's, a channel's, a job's), never an account endpoint's; it rides in x-webhook-secret only on a live object's callback_url delivery, never on a sandbox one. expires_in_hours keeps the previous secret signing alongside: each delivery's x-webhook-signature then carries two signatures, current first, while x-webhook-secret (live callback deliveries only) carries the current one. 0 drops the previous secret at once. Only one previous secret is kept: a second rotation inside the window replaces it. A first attempt dispatched before the rotation was signed, and its x-webhook-secret chosen, at dispatch, before the wait for a delivery slot: it goes out with the old secret only. On a sandbox-only host no key can call it (a live key gets 403 sandbox_host_requires_test_key first). Audit row webhook_secret.rotated, actor the tenant (member, key and IP), target the tenant, details {expiresInHours, previousSecretExpiresAt}, written after the rotation and outside a transaction. Idempotency-Key (tenant layer): a repeat replays the same secret and writes no audit row; the stored answer is sealed under the calling key. a sandbox key can no longer mint a live key there.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookSecretRotateRequest"
              },
              "examples": {
                "default24h": {
                  "value": {}
                },
                "overlap48h": {
                  "value": {
                    "expires_in_hours": 48
                  }
                },
                "immediate": {
                  "value": {
                    "expires_in_hours": 0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new tenant webhook secret, shown this once.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretRotation"
                },
                "examples": {
                  "rotated": {
                    "$ref": "#/components/examples/WebhookSecretRotationExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- invalid_json (Shape B): A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header. \n- invalid_request (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header. \n- invalid_expires_in_hours (Shape B): expires_in_hours does not read (through Number) as a number from 0 to 72: \"expires_in_hours must be a number between 0 and 72 (0 invalidates the previous secret immediately).\" A numeric string, a fraction, true (1), \"\" and false (0) pass.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "badHours": {
                    "$ref": "#/components/examples/WebhookInvalidExpiresInHoursExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/WebhooksTenantKeyUnauthorized"
          },
          "403": {
            "description": "- sandbox_host_requires_test_key (Shape A): A live key on a host listed in sandbox-only. \n- ip_not_allowed (Shape A): The key's IP allow-list excludes the caller. \n- insufficient_scope (Shape A): The key, narrowed to its member's role ceiling, holds neither webhooks:write nor *. A path sent with capital letters is classified in lower case. \n- live_key_required (Shape B): A sandbox (fp_test_) key: the tenant has one webhook secret for both modes, which signs live callback deliveries too. Answered before the body is read; nothing is rotated. Error type permission_error.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sandboxKey": {
                    "$ref": "#/components/examples/WebhookLiveKeyRequiredExample"
                  },
                  "scope": {
                    "$ref": "#/components/examples/WebhooksInsufficientScopeExample"
                  }
                }
              }
            }
          },
          "409": {
            "description": "- idempotency_key_in_use (Shape B): The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run. \n- idempotency_parameter_mismatch (Shape B): The same Idempotency-Key with another body or query. \n- idempotency_key_in_progress (Shape B): The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/WebhooksBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/WebhooksUnsupportedMediaType"
          },
          "500": {
            "description": "- internal_error (Shape B): An untyped failure, masked. After the rotation (writing its audit row) the secret is replaced and the answer lost; under an Idempotency-Key the 500 is stored and replayed.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "$ref": "#/components/examples/ErrInternalExample"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant (an expired key and a suspended member's key are named first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended; error_type authentication_error."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a host listed in sandbox-only."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, holds neither webhooks:write nor *. A path sent with capital letters is classified in lower case."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same Idempotency-Key with another body or query."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made."
          },
          {
            "status": 403,
            "code": "live_key_required",
            "envelope": "B",
            "when": "A sandbox (fp_test_) key: the tenant has one webhook secret for both modes, which signs live callback deliveries too. Answered before the body is read; nothing is rotated. Error type permission_error."
          },
          {
            "status": 400,
            "code": "invalid_expires_in_hours",
            "envelope": "B",
            "when": "expires_in_hours does not read (through Number) as a number from 0 to 72: \"expires_in_hours must be a number between 0 and 72 (0 invalidates the previous secret immediately).\" A numeric string, a fraction, true (1), \"\" and false (0) pass."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure, masked. After the rotation (writing its audit row) the secret is replaced and the answer lost; under an Idempotency-Key the 500 is stored and replayed."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "webhooks:write"
      }
    },
    "/v1/webhook-endpoints": {
      "post": {
        "operationId": "createWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Create an account-level webhook endpoint in the calling key's mode (secret shown once)",
        "description": "Validates the body in this order, the first failure answering Shape B 400: url - parameter_missing when absent, empty or not a string (1000); then invalid_url when it is over 2048 characters, does not parse, does not start with https:// (any letter case) or carries a username or password (984-994, 1001); then invalid_url when its host is localhost, does not resolve, or resolves to any non-public address, with the SSRF check's own message, which says \"callbackUrl\" (1002); description (invalid_description: not a string or over 500 characters; null stores null; 972, 1005-1007); enabled_events (invalid_enabled_events: not a non-empty array of catalogue types or \"*\"; omitted or null means [\"*\"]; duplicates dropped; any \"*\" collapses to [\"*\"]); status (invalid_status unless enabled or disabled - validated, then IGNORED: an endpoint is always created enabled, 1009-1012); then the cap (webhook_endpoint_limit_reached when the tenant already has the maximum (16) endpoints, BOTH modes counted, 1061-1063). The host must resolve when the call is made (a passing host is cached 2 minutes). No URL uniqueness: a retry without an Idempotency-Key makes another endpoint. Mode = the key's: a fp_test_ key makes a sandbox endpoint that receives sandbox events only, a fp_live_ key a live one. The 201 carries secret (whsec_ + 48 hex), shown here and on rotation only. Deliveries to the endpoint carry x-webhook-timestamp and x-webhook-signature only, never x-webhook-secret. Audit row webhook_endpoint.created, target the endpoint, details {url, enabledEvents, isTest}, written after the insert and outside a transaction; the url is recorded as sent, query string included. Idempotency-Key (tenant layer): a repeat replays the same secret, unaudited, from an answer sealed under the calling key. Every refusal is stored under the key too: after correcting the body, send a new key.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEndpointCreateRequest"
              },
              "examples": {
                "payroll": {
                  "summary": "The 16 payroll types",
                  "value": {
                    "url": "https://hooks.example.com/fastpay/webhooks",
                    "description": "Payroll",
                    "enabled_events": [
                      "payroll_batch.awaiting_approval",
                      "payroll_batch.approved",
                      "payroll_batch.rejected",
                      "payroll_batch.authorization_expired",
                      "payroll_batch.closed",
                      "payroll_item.completed",
                      "payroll_item.attempt_failed",
                      "payroll_item.failed",
                      "payroll_item.cancelled",
                      "payroll_item.released",
                      "payroll_authorization.flagged",
                      "payroll_authorization.fee_overcharge",
                      "payroll_authorization.resolved",
                      "payroll_authorization.fee_shortfall",
                      "payroll_payer_wallet.reverification_required",
                      "payroll_fee_settlement.paid"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The endpoint with its signing secret (only here and on rotate-secret).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointWithSecret"
                },
                "examples": {
                  "created": {
                    "$ref": "#/components/examples/WebhookEndpointWithSecretExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- invalid_json (Shape B): A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header. \n- invalid_request (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header. \n- parameter_missing (Shape B): url is absent, empty or not a string: \"url is required.\" (a number or null too). \n- invalid_url (Shape B): In this order: over 2048 characters (\"url must be at most 2048 characters.\"); not parseable or not starting with https:// in any letter case (\"url must be an https URL.\"); a username or password (\"url must not contain a username or password.\"); then the SSRF check, whose messages say \"callbackUrl\": the host is localhost (\"callbackUrl may not target localhost.\"), does not resolve now (\"callbackUrl host could not be resolved...\"), or resolves to any non-public address (\"callbackUrl resolves to a non-public address (203.0.113.5); refusing to accept.\"). \n- invalid_description (Shape B): description is neither null nor a string of at most 500 characters: \"description must be a string of at most 500 characters.\" \n- invalid_enabled_events (Shape B): enabled_events is not a non-empty array (\"enabled_events must be a non-empty array of event types, or [\"*\"] for every event.\"), or holds a value that is not a catalogue type or \"*\" (the message names it). \n- invalid_status (Shape B): status is sent and is not \"enabled\" or \"disabled\" (it is validated, then ignored: an endpoint is always created enabled). \n- webhook_endpoint_limit_reached (Shape B): The tenant already has the maximum (16) endpoints, both modes and both statuses counted: \"At most 16 webhook endpoints per account.\"",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "notHttps": {
                    "$ref": "#/components/examples/WebhookInvalidUrlHttpsExample"
                  },
                  "badUrl": {
                    "$ref": "#/components/examples/WebhookInvalidUrlExample"
                  },
                  "cap": {
                    "$ref": "#/components/examples/WebhookEndpointLimitExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/WebhooksTenantKeyUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WebhooksTenantKeyForbidden"
          },
          "409": {
            "description": "- idempotency_key_in_use (Shape B): The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run. \n- idempotency_parameter_mismatch (Shape B): The same Idempotency-Key with another body or query. \n- idempotency_key_in_progress (Shape B): The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/WebhooksBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/WebhooksUnsupportedMediaType"
          },
          "500": {
            "description": "- internal_error (Shape B): An untyped failure, masked. After the insert (writing its audit row) the endpoint exists, takes a slot of the cap, and its secret was never shown; under an Idempotency-Key the 500 is stored and replayed. List the endpoints before acting.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "$ref": "#/components/examples/ErrInternalExample"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant (an expired key and a suspended member's key are named first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended; error_type authentication_error."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a host listed in sandbox-only."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, holds neither webhooks:write nor *. A path sent with capital letters is classified in lower case."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same Idempotency-Key with another body or query."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made."
          },
          {
            "status": 400,
            "code": "parameter_missing",
            "envelope": "B",
            "when": "url is absent, empty or not a string: \"url is required.\" (a number or null too)."
          },
          {
            "status": 400,
            "code": "invalid_url",
            "envelope": "B",
            "when": "In this order: over 2048 characters (\"url must be at most 2048 characters.\"); not parseable or not starting with https:// in any letter case (\"url must be an https URL.\"); a username or password (\"url must not contain a username or password.\"); then the SSRF check, whose messages say \"callbackUrl\": the host is localhost (\"callbackUrl may not target localhost.\"), does not resolve now (\"callbackUrl host could not be resolved...\"), or resolves to any non-public address (\"callbackUrl resolves to a non-public address (203.0.113.5); refusing to accept.\")."
          },
          {
            "status": 400,
            "code": "invalid_description",
            "envelope": "B",
            "when": "description is neither null nor a string of at most 500 characters: \"description must be a string of at most 500 characters.\""
          },
          {
            "status": 400,
            "code": "invalid_enabled_events",
            "envelope": "B",
            "when": "enabled_events is not a non-empty array (\"enabled_events must be a non-empty array of event types, or [\"*\"] for every event.\"), or holds a value that is not a catalogue type or \"*\" (the message names it)."
          },
          {
            "status": 400,
            "code": "invalid_status",
            "envelope": "B",
            "when": "status is sent and is not \"enabled\" or \"disabled\" (it is validated, then ignored: an endpoint is always created enabled)."
          },
          {
            "status": 400,
            "code": "webhook_endpoint_limit_reached",
            "envelope": "B",
            "when": "The tenant already has the maximum (16) endpoints, both modes and both statuses counted: \"At most 16 webhook endpoints per account.\""
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure, masked. After the insert (writing its audit row) the endpoint exists, takes a slot of the cap, and its secret was never shown; under an Idempotency-Key the 500 is stored and replayed. List the endpoints before acting."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "webhooks:write"
      },
      "get": {
        "operationId": "listWebhookEndpoints",
        "tags": [
          "Webhooks"
        ],
        "summary": "List the tenant's webhook endpoints in the key's mode (never a secret)",
        "description": "One page of the key's mode's endpoints, newest first by rowid. Default page 20, at most 100. Filters: status (enabled|disabled), created[gte]/created[lte]. An unknown cursor answers the first page; a cursor that is the other mode's endpoint still places the page at its rowid. No secret on any row. An endpoint created before 2026-10-28 may still hold an http:// url. No audit row, no rate limiter.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "$ref": "#/components/parameters/WebhookEndpointStatusQuery"
          },
          {
            "$ref": "#/components/parameters/CreatedGte"
          },
          {
            "$ref": "#/components/parameters/CreatedLte"
          }
        ],
        "responses": {
          "200": {
            "description": "One page of endpoints.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointList"
                },
                "examples": {
                  "page": {
                    "$ref": "#/components/examples/WebhookEndpointListExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- invalid_json (Shape B): A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header. \n- invalid_request (Shape B): The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header. \n- invalid_pagination_parameter (Shape B): limit is not an integer from 1 to 100 (an empty ?limit= included), or both starting_after and ending_before are sent. \n- invalid_filter_parameter (Shape B): status is not exactly one of enabled, disabled; created is sent as a plain value; a created bound is neither ISO-8601 nor unix seconds; or created[gte] is later than created[lte].",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "badLimit": {
                    "$ref": "#/components/examples/ErrInvalidPaginationExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/WebhooksTenantKeyUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WebhooksTenantKeyForbidden"
          },
          "413": {
            "$ref": "#/components/responses/WebhooksBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/WebhooksUnsupportedMediaType"
          },
          "500": {
            "description": "- internal_error (Shape B): An untyped failure (a database error), masked.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "$ref": "#/components/examples/ErrInternalExample"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "The body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload, before every other check, with request_id null and no X-Request-ID header."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant (an expired key and a suspended member's key are named first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended; error_type authentication_error."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a host listed in sandbox-only."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, holds neither webhooks:read, webhooks:write nor *. A path sent with capital letters is classified in lower case."
          },
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B",
            "when": "limit is not an integer from 1 to 100 (an empty ?limit= included), or both starting_after and ending_before are sent."
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B",
            "when": "status is not exactly one of enabled, disabled; created is sent as a plain value; a created bound is neither ISO-8601 nor unix seconds; or created[gte] is later than created[lte]."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a database error), masked."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "webhooks:read"
      }
    },
    "/v1/webhook-endpoints/{id}": {
      "get": {
        "operationId": "retrieveWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Retrieve one webhook endpoint (never its secret)",
        "description": "The tenant's endpoint in the key's mode. Unknown, another tenant's or the other mode's: Shape B 404 resource_missing \"No such webhook endpoint.\". previous_secret_expires_at shows only while the rotation overlap is still open. Read-only; no audit row.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEndpointIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "The endpoint.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                },
                "examples": {
                  "endpoint": {
                    "$ref": "#/components/examples/WebhookEndpointExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- invalid_json (Shape B): A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header. \n- invalid_request (Shape B): A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/WebhooksTenantKeyUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WebhooksTenantKeyForbidden"
          },
          "404": {
            "description": "- resource_missing (Shape B): No endpoint of the tenant has this id in the key's mode (another tenant's, or the other mode's, included): \"No such webhook endpoint.\"",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing": {
                    "$ref": "#/components/examples/WebhookEndpointMissingExample"
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/WebhooksBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/WebhooksUnsupportedMediaType"
          },
          "500": {
            "description": "- internal_error (Shape B): An untyped failure (a database error), masked.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "$ref": "#/components/examples/ErrInternalExample"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header)."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant (an expired key and a suspended member's key are named first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended; error_type authentication_error."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a host listed in sandbox-only."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, holds neither webhooks:read, webhooks:write nor *. A path sent with capital letters is classified in lower case."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No endpoint of the tenant has this id in the key's mode (another tenant's, or the other mode's, included): \"No such webhook endpoint.\""
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure (a database error), masked."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "webhooks:read"
      },
      "post": {
        "operationId": "updateWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Update a webhook endpoint's url, description, enabled_events or status",
        "description": "Reads the endpoint first in the key's mode, so an unknown or other-mode id answers 404 before any body error. Then only the fields present change: url (if present must be a non-empty string - null or \"\" is parameter_missing - and pass the create's https, length, no-user-info and public-address checks, even when it is the current url); absent, the stored url is kept and not checked again, so an endpoint stored with an http url before 2026-10-28 keeps it; description (string up to 500, null clears it); enabled_events (null RESETS it to [\"*\"], every event; [] is invalid_enabled_events); status (enabled|disabled; null is invalid_status). The mode (is_test) cannot change. An empty body still bumps updated_at and writes the audit row. The answer is the row the UPDATE wrote; an endpoint deleted between the read and the write answers 404. Changing url does not move rows already queued: a pending row keeps the URL it was recorded. Disabling stops new fan-out at once and the sweep closes the endpoint's pending rows as superseded when it reaches them; a send already queued or under way still goes out. Audit row webhook_endpoint.updated, target the endpoint, details = the validated fields (url as sent), written after the update and outside a transaction. Idempotency-Key (tenant layer) replays the stored answer, sealed under the calling key.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEndpointIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEndpointUpdateRequest"
              },
              "examples": {
                "disable": {
                  "value": {
                    "status": "disabled"
                  }
                },
                "addReleased": {
                  "summary": "Re-send the full payroll list",
                  "value": {
                    "enabled_events": [
                      "payroll_batch.awaiting_approval",
                      "payroll_batch.approved",
                      "payroll_batch.rejected",
                      "payroll_batch.authorization_expired",
                      "payroll_batch.closed",
                      "payroll_item.completed",
                      "payroll_item.attempt_failed",
                      "payroll_item.failed",
                      "payroll_item.cancelled",
                      "payroll_item.released",
                      "payroll_authorization.flagged",
                      "payroll_authorization.fee_overcharge",
                      "payroll_authorization.resolved",
                      "payroll_authorization.fee_shortfall",
                      "payroll_payer_wallet.reverification_required",
                      "payroll_fee_settlement.paid"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The endpoint after the update (no secret).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                },
                "examples": {
                  "updated": {
                    "$ref": "#/components/examples/WebhookEndpointExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- invalid_json (Shape B): A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header. \n- invalid_request (Shape B): A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header). \n- parameter_missing (Shape B): url is sent as null, \"\" or a non-string: \"url is required.\" \n- invalid_url (Shape B): A url sent that fails a create rule, with the same messages: over 2048 characters, not https, a username or password, then localhost, unresolvable or non-public. \n- invalid_description (Shape B): description is neither null nor a string of at most 500 characters. \n- invalid_enabled_events (Shape B): enabled_events is [] or not an array, or holds an unknown type (null resets it to [\"*\"] and is accepted). \n- invalid_status (Shape B): status is sent (null included) and is not \"enabled\" or \"disabled\".",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "notHttps": {
                    "$ref": "#/components/examples/WebhookInvalidUrlHttpsExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/WebhooksTenantKeyUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WebhooksTenantKeyForbidden"
          },
          "404": {
            "description": "- resource_missing (Shape B): No endpoint of the tenant has this id in the key's mode, checked before the body; or it was deleted between that read and the write: \"No such webhook endpoint.\"",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing": {
                    "$ref": "#/components/examples/WebhookEndpointMissingExample"
                  }
                }
              }
            }
          },
          "409": {
            "description": "- idempotency_key_in_use (Shape B): The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run. \n- idempotency_parameter_mismatch (Shape B): The same Idempotency-Key with another body or query. \n- idempotency_key_in_progress (Shape B): The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/WebhooksBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/WebhooksUnsupportedMediaType"
          },
          "500": {
            "description": "- internal_error (Shape B): An untyped failure, masked. After the update (writing its audit row) the change is made: read the endpoint before retrying.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "$ref": "#/components/examples/ErrInternalExample"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header)."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant (an expired key and a suspended member's key are named first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended; error_type authentication_error."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a host listed in sandbox-only."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, holds neither webhooks:write nor *. A path sent with capital letters is classified in lower case."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same Idempotency-Key with another body or query."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No endpoint of the tenant has this id in the key's mode, checked before the body; or it was deleted between that read and the write: \"No such webhook endpoint.\""
          },
          {
            "status": 400,
            "code": "parameter_missing",
            "envelope": "B",
            "when": "url is sent as null, \"\" or a non-string: \"url is required.\""
          },
          {
            "status": 400,
            "code": "invalid_url",
            "envelope": "B",
            "when": "A url sent that fails a create rule, with the same messages: over 2048 characters, not https, a username or password, then localhost, unresolvable or non-public."
          },
          {
            "status": 400,
            "code": "invalid_description",
            "envelope": "B",
            "when": "description is neither null nor a string of at most 500 characters."
          },
          {
            "status": 400,
            "code": "invalid_enabled_events",
            "envelope": "B",
            "when": "enabled_events is [] or not an array, or holds an unknown type (null resets it to [\"*\"] and is accepted)."
          },
          {
            "status": 400,
            "code": "invalid_status",
            "envelope": "B",
            "when": "status is sent (null included) and is not \"enabled\" or \"disabled\"."
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure, masked. After the update (writing its audit row) the change is made: read the endpoint before retrying."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "webhooks:write"
      },
      "delete": {
        "operationId": "deleteWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook endpoint of the key's mode (hard delete)",
        "description": "Deletes the row in the key's mode, the read and the delete one statement. Dispatch stops listing it at once; its pending rows are closed as superseded when the sweep reaches them; a send already queued for a delivery slot or under way still goes out with its in-process retries. Its slot under the 16-endpoint cap is freed. Audit row webhook_endpoint.deleted, target the endpoint, details {} (no URL, mode or events), written after the delete and outside a transaction. The answer {id, object, deleted: true} names the id from the path. A repeat without an Idempotency-Key answers 404; with the same key the stored 200 replays, sealed under the calling key.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEndpointIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointDeleted"
                },
                "examples": {
                  "deleted": {
                    "$ref": "#/components/examples/WebhookEndpointDeletedExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- invalid_json (Shape B): A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header. \n- invalid_request (Shape B): A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/WebhooksTenantKeyUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WebhooksTenantKeyForbidden"
          },
          "404": {
            "description": "- resource_missing (Shape B): No endpoint of the tenant has this id in the key's mode (a second delete included): \"No such webhook endpoint.\"",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing": {
                    "$ref": "#/components/examples/WebhookEndpointMissingExample"
                  }
                }
              }
            }
          },
          "409": {
            "description": "- idempotency_key_in_use (Shape B): The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run. \n- idempotency_parameter_mismatch (Shape B): The same Idempotency-Key with another body or query. \n- idempotency_key_in_progress (Shape B): The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/WebhooksBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/WebhooksUnsupportedMediaType"
          },
          "500": {
            "description": "- internal_error (Shape B): An untyped failure, masked. After the delete (writing its audit row) the endpoint is gone: a read answering 404 confirms it.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "$ref": "#/components/examples/ErrInternalExample"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header)."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant (an expired key and a suspended member's key are named first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended; error_type authentication_error."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a host listed in sandbox-only."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, holds neither webhooks:write nor *. A path sent with capital letters is classified in lower case."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same Idempotency-Key with another body or query."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No endpoint of the tenant has this id in the key's mode (a second delete included): \"No such webhook endpoint.\""
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure, masked. After the delete (writing its audit row) the endpoint is gone: a read answering 404 confirms it."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "webhooks:write"
      }
    },
    "/v1/webhook-endpoints/{id}/rotate-secret": {
      "post": {
        "operationId": "rotateWebhookEndpointSecret",
        "tags": [
          "Webhooks"
        ],
        "summary": "Rotate one endpoint's signing secret (the new secret shown once)",
        "description": "expires_in_hours is checked FIRST (400 invalid_expires_in_hours before any 404), then the endpoint is read and rotated in the key's mode. The new whsec_ secret signs every delivery from now; the previous one keeps signing beside it for expires_in_hours (0-72, default 24; 0 = dropped at once), so x-webhook-signature carries two values, current first. Neither secret travels in x-webhook-secret: a delivery to an account endpoint never carries it. A sweep or manual redelivery signs with the secrets active when it runs. A first attempt dispatched before the rotation was signed at dispatch, before its wait for a delivery slot, so it still goes out signed with the old secret only: inside the overlap the receiver's old secret still verifies it; with expires_in_hours 0 a receiver that already holds only the new secret rejects it, and the sweep later resends it signed with the new one. One previous secret only: rotating again inside the window drops the older one. A sandbox key rotates a sandbox endpoint's secret. Audit row webhook_endpoint.secret_rotated, target the endpoint, details {expiresInHours, previousSecretExpiresAt}, after the write and outside a transaction. Idempotency-Key (tenant layer): a repeat replays the same secret, unaudited, from an answer sealed under the calling key.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEndpointIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookSecretRotateRequest"
              },
              "examples": {
                "default24h": {
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The endpoint with its NEW secret.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointWithSecret"
                },
                "examples": {
                  "rotated": {
                    "$ref": "#/components/examples/WebhookEndpointRotatedExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "- invalid_json (Shape B): A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header. \n- invalid_request (Shape B): A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header). \n- invalid_expires_in_hours (Shape B): expires_in_hours does not read (through Number) as a number from 0 to 72; checked before the endpoint is looked up.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "badHours": {
                    "$ref": "#/components/examples/WebhookInvalidExpiresInHoursExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/WebhooksTenantKeyUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WebhooksTenantKeyForbidden"
          },
          "404": {
            "description": "- resource_missing (Shape B): No endpoint of the tenant has this id in the key's mode (another tenant's, or the other mode's, included): \"No such webhook endpoint.\"",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing": {
                    "$ref": "#/components/examples/WebhookEndpointMissingExample"
                  }
                }
              }
            }
          },
          "409": {
            "description": "- idempotency_key_in_use (Shape B): The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run. \n- idempotency_parameter_mismatch (Shape B): The same Idempotency-Key with another body or query. \n- idempotency_key_in_progress (Shape B): The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/WebhooksBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/WebhooksUnsupportedMediaType"
          },
          "500": {
            "description": "- internal_error (Shape B): An untyped failure, masked. After the rotation (writing its audit row) the secret is replaced and the answer lost; under an Idempotency-Key the 500 is stored and replayed.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "$ref": "#/components/examples/ErrInternalExample"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "A body that is not valid JSON, or whose top-level value is neither an object nor an array, under a JSON content-type. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A path parameter that is not valid percent-encoding (for example %E0%A4%A): Express decodes it while matching the route, before any route step (after the body parser), and the message is fixed (\"A path parameter is not valid percent-encoding.\"). Also the body parser's own refusal of a body whose length does not match its Content-Length, or of an aborted upload (before every other check, request_id null, no X-Request-ID header)."
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "when": "A body over the request body limit. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "A JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate. The body parser answers before every other check, with request_id null and no X-Request-ID or X-API-Version header.",
            "note": "Raised by body-parser 1.20.6, (charset) and (encoding), answered."
          },
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A",
            "when": "No Authorization header, or not \"Bearer <key>\"; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A",
            "when": "An unknown or revoked key, or a key of a suspended tenant (an expired key and a suspended member's key are named first); error_type authentication_error."
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A",
            "when": "The key is past its expires_at; error_type authentication_error."
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A",
            "when": "The member the key is issued to is suspended; error_type authentication_error."
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A",
            "when": "A live key on a host listed in sandbox-only."
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A",
            "when": "The key's IP allow-list excludes the caller."
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "The key, narrowed to its member's role ceiling, holds neither webhooks:write nor *. A path sent with capital letters is classified in lower case."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "The Idempotency-Key was claimed by another API key of the tenant (checked first), or its stored answer does not open under this key: an answer stored unsealed before the 2026-10-28 deploy answers no one until it expires, 24 h after it was stored. The request is not run."
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B",
            "when": "The same Idempotency-Key with another body or query."
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B",
            "when": "The first request under this key has not answered yet; after a relay restart mid-request the claim holds until it expires, 24 h after it was made."
          },
          {
            "status": 400,
            "code": "invalid_expires_in_hours",
            "envelope": "B",
            "when": "expires_in_hours does not read (through Number) as a number from 0 to 72; checked before the endpoint is looked up."
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B",
            "when": "No endpoint of the tenant has this id in the key's mode (another tenant's, or the other mode's, included): \"No such webhook endpoint.\""
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "An untyped failure, masked. After the rotation (writing its audit row) the secret is replaced and the answer lost; under an Idempotency-Key the 500 is stored and replayed."
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "webhooks:write"
      }
    },
    "/checkout/tonconnect-manifest.json": {
      "get": {
        "operationId": "getTonConnectManifest",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "TonConnect app manifest for TON wallets",
        "description": "The TonConnect manifest a TON wallet fetches when the hosted checkout connects to it: `url` is `<base>/checkout/index.html`, `name` is the fixed text \"FastPay Checkout\", `iconUrl` is `<base>/checkout/fastpay-icon.png`. `<base>` is the production host, else the request's own protocol and Host header (production sets the production host). Always the live origin (no sandbox variant). Registered before the /checkout static mount, so no file can shadow it. CORS-open and cacheable 5 minutes. No authentication, no limiter, no side effect. HEAD is answered too (Express GET route).",
        "security": [],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "unexpected error (global handler)"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "The manifest.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/CheckoutCorsAllowOriginAny"
              },
              "Cache-Control": {
                "description": "`public, max-age=300`.",
                "schema": {
                  "type": "string",
                  "const": "public, max-age=300"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TonConnectManifest"
                },
                "examples": {
                  "manifest": {
                    "$ref": "#/components/examples/TonConnectManifestExample"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutBodyRefused"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/sdk/{sdkFile}": {
      "get": {
        "operationId": "getSdkFile",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Static browser SDK file",
        "description": "express.static over (express 4.22.2, serve-static 1.16.3). Serves the files of the DEPLOYED tree, not a published npm version (fastpay-checkout-sdk 1.7.0; npm 1.2.0 to 1.7.0 are unpublished). Files: fastpay-checkout.js (window.FastPayCheckout and FastPayPayroll; the hosted page's SDK), checkout.js (window.GatewayCheckout, cs_ sessions), fastpay-embed.js (window.FastPayEmbed, iframe of the hosted page), fastpay-react.js (React wrapper of the embed), fastpay-payroll-verify.js (the Bitcoin payroll PSBT verifier, loaded pinned by Subresource Integrity), package.json, README.md; and vendor/ (next path). GET and HEAD only; another method, a missing file or a dotfile falls through to Express's HTML 404 (no JSON body). serve-static answers ETag, Last-Modified, `Cache-Control: public, max-age=0`, 304 on a matching conditional GET and 206 on a Range request; a failed If-Match or If-Unmodified-Since precondition, or a bytes= Range it cannot satisfy, is a Shape B 500 internal_error from the global handler, not a 412 or 416. No authentication, no limiter.",
        "security": [],
        "parameters": [
          {
            "name": "sdkFile",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "fastpay-checkout.js",
                "checkout.js",
                "fastpay-embed.js",
                "fastpay-react.js",
                "fastpay-payroll-verify.js",
                "package.json",
                "README.md"
              ]
            }
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "the file exists and a precondition fails (If-Match matching neither its ETag nor *, or If-Unmodified-Since before its Last-Modified) or a bytes= Range cannot be satisfied: serve-static hands its 412 or 416 to the global handler, which answers an error with no body-parser type and no slug as a 500, sent with the file's headers; or any other unexpected error"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "The file (Content-Type by extension, e.g. application/javascript; charset=UTF-8).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "serve-static ETag."
              },
              "Last-Modified": {
                "schema": {
                  "type": "string"
                },
                "description": "The file's mtime in the deployed image."
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "public, max-age=0"
                },
                "description": "serve-static default (maxAge 0)."
              }
            },
            "content": {
              "application/javascript": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (conditional GET)."
          },
          "400": {
            "$ref": "#/components/responses/CheckoutBodyRefused"
          },
          "404": {
            "description": "Express's default HTML \"Cannot GET\" page - no JSON error body.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/sdk/vendor/{vendorFile}": {
      "get": {
        "operationId": "getSdkVendorFile",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Vendored SDK dependency (WalletConnect bundle)",
        "description": "The same express.static mount as /sdk/{sdkFile}, sub-folder: fastpay-walletconnect.js (the WalletConnect EthereumProvider bundle FastPayCheckout loads on demand from `<relayUrl>/sdk/vendor/fastpay-walletconnect.js`) and README.md. `/sdk/vendor` without the slash answers a 301 redirect to `/sdk/vendor/`, which has no index file (HTML 404). Same headers and limits as /sdk/{sdkFile}.",
        "security": [],
        "parameters": [
          {
            "name": "vendorFile",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "fastpay-walletconnect.js",
                "README.md"
              ]
            }
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "the file exists and a precondition fails (If-Match matching neither its ETag nor *, or If-Unmodified-Since before its Last-Modified) or a bytes= Range cannot be satisfied: serve-static hands its 412 or 416 to the global handler, which answers an error with no body-parser type and no slug as a 500, sent with the file's headers; or any other unexpected error"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "The file.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/javascript": {
                "schema": {
                  "type": "string"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (conditional GET)."
          },
          "400": {
            "$ref": "#/components/responses/CheckoutBodyRefused"
          },
          "404": {
            "description": "Express's default HTML 404.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/checkout/{checkoutFile}": {
      "get": {
        "operationId": "getHostedCheckoutFile",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Hosted checkout page and its assets",
        "description": "express.static over. Files: index.html (the hosted payment page; `?id=pr_...|cs_...`; loads ethers 6.9.0 from cdnjs with SRI and /sdk/fastpay-checkout.js, polls GET /v1/payment-requests/{id}/public every 6 s, it refuses an id that is not letters, digits and underscores before any fetch), link.html (the payment-link page; `?id=plink_...`, the same id check, calls GET /v1/payment-links/{id}/public and POST /v1/payment-links/{id}/sessions), fastpay-icon.png (the TonConnect icon), fastpay-icon.svg (the Solana Pay icon). `/checkout/` serves index.html (serve-static default index); `/checkout` answers a 301 to `/checkout/`. GET/HEAD only, same headers as /sdk; a missing file is Express's HTML 404. Every checkout link the relay builds points here: `<base>/checkout/index.html?id=<id>` and `<base>/checkout/link.html?id=<plink id>`.",
        "security": [],
        "parameters": [
          {
            "name": "checkoutFile",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "index.html",
                "link.html",
                "fastpay-icon.png",
                "fastpay-icon.svg"
              ]
            }
          },
          {
            "name": "id",
            "in": "query",
            "required": false,
            "description": "Read by the page's own script (not by the relay) - the pr_/cs_ id (index.html) or the plink_ id (link.html).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "the file exists and a precondition fails (If-Match matching neither its ETag nor *, or If-Unmodified-Since before its Last-Modified) or a bytes= Range cannot be satisfied: serve-static hands its 412 or 416 to the global handler, which answers an error with no body-parser type and no slug as a 500, sent with the file's headers; or any other unexpected error"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "The file.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "contentEncoding": "binary"
                }
              },
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (conditional GET)."
          },
          "400": {
            "$ref": "#/components/responses/CheckoutBodyRefused"
          },
          "404": {
            "description": "Express's default HTML 404.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/rates": {
      "get": {
        "operationId": "getExchangeRate",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Indicative exchange rate (not a locked quote)",
        "description": "Answers how many units of `to` one unit of `from` is worth, from the same pipeline that prices a checkout session. Tiers: a cached live CoinGecko table younger than the cache lifetime (30 s), a fresh fetch, a live table younger than the staleness limit (5 min) when the fetch fails (`live_stale`), else the static configured table (`static_fallback`). The tier is chosen for the whole table, never per pair: a pair the live table lacks is 503 even where the static table has it. The table is fiat (EUR, USD) to crypto; the inverse pair is computed; any other pair (fiat to fiat, crypto to crypto, an unknown symbol) is 503 rate_unavailable. `from` and `to` are upper-cased except the stablecoins' own spelling (USDe); equal codes answer rate \"1.0\" with source `identity` whatever the symbol. Only POST /v1/checkout/sessions locks a rate. Per-IP limiter (default 30 a minute, its own budget). No authentication, no side effect.",
        "security": [],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "Source currency code, any letter case (e.g. EUR, usdc). Must be a single non-blank string.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "EUR"
              ]
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "description": "Target currency code, any letter case.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "USDC"
              ]
            }
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "parameter_missing",
            "envelope": "B",
            "when": "from or to absent, blank or repeated"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "a body that is not JSON"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A",
            "when": "per IP per minute (the 429)"
          },
          {
            "status": 503,
            "code": "rate_unavailable",
            "envelope": "B",
            "when": "no live, stale or static rate for the pair"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "The rate.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExchangeRate"
                },
                "examples": {
                  "eurToUsdc": {
                    "$ref": "#/components/examples/ExchangeRateExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "parameter_missing (Shape B), or invalid_json.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          },
          "503": {
            "description": "rate_unavailable (Shape B, error.type api_error, error.param null).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payment-requests": {
      "post": {
        "operationId": "createPaymentRequest",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Create a payment request (a client-signed checkout intent record)",
        "description": "Records where a future payer-signed transaction must send funds; signs nothing, broadcasts nothing and moves no payout quota. Order of the checks: authentication, scope checkout:write and the tenant Idempotency-Key layer; the per-tenant limiter (the tenant's requests_per_minute or 60 per minute by default 60); the body (reference, network, currency, expiresAt, metadata, callbackUrl format, then amount and recipient); the callbackUrl public-address check (DNS); the Idempotency-Key header shape; the insert. The row is created `pending` in the calling key's mode. Idempotency: two layers. The tenant layer replays the first answer for 24 h (X-Cache-Lookup HIT, a stored 201 replays as 201), sealed under the calling API key: another key of the tenant gets 409 idempotency_key_in_use. The payment request also keeps its own key for good, per tenant and mode (UNIQUE (tenant_id, idempotency_key, is_test)): after the 24 h, the same key with the same request answers 200 with the existing request; with a different request, 409 idempotency_key_conflict. Without a key every retry creates another request. Live vs sandbox: an fp_test_ key creates a sandbox row (is_test true) whose intent is simulated, whose QR has no Solana Pay payload and whose links use the sandbox host when set. No webhook, no audit row, no ledger entry on creation; a paid bare pr_ request is never credited to the ledger (only a checkout session's settlement is). Recipient: the registered wallet is only the default; a key with checkout:write may name any recipient and the relay checks only its format, so the wallet registry bounds where checkout-session money goes, not payment requests or links.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaymentRequestCreateRequest"
              },
              "examples": {
                "usdcOnBase": {
                  "$ref": "#/components/examples/PaymentRequestCreateExample"
                }
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:write",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A",
            "when": "no checkout:write (or the member's role ceiling removes it, e.g. approver or viewer)"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "the key was used by another API key of this tenant, or its stored answer does not open under this key (a row stored unsealed before the 2026-10-28 deploy, until it expires)"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_conflict",
            "envelope": "A",
            "when": "the request's own durable key was used for a different request"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A",
            "when": "per-tenant limiter"
          },
          {
            "status": 400,
            "code": "missing_recipient",
            "envelope": "A",
            "when": "recipient omitted and no tenant wallet on that network"
          },
          {
            "status": 400,
            "code": "invalid_payment_request",
            "envelope": "A",
            "when": "any body field invalid"
          },
          {
            "status": 400,
            "code": "invalid_callback_url",
            "envelope": "A",
            "when": "callbackUrl does not resolve to a public address"
          },
          {
            "status": 400,
            "code": "invalid_idempotency_key",
            "envelope": "A",
            "when": "Idempotency-Key empty or over 200 characters"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "A",
            "note": "A failure outside the route's own catch (its loader, the body parser) is the global handler's Shape B 500 instead."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "The request's own key matched an existing identical request (past the 24 h tenant layer).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestTenantView"
                }
              }
            }
          },
          "201": {
            "description": "Created.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestTenantView"
                },
                "examples": {
                  "created": {
                    "$ref": "#/components/examples/PaymentRequestTenantViewExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "missing_recipient, invalid_payment_request, invalid_callback_url, invalid_idempotency_key (Shape A); invalid_json (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalidRequest": {
                    "$ref": "#/components/examples/InvalidPaymentRequestErrorExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "409": {
            "description": "idempotency_key_in_use, idempotency_parameter_mismatch, idempotency_key_in_progress (Shape B, tenant layer); idempotency_key_conflict (Shape A, the request's own key).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeA"
          }
        }
      },
      "get": {
        "operationId": "listPaymentRequests",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "List payment requests",
        "description": "This tenant's payment requests in the calling key's mode, newest first by rowid, cursor-paged. A checkout session's own settlement row (id cs_...) is a payment request too and is listed here; requests opened from a payment link carry paymentLinkId. Each item is the tenant view with its checkoutUrl and QR payloads. Filters: status (pending, paid, paid_after_expiry), network (exact lower-case key), payment_link_id, created[gte]/created[lte]. No limiter, read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "$ref": "#/components/parameters/CreatedGte"
          },
          {
            "$ref": "#/components/parameters/CreatedLte"
          },
          {
            "$ref": "#/components/parameters/PaymentRequestStatusFilter"
          },
          {
            "$ref": "#/components/parameters/CheckoutNetworkFilter"
          },
          {
            "$ref": "#/components/parameters/PaymentLinkIdFilter"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:read",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "A page.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutListParameterError"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payment-requests/{requestId}": {
      "get": {
        "operationId": "getPaymentRequest",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Retrieve one payment request (tenant view)",
        "description": "The calling tenant's request in the key's mode; another tenant's, the other mode's or an unknown id is the same flat 404 payment_request_not_found. The view is the stored row in camelCase plus checkoutUrl and qr, built on the request's own mode's origin. `payer` is the wallet the latest intent was built for - never verified, never a destination; `settledPayer` is the payer verified on chain at settlement, null for TON, sandbox and requests paid before 2026-10-18. Works for a cs_ id too. Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentRequestIdPath"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:read",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "payment_request_not_found",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "the path parameter is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The request.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestTenantView"
                },
                "examples": {
                  "pending": {
                    "$ref": "#/components/examples/PaymentRequestTenantViewExample"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutPaymentRequestNotFound"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payment-requests/{requestId}/public": {
      "get": {
        "operationId": "getPublicPaymentRequest",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Public view of a payment request (for the payer)",
        "description": "What the hosted page and the SDK render before a wallet connects. By id alone, any tenant and mode. The public view leaves out tenantId, idempotencyKey, callbackUrl, payer, settledPayer and metadata except a scheme-checked metadata.redirect_url (http or https only) as redirectUrl; it adds chainId on EVM networks and walletChain on Sui. Neither the tenant's status nor the request's expiry is checked here: a suspended tenant's request still reads `pending`, and only the intent says 409 tenant_not_active. Shared per-IP limiter (default 30 a minute) - ONE budget across every public checkout route (express-rate-limit 8.6.2 keys an IPv6 client by its /56). No CORS header: a browser on another origin cannot read it (only the hosted page, same origin, does).",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentRequestIdPath"
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "payment_request_not_found",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The public view.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestPublicView"
                },
                "examples": {
                  "public": {
                    "$ref": "#/components/examples/PaymentRequestPublicViewExample"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutPaymentRequestNotFound"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payment-requests/{requestId}/intent": {
      "post": {
        "operationId": "createPaymentIntent",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Build the unsigned payment transaction(s) for the payer's wallet",
        "description": "Anonymous: the payer has only the id. Steps, in order: payerAddress present (else 400 invalid_payer_address); a request opened from a payment link must still have a use left - the OTHER paid requests are counted; then: the kill switch (503 service_paused, sandbox included); the merchant tenant's status (409 tenant_not_active unless it is active, sandbox included, read on every call); the adapter (a sandbox request always gets the simulator); the request still pending and not expired (409 payment_request_not_payable); no x402 settlement of it in flight or being settled (409 x402_settlement_in_progress); The authorization expires at the sooner of the request's expiresAt and the authorization validity (15 min) from now; while it is outstanding the x402 seller refuses the request. Each call signs again (no cache); a Tron TRC20 payer with a short allowance first gets an approval-only intent and calls again after the approve lands. The response shape is per network (see CheckoutIntent). Not idempotent and not covered by any Idempotency-Key layer (no tenant). No webhook, no audit row. Shared per-IP public limiter.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentRequestIdPath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutIntentRequest"
              },
              "examples": {
                "evmPayer": {
                  "value": {
                    "payerAddress": "0x2222222222222222222222222222222222222222"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_payer_address",
            "envelope": "A",
            "when": "payerAddress missing or not a non-empty string"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "A",
            "when": "payerAddress not an address of the request's network, or a network with no adapter",
            "note": "The same slug in Shape B answers a path parameter that is not valid percent-encoding and a body body-parser refused."
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "payment_request_not_found",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "payment_link_unavailable",
            "envelope": "A",
            "when": "the link's max_uses is reached; body.reason exhausted"
          },
          {
            "status": 409,
            "code": "tenant_not_active",
            "envelope": "A",
            "when": "the merchant's tenant is suspended (not active), sandbox or live; checked after the kill switch and before anything is signed"
          },
          {
            "status": 409,
            "code": "payment_request_not_payable",
            "envelope": "A",
            "when": "not pending, or expired"
          },
          {
            "status": 409,
            "code": "x402_settlement_in_progress",
            "envelope": "A",
            "when": "an x402 payment of this request is in flight or being settled"
          },
          {
            "status": 409,
            "code": "currency_not_allowlisted",
            "envelope": "A",
            "when": "the token has no configured address here, or the disburser's allowlist refuses it"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 503,
            "code": "service_paused",
            "envelope": "A",
            "when": "kill switch on"
          },
          {
            "status": 503,
            "code": "signing_busy",
            "envelope": "A",
            "when": "Retry-After set"
          },
          {
            "status": 503,
            "code": "disburser_paused",
            "envelope": "A",
            "when": "Aptos, Sui or TON disburser paused"
          },
          {
            "status": 503,
            "code": "payment_authorizer_mismatch",
            "envelope": "A",
            "when": "Solana, Aptos, Sui, TON (EVM and Tron answer 500)"
          },
          {
            "status": 503,
            "code": "authorizer_cluster_mismatch",
            "envelope": "A",
            "when": "Solana mainnet authorizer on a non-mainnet cluster"
          },
          {
            "status": 503,
            "code": "payroll_network_not_configured",
            "envelope": "A",
            "when": "Aptos/Sui/TON settings or pins missing"
          },
          {
            "status": 503,
            "code": "payroll_deployment_not_audited",
            "envelope": "A",
            "when": "Aptos/Sui/TON deployment not the audited one"
          },
          {
            "status": 503,
            "code": "authorizer_network_mismatch",
            "envelope": "A",
            "when": "Aptos/Sui/TON authorizer key of another network"
          },
          {
            "status": 503,
            "code": "chain_unavailable",
            "envelope": "A",
            "when": "TON jetton wallet does not verify against the pinned code"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "A",
            "when": "untyped failure: an RPC error, a network this deployment has not configured, an EVM or Tron authorizer mismatch, a Sui payer whose coins do not cover the total",
            "note": "A failure outside the route's own catch (its loader, the body parser) is the global handler's Shape B 500 instead."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The intent (shape per network; a sandbox request gets the simulated descriptor).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutIntent"
                },
                "examples": {
                  "evm": {
                    "$ref": "#/components/examples/CheckoutIntentEvmExample"
                  },
                  "sandbox": {
                    "$ref": "#/components/examples/CheckoutIntentSandboxExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_payer_address, invalid_request (Shape A); invalid_json (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/CheckoutPaymentRequestNotFound"
          },
          "409": {
            "description": "payment_link_unavailable (with top-level reason), tenant_not_active, payment_request_not_payable, x402_settlement_in_progress, currency_not_allowlisted (Shape A).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/PaymentLinkUnavailableError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeA"
          },
          "503": {
            "description": "service_paused, signing_busy (Retry-After), disburser_paused, payment_authorizer_mismatch, authorizer_cluster_mismatch, payroll_network_not_configured, payroll_deployment_not_audited, authorizer_network_mismatch, chain_unavailable (Shape A, error_type api_error; never details).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payment-requests/{requestId}/confirm": {
      "post": {
        "operationId": "confirmPaymentRequest",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Report the payer's transaction; the relay re-derives it from the chain",
        "description": "Anonymous. Never trusts the hash: the network's adapter re-reads the transaction and the disburser's own Disbursed event for this request's allocation id, recipient, token and amount before anything is written. TON: send `boc` (the TonConnect sendTransaction result); the relay derives the message hash as the stored reference; a jetton (USDT, USDe, FDUSD) payment without a parseable wallet external message is 400 invalid_boc, and is paid only once its delivery hops are proven on chain; TON has no `failed` outcome. Tron: the verdict is the transaction's own receipt on the solidity node (walletsolidity/gettransactioninfobyid), no longer TronGrid's event index; an unreadable disburser log or a successful disburser call with no log answers pending, not failed; a node answering another transaction is a 500. On a verified payment one compare-and-set records `paid`, or `paid_after_expiry` when the window had closed or the session was expired, with the verified payer as settledPayer; for a cs_ session it also completes the session and payment and credits the ledger with the NET fiat settlement. The webhook (payment_request.paid.paid_after_expiry for a pr_ id; checkout_session.paid.paid_after_expiry for a cs_ id) goes in the background to the request's callbackUrl and the tenant's matching webhook endpoints. A request already paid answers its recorded payment (a different hash is checked on chain for the operator's log only). After expiry a live request still records a real late settlement; a sandbox one answers 409. Not refused for a suspended tenant: a payment already sent is still recorded. On EVM a token whose address this deployment no longer configures is a 500, never a verdict; on Solana a token request with no configured mint is failed token_mismatch. A failed RPC read is a 500 before expiry and a 409 expired after it on networks with a detection sweep (EVM, Solana, Tron); TON's typed 400/503 stay. Sandbox: any well-formed hash pays; 0x+'e'*64 answers pending and 0x+'f'*64 failed sandbox_forced_failure - usable only where the network's hash format accepts 0x-hex (EVM, Tron, Aptos). Open while the kill switch is on. Idempotent by outcome (one compare-and-set), no Idempotency-Key layer. No audit row. Shared per-IP public limiter.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentRequestIdPath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaymentRequestConfirmRequest"
              },
              "examples": {
                "evm": {
                  "value": {
                    "txHash": "0x0000000000000000000000000000000000000000000000000000000000000001"
                  }
                },
                "ton": {
                  "value": {
                    "boc": "te6cckEBAQEAAgAAAEysuc0="
                  }
                }
              }
            }
          }
        },
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_boc",
            "envelope": "A",
            "when": "TON boc not a single-root BOC, or a TON jetton confirm without a wallet external message"
          },
          {
            "status": 400,
            "code": "invalid_tx_hash",
            "envelope": "A",
            "when": "txHash not this network's format"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "A",
            "when": "network without an adapter (unreachable for a validated network)",
            "note": "The same slug in Shape B answers a path parameter that is not valid percent-encoding and a body body-parser refused."
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "payment_request_not_found",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "payment_request_not_payable",
            "envelope": "A",
            "when": "expired with no late settlement found, or a status that is neither pending nor paid"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 503,
            "code": "chain_unavailable",
            "envelope": "A",
            "when": "TON: the is_allocation_used read or a jetton delivery read failed (nothing recorded; retry)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "A",
            "when": "untyped failure (an RPC error before expiry, a missing setting, Tron node answering another tx)",
            "note": "A failure outside the route's own catch (its loader, the body parser) is the global handler's Shape B 500 instead."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The outcome.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestConfirmResult"
                },
                "examples": {
                  "paid": {
                    "value": {
                      "status": "paid",
                      "txHash": "0x0000000000000000000000000000000000000000000000000000000000000001"
                    }
                  },
                  "pending": {
                    "value": {
                      "status": "pending",
                      "txHash": "0x0000000000000000000000000000000000000000000000000000000000000001"
                    }
                  },
                  "failed": {
                    "value": {
                      "status": "failed",
                      "txHash": "0x0000000000000000000000000000000000000000000000000000000000000001",
                      "reason": "no_disbursed_event"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_boc, invalid_tx_hash, invalid_request (Shape A); invalid_json (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/CheckoutPaymentRequestNotFound"
          },
          "409": {
            "description": "payment_request_not_payable (Shape A).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeA"
          },
          "503": {
            "description": "chain_unavailable (TON), or another adapter's typed 503 under its own slug (Shape A).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payment-requests/{requestId}/qr": {
      "get": {
        "operationId": "getPaymentRequestQr",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "QR code (SVG) of the checkout URL or the Solana Pay request",
        "description": "Deterministic SVG (qrcode-generator, error correction M, quiet zone 2) of `type=checkout` (default: `<base>/checkout/index.html?id=<id>`, every network) or `type=solana-pay` (`solana:<base>/v1/payment-requests/<id>/solana-pay`, live Solana requests only). `<base>` is the request's own mode's origin. `size` is 1 to 5 digits and is CLAMPED to 128-1024 (default 256), not refused, when out of range; anything else is 400 invalid_qr_size. Cache-Control private, max-age=300. Shared per-IP public limiter. No side effect.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentRequestIdPath"
          },
          {
            "$ref": "#/components/parameters/CheckoutQrType"
          },
          {
            "$ref": "#/components/parameters/CheckoutQrSize"
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_qr_type",
            "envelope": "A",
            "when": "type not checkout or solana-pay (also when repeated)"
          },
          {
            "status": 400,
            "code": "invalid_qr_size",
            "envelope": "A",
            "when": "size not 1-5 digits"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 404,
            "code": "payment_request_not_found",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "qr_type_not_available",
            "envelope": "A",
            "when": "solana-pay on a non-Solana or sandbox request"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The SVG.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Cache-Control": {
                "description": "`private, max-age=300`.",
                "schema": {
                  "type": "string",
                  "const": "private, max-age=300"
                }
              }
            },
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "invalid_qr_type, invalid_qr_size (Shape A); invalid_request (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/CheckoutPaymentRequestNotFound"
          },
          "409": {
            "description": "qr_type_not_available (Shape A).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payment-requests/{requestId}/solana-pay": {
      "get": {
        "operationId": "getPaymentRequestSolanaPayLabel",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Solana Pay GET - label and icon",
        "description": "The Solana Pay transaction request's GET (https://docs.solanapay.com/spec): `{label, icon}`, the label being the tenant's name (or \"FastPay\") and the icon `<base>/checkout/fastpay-icon.svg`. CORS headers are set before the lookup, so its 404 carries them; a 404 for any request whose network is not solana. A sandbox request still answers 200 here (only the POST refuses it). Shared per-IP public limiter; its 429 is sent before the CORS headers. No side effect.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentRequestIdPath"
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "payment_request_not_found",
            "envelope": "A",
            "when": "unknown id or not a Solana request; body carries the spec's top-level message"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A",
            "when": "no CORS header, no message field"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "Label and icon.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/CheckoutCorsAllowOriginAny"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayLabel"
                },
                "examples": {
                  "label": {
                    "value": {
                      "label": "Example Merchant Ltd",
                      "icon": "https://relay.example.com/checkout/fastpay-icon.svg"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "404": {
            "description": "payment_request_not_found in the Solana Pay error body.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/CheckoutCorsAllowOriginAny"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayError"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      },
      "post": {
        "operationId": "createPaymentRequestSolanaPayTransaction",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Solana Pay POST - the unsigned transaction for the scanning wallet",
        "description": "The wallet posts `{account}`; the relay answers the same unsigned transaction POST.../intent builds (fulfillment.buildPaymentIntent, FastPay's authorization and the disburser's own on-chain fee included) re-shaped as `{transaction, message}` with message \"Pay <amount> <currency> for <reference>\". The wallet signs and broadcasts and never calls /confirm: the payment detection sweep finds it by the reference key every Solana intent carries. Refusals, in order: not a Solana request 404; a sandbox request 409 payment_request_not_payable; `account` missing 400 invalid_payer_address; a link with no use left 409 payment_link_unavailable (no `reason` field here); then the intent's own refusals as on POST.../intent (409 tenant_not_active for a suspended merchant among them), every one in the Solana Pay body (the error triad plus a top-level `message`), with Retry-After on signing_busy. CORS-open. Records `payer` and intent_served_at like the intent route. Shared per-IP public limiter.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentRequestIdPath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SolanaPayTransactionRequest"
              }
            }
          }
        },
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "payment_request_not_found",
            "envelope": "A",
            "when": "unknown id or not Solana"
          },
          {
            "status": 409,
            "code": "payment_request_not_payable",
            "envelope": "A",
            "when": "sandbox request, or not pending, or expired"
          },
          {
            "status": 400,
            "code": "invalid_payer_address",
            "envelope": "A",
            "when": "account missing"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "A",
            "when": "account not a Solana public key",
            "note": "The same slug in Shape B answers a path parameter that is not valid percent-encoding and a body body-parser refused."
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "no CORS header"
          },
          {
            "status": 409,
            "code": "payment_link_unavailable",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "tenant_not_active",
            "envelope": "A",
            "when": "the merchant's tenant is suspended; nothing is signed"
          },
          {
            "status": 409,
            "code": "x402_settlement_in_progress",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "currency_not_allowlisted",
            "envelope": "A"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A",
            "when": "no CORS header"
          },
          {
            "status": 503,
            "code": "service_paused",
            "envelope": "A"
          },
          {
            "status": 503,
            "code": "signing_busy",
            "envelope": "A"
          },
          {
            "status": 503,
            "code": "payment_authorizer_mismatch",
            "envelope": "A"
          },
          {
            "status": 503,
            "code": "authorizer_cluster_mismatch",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "A",
            "note": "A failure outside the route's own catch (its loader, the body parser) is the global handler's Shape B 500 instead."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The transaction.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/CheckoutCorsAllowOriginAny"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayTransactionResponse"
                }
              }
            }
          },
          "400": {
            "description": "invalid_payer_address, invalid_request (Solana Pay body); invalid_json (Shape B, no CORS).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/SolanaPayError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "payment_request_not_found (Solana Pay body).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayError"
                }
              }
            }
          },
          "409": {
            "description": "payment_request_not_payable, payment_link_unavailable, tenant_not_active, x402_settlement_in_progress, currency_not_allowlisted (Solana Pay body).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayError"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "description": "internal_error (Solana Pay body).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayError"
                }
              }
            }
          },
          "503": {
            "description": "service_paused, signing_busy (Retry-After), payment_authorizer_mismatch, authorizer_cluster_mismatch (Solana Pay body).",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/checkout/sessions": {
      "post": {
        "operationId": "createCheckoutSession",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Create a fiat-quoted checkout session (rate locked at creation)",
        "description": "A fiat amount (EUR or USD) quoted into a crypto settlement currency on one network, the rate locked now. Checks, in order: authentication (checkout:write) and the tenant Idempotency-Key layer; the per-tenant limiter; the fields (amount, currency, success_url, cancel_url, network, settlement_currency, callback_url with its public DNS check); the tenant's payout wallet on `network` (400 missing_recipient: a session never takes a recipient from the body); the rate (503 rate_unavailable); the public base URL (the production host, for a sandbox key the sandbox host else the same; unset, 500 internal_error before anything is written). It then writes, in one transaction, the session (open), its payment (created), the payment request with the same cs_ id (pending, expiring the checkout session expiry (30 minutes) = 30 min later), its status history and a pending payment attempt. The quote: payment_amount = amount x rate truncated to the token's decimals; the gateway fee (the gateway fee percentage) is taken in fiat cents and the NET is what the merchant's ledger is credited at settlement. payment_url is built on that base URL. the checkout session expiry (30 minutes) may not exceed the payment request maximum expiry (7 days): the relay refuses to start otherwise. The payer pays through the same public payment-request routes with the cs_ id. A session past its expiry stays `open` until expired by hand. Idempotency: tenant layer only - without a key every retry creates another session; a stored 503 rate_unavailable or 500 replays for 24 h under the same key. No webhook, no audit row at creation. Sandbox key: is_test rows, the sandbox ledger at settlement, the same tenant payout wallet (wallets have no mode).",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutSessionCreateRequest"
              },
              "examples": {
                "eurToUsdcOnSolana": {
                  "$ref": "#/components/examples/CheckoutSessionCreateExample"
                }
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:write",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "the key was used by another API key of this tenant, or its stored answer does not open under this key (a row stored unsealed before the 2026-10-28 deploy, until it expires)"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_amount",
            "envelope": "B",
            "when": "not a positive decimal string, more than 2 decimals or 78 digits"
          },
          {
            "status": 400,
            "code": "invalid_currency",
            "envelope": "B",
            "when": "not EUR or USD in any letter case"
          },
          {
            "status": 400,
            "code": "invalid_url",
            "envelope": "B",
            "when": "success_url, cancel_url or callback_url not an http(s) string (callback_url null included), or callback_url not public"
          },
          {
            "status": 400,
            "code": "invalid_network",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_settlement_currency",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "missing_recipient",
            "envelope": "B",
            "when": "no tenant payout wallet on network"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 503,
            "code": "rate_unavailable",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "e.g. the production host (or, for a sandbox key with the sandbox host unset, the same variable) unset: resolved before the write, so nothing is left behind; or a non-string merchant_reference or customer.id"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "201": {
            "description": "Created; the checkout_session object plus the quote's provenance.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSessionCreated"
                },
                "examples": {
                  "created": {
                    "$ref": "#/components/examples/CheckoutSessionCreatedExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_amount, invalid_currency, invalid_url, invalid_network, invalid_settlement_currency, missing_recipient, invalid_json (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "409": {
            "$ref": "#/components/responses/CheckoutIdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          },
          "503": {
            "description": "rate_unavailable (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listCheckoutSessions",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "List checkout sessions",
        "description": "This tenant's sessions in the key's mode, newest first by rowid, each with its payments and network from two batched reads. Filters: status (open, completed, expired - the stored status: a session past its window is still `open`), customer_id, created[gte]/created[lte]. payment_url uses the session's own mode's origin, falling back to the request's host when unset. Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "$ref": "#/components/parameters/CreatedGte"
          },
          {
            "$ref": "#/components/parameters/CreatedLte"
          },
          {
            "$ref": "#/components/parameters/CheckoutSessionStatusFilter"
          },
          {
            "$ref": "#/components/parameters/CheckoutCustomerIdFilter"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:read",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "A page.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSessionList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutListParameterError"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/checkout/sessions/{id}": {
      "get": {
        "operationId": "getCheckoutSession",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Retrieve a checkout session",
        "description": "The checkout_session object; the create response's rate_source/rate_fetched_at are not stored and do not appear here. Another tenant's, the other mode's or an unknown id: 404 resource_missing (Shape B). Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CheckoutSessionIdPath"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:read",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The session.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSession"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutResourceMissing"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/checkout/sessions/{id}/expire": {
      "post": {
        "operationId": "expireCheckoutSession",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Expire an open checkout session now",
        "description": "In one transaction: the session open -> expired, its created payment -> expired with expires_at now and a history row, and its payment request's expires_at pulled back to now. A payer who already holds a signed authorization can still pay until that authorization's own expiry: such a settlement is recorded paid_after_expiry and leaves the session expired. Answers the whole session as it now stands. A session already completed or expired: 409 checkout_session_not_expirable (the state machine). No body is read. Tenant Idempotency-Key layer: a retry under the same key replays the first 200; without a key a retry answers 409. No webhook, no audit row. No per-tenant limiter.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CheckoutSessionIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:write",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "the key was used by another API key of this tenant, or its stored answer does not open under this key (a row stored unsealed before the 2026-10-28 deploy, until it expires)"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "checkout_session_not_expirable",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The session, now expired.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSession"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutResourceMissing"
          },
          "409": {
            "description": "checkout_session_not_expirable, or an Idempotency-Key conflict (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payments": {
      "get": {
        "operationId": "listPayments",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "List checkout payments",
        "description": "Each item is exactly the GET /v1/payments/{id} object, assembled from one batched query per table. Only checkout-session payments (pay_) exist; a bare pr_ request has no payment. Filters: status (created, paid, expired: the payment's own status - a late settlement leaves it created or expired and shows paid_after_expiry only in `lifecycle`), checkout_session_id, created[gte]/created[lte]. Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "$ref": "#/components/parameters/CreatedGte"
          },
          {
            "$ref": "#/components/parameters/CreatedLte"
          },
          {
            "$ref": "#/components/parameters/CheckoutPaymentStatusFilter"
          },
          {
            "$ref": "#/components/parameters/CheckoutSessionIdFilter"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:read",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "A page.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutPaymentList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutListParameterError"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payments/{id}": {
      "get": {
        "operationId": "getPayment",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Retrieve a payment with its history, money and chain data",
        "description": "lifecycle (every status in order), financial (fiat amount, quoted crypto amount and rate, the newest attempt's fees, net settlement, refunded and refundable amounts counting pending and confirmed refunds), blockchain (the newest attempt), related ids. 404 resource_missing (Shape B) for another tenant's, the other mode's or an unknown id. Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CheckoutPaymentIdPath"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:read",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The payment.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutPayment"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutResourceMissing"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payments/{id}/transactions": {
      "get": {
        "operationId": "listPaymentTransactions",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Every on-chain attempt of a payment",
        "description": "Every payment_attempts row of the payment, newest first, not paged (has_more always false), with the confirmation count the confirmation sweep keeps and the network's required count (12 on EVM unless a per-network override is set, 1 on Tron, Solana, Aptos, Sui, TON). Attempt status: pending, confirmed, reorged. Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CheckoutPaymentIdPath"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:read",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The attempts.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutBlockchainTransactionList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutResourceMissing"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payments/{id}/refunds": {
      "post": {
        "operationId": "createRefund",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Create a refund and get the unsigned transaction the merchant's wallet signs",
        "description": "Only a checkout-session payment (pay_) can be refunded; FastPay never held the funds, so the answer is an unsigned transaction FROM the merchant's own wallet (the payment attempt's address, i.e. the tenant payout wallet at session creation) TO the destination. Checks, in order: authentication (scope refunds:write - the route is matched before the /v1/payments rule, on the path in lower case) and the tenant Idempotency-Key layer; the payment in this tenant and mode (404); its payment request paid by either literal (409 payment_not_refundable); a live payment on a refund network (400 unsupported_network); amount a positive decimal string; the destination - `destination`, else the payer verified at settlement, else 400 missing_destination (TON, sandbox and pre-2026-10-18 payments have none); a live destination validated for the network (400 invalid_destination; a sandbox one is not checked). Then, in one transaction, the pending refund row reserves its amount against what is still refundable (pending + confirmed, in the payment's crypto currency) and computes its fiat share of the net settlement in cents (400 invalid_amount past the token's decimals, settlement_amount_too_small, amount_exceeds_refundable). The adapter then builds the transaction; if that fails the refund is marked failed `build_failed` (releasing the reservation) and the error is answered in Shape B with its own status, slug and details. Audit row refund.created (best effort, actor tenant only). No webhook at creation. No maker-checker: the approval policy does not apply to refunds. A pending refund expires after the pending refund expiry (72 h) if never confirmed. Sandbox: the simulator's descriptor, no chain. Idempotency: tenant layer only; without a key every retry reserves another refund. No per-tenant limiter.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CheckoutPaymentIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutRefundCreateRequest"
              },
              "examples": {
                "partial": {
                  "value": {
                    "amount": "5.000000",
                    "reason": "customer request"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "refunds:write",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "the key was used by another API key of this tenant, or its stored answer does not open under this key (a row stored unsealed before the 2026-10-28 deploy, until it expires)"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payment_not_refundable",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "unsupported_network",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_amount",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "missing_destination",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_destination",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "settlement_amount_too_small",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "amount_exceeds_refundable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "currency_not_configured",
            "envelope": "B",
            "when": "the token has no address on this deployment"
          },
          {
            "status": 503,
            "code": "payroll_network_not_configured",
            "envelope": "B",
            "when": "TON jetton with no wallet-code pin"
          },
          {
            "status": 503,
            "code": "chain_unavailable",
            "envelope": "B",
            "when": "TON jetton wallet does not verify"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B",
            "when": "untyped build failure (refund marked failed build_failed), or a non-string reason/sandbox destination"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "201": {
            "description": "The refund (pending) and its unsigned transaction.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutRefundCreated"
                },
                "examples": {
                  "evmToken": {
                    "$ref": "#/components/examples/CheckoutRefundCreatedExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "unsupported_network, invalid_amount, missing_destination, invalid_destination, settlement_amount_too_small, amount_exceeds_refundable, invalid_request, invalid_json (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutResourceMissing"
          },
          "409": {
            "description": "payment_not_refundable, currency_not_configured, or an Idempotency-Key conflict (Shape B; an adapter's typed refusal keeps its details).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          },
          "503": {
            "description": "An adapter's typed 503 (payroll_network_not_configured, chain_unavailable), Shape B with details, no Retry-After.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/refunds": {
      "get": {
        "operationId": "listRefunds",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "List refunds",
        "description": "This tenant's refunds in the key's mode, newest first by rowid. Filters: status (pending, confirmed, failed, expired), payment_id, created[gte]/created[lte]. Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "$ref": "#/components/parameters/CreatedGte"
          },
          {
            "$ref": "#/components/parameters/CreatedLte"
          },
          {
            "$ref": "#/components/parameters/CheckoutRefundStatusFilter"
          },
          {
            "$ref": "#/components/parameters/CheckoutPaymentIdFilter"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "refunds:read",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "A page.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutRefundList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutListParameterError"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/refunds/{id}": {
      "get": {
        "operationId": "getRefund",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Retrieve a refund",
        "description": "The refund object. 404 resource_missing (Shape B) otherwise. Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CheckoutRefundIdPath"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "refunds:read",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The refund.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutRefund"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutResourceMissing"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/refunds/{id}/confirm": {
      "post": {
        "operationId": "confirmRefund",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Report the merchant's refund transaction; the relay re-verifies it",
        "description": "Merchant-authenticated. A refund no longer pending answers its current state at once, no chain read. Otherwise txHash must have the network's format (400 invalid_tx_hash), then, under the refund's own in-process lock (the expiry sweep skips a refund whose confirm is running), the adapter checks the transaction moved this refund's token and amount to its destination FROM the merchant's wallet. Verdicts: pending (not readable as landed yet; answers tx_hash null; nothing written); mismatch (answers status failed with a failure_reason - reverted_not_this_refund, no_matching_transfer, sender_mismatch, not_a_transaction_hash - and the refund STAYS pending; nothing written); tx_reverted on the refund's own transaction (terminal: failed, reservation released, audit refund.failed, webhook refund.failed); confirmed: in one transaction the one-transfer-one-refund guard (409 refund_tx_already_used, live only), pending -> confirmed, the double-entry ledger debit of the refund's fiat share, audit refund.confirmed; then webhook refund.confirmed. The webhook goes to the session's callbackUrl and the tenant's matching endpoints; its payload carries is_test. Pitfall: the tenant Idempotency-Key layer stores the pending answer for 24 h - poll with a new key, or none. Sandbox: the simulator (0x+'e'*64 pending, 0x+'f'*64 reverted, anything else confirmed), no transfer guard. No per-tenant limiter.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CheckoutRefundIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutRefundConfirmRequest"
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "refunds:write",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "the key was used by another API key of this tenant, or its stored answer does not open under this key (a row stored unsealed before the 2026-10-28 deploy, until it expires)"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_tx_hash",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "refund_tx_already_used",
            "envelope": "B",
            "when": "the matching transfer already confirmed another live refund"
          },
          {
            "status": 409,
            "code": "currency_not_configured",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The refund's state after this call.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutRefundStatus"
                },
                "examples": {
                  "confirmed": {
                    "value": {
                      "id": "re_00000000000000000000000000000000",
                      "object": "refund",
                      "status": "confirmed",
                      "tx_hash": "0x0000000000000000000000000000000000000000000000000000000000000002",
                      "failure_reason": null
                    }
                  },
                  "mismatch": {
                    "value": {
                      "id": "re_00000000000000000000000000000000",
                      "object": "refund",
                      "status": "failed",
                      "tx_hash": "0x0000000000000000000000000000000000000000000000000000000000000002",
                      "failure_reason": "no_matching_transfer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_tx_hash, invalid_request, invalid_json (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutResourceMissing"
          },
          "409": {
            "description": "refund_tx_already_used (global handler, error.param null, error.request_id set), currency_not_configured, or an Idempotency-Key conflict (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payment-links": {
      "post": {
        "operationId": "createPaymentLink",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Create a reusable payment link",
        "description": "A link fixes network, currency and recipient, and either a fixed amount or a customer-entered one bounded by min_amount/max_amount; every open creates an ordinary payment request. Checks, in order: authentication (checkout:write) and the tenant Idempotency-Key layer; the per-tenant limiter; a missing recipient filled from the tenant's payout wallet on network (400 missing_recipient); the body; callback_url's public DNS check. Stored active, in the key's mode. `max_uses` counts PAID requests, never opens; it is checked at open and again before an intent or an x402 payment, but intents already served can still all pay. The link's metadata and callback_url are copied onto every request it opens. Payments of a link are bare payment requests: payment_request.paid webhook (with payment_link_id), no ledger credit. No audit row, no webhook at creation. Idempotency: tenant layer only. A `recipient` in the body wins over the registered wallet whatever address it is (format only).",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaymentLinkCreateRequest"
              },
              "examples": {
                "fixedUsdcOnSolana": {
                  "$ref": "#/components/examples/PaymentLinkCreateExample"
                }
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:write",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "the key was used by another API key of this tenant, or its stored answer does not open under this key (a row stored unsealed before the 2026-10-28 deploy, until it expires)"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "missing_recipient",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "A",
            "when": "body not an object - unreachable through this route, which spreads the body into an object first",
            "note": "The same slug in Shape B answers a path parameter that is not valid percent-encoding and a body body-parser refused."
          },
          {
            "status": 400,
            "code": "invalid_network",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_currency",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_amount",
            "envelope": "A",
            "when": "amount invalid, or amount with min/max, or min > max"
          },
          {
            "status": 400,
            "code": "invalid_min_amount",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_max_amount",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_recipient",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_reference_prefix",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_session_ttl",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_max_uses",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_expires_at",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_metadata",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_callback_url",
            "envelope": "A",
            "when": "format or public-address check"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "A",
            "note": "A failure outside the route's own catch (its loader, the body parser) is the global handler's Shape B 500 instead."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "201": {
            "description": "The link (tenant view).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLinkTenantView"
                },
                "examples": {
                  "created": {
                    "$ref": "#/components/examples/PaymentLinkTenantViewExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "The validation slugs above (Shape A); invalid_json (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "409": {
            "$ref": "#/components/responses/CheckoutIdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeA"
          }
        }
      },
      "get": {
        "operationId": "listPaymentLinks",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "List payment links",
        "description": "This tenant's links in the key's mode, newest first, each with its paid count from one GROUP BY query. Filters: status (active, inactive - the stored status; `available` and `unavailable_reason` say whether it can be opened now), network, created[gte]/created[lte]. Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "$ref": "#/components/parameters/CreatedGte"
          },
          {
            "$ref": "#/components/parameters/CreatedLte"
          },
          {
            "$ref": "#/components/parameters/PaymentLinkStatusFilter"
          },
          {
            "$ref": "#/components/parameters/CheckoutNetworkFilter"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:read",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "A",
            "note": "A failure outside the route's own catch (its loader, the body parser) is the global handler's Shape B 500 instead."
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "A page.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLinkList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutListParameterError"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeA"
          }
        }
      }
    },
    "/v1/payment-links/{linkId}": {
      "get": {
        "operationId": "getPaymentLink",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Retrieve a payment link (tenant view)",
        "description": "The link with its use count and availability. Another tenant's, the other mode's or an unknown id: flat 404 payment_link_not_found. Read-only.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentLinkIdPath"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:read",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "payment_link_not_found",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The link.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLinkTenantView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutPaymentLinkNotFound"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payment-links/{linkId}/deactivate": {
      "post": {
        "operationId": "deactivatePaymentLink",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Deactivate a payment link",
        "description": "Sets status inactive (idempotent: already inactive answers 200 again) and answers the tenant view. New opens answer 409 payment_link_unavailable reason inactive; requests already opened keep their own expiry and stay payable. No body read, no audit row, no webhook. Tenant Idempotency-Key layer.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentLinkIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:write",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "the key was used by another API key of this tenant, or its stored answer does not open under this key (a row stored unsealed before the 2026-10-28 deploy, until it expires)"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "payment_link_not_found",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The link, inactive.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLinkTenantView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutPaymentLinkNotFound"
          },
          "409": {
            "$ref": "#/components/responses/CheckoutIdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payment-links/{linkId}/activate": {
      "post": {
        "operationId": "activatePaymentLink",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Reactivate a payment link",
        "description": "Sets status active (idempotent) and answers the tenant view. An expired or exhausted link stays unavailable (`available` false). No body read, no audit row, no webhook. Tenant Idempotency-Key layer.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentLinkIdPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:write",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "when": "the key was used by another API key of this tenant, or its stored answer does not open under this key (a row stored unsealed before the 2026-10-28 deploy, until it expires)"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 404,
            "code": "payment_link_not_found",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The link, active.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLinkTenantView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutPaymentLinkNotFound"
          },
          "409": {
            "$ref": "#/components/responses/CheckoutIdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payment-links/{linkId}/public": {
      "get": {
        "operationId": "getPublicPaymentLink",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Public view of a payment link (what link.html renders)",
        "description": "By id alone, any tenant and mode. Never metadata, callback_url, tenant id or the use count; adds merchant_name (the tenant's name or null). The tenant's status is not read for `available`: a suspended tenant's link reads available, and only the open answers 409 tenant_not_active. Shared per-IP public limiter. Read-only.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentLinkIdPath"
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "payment_link_not_found",
            "envelope": "A"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The public view.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLinkPublicView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "404": {
            "$ref": "#/components/responses/CheckoutPaymentLinkNotFound"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payment-links/{linkId}/sessions": {
      "post": {
        "operationId": "openPaymentLinkSession",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Open a payment link (creates the visitor's own payment request)",
        "description": "Anonymous. Refuses first a link whose tenant is suspended (409 tenant_not_active, no request opened), then an inactive, expired or exhausted link (409 payment_link_unavailable with reason); on a fixed-amount link an `amount`, when sent, must be a decimal string equal in base units to the fixed amount (a different, malformed or non-string value is 400 amount_not_customizable); on a customer-entered link `amount` is required and bounded. Creates a pending payment request (reference `<reference_prefix or link id>-<8 hex>`, expiring session_ttl_seconds from now, the link's metadata and callback_url copied, payment_link_id set) in the LINK's mode. Every call inserts a row - nothing reuses an open unpaid request. No Idempotency-Key layer, no webhook, no audit row. Shared per-IP public limiter.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentLinkIdPath"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaymentLinkSessionRequest"
              }
            }
          }
        },
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "payment_link_not_found",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "tenant_not_active",
            "envelope": "A",
            "when": "the link's tenant is suspended; checked first, before the link's availability, and no request is opened"
          },
          {
            "status": 409,
            "code": "payment_link_unavailable",
            "envelope": "A",
            "when": "reason inactive, expired or exhausted"
          },
          {
            "status": 400,
            "code": "amount_not_customizable",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_amount",
            "envelope": "A",
            "when": "missing on a customer-entered link, or not a valid amount at the token's precision"
          },
          {
            "status": 400,
            "code": "amount_below_minimum",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "amount_above_maximum",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "A",
            "note": "A failure outside the route's own catch (its loader, the body parser) is the global handler's Shape B 500 instead."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "201": {
            "description": "The visitor's payment request and where to pay it.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLinkSessionCreated"
                },
                "examples": {
                  "opened": {
                    "value": {
                      "payment_request_id": "pr_00000000000000000000000000000000",
                      "payment_link_id": "plink_00000000000000000000000000000000",
                      "checkout_url": "https://relay.example.com/checkout/index.html?id=pr_00000000000000000000000000000000",
                      "expires_at": "2026-10-30T10:30:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "amount_not_customizable, invalid_amount, amount_below_minimum, amount_above_maximum (Shape A); invalid_request, invalid_json (Shape B).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/CheckoutPaymentLinkNotFound"
          },
          "409": {
            "description": "payment_link_unavailable with its reason, or tenant_not_active (Shape A).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/PaymentLinkUnavailableError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeA"
          }
        }
      }
    },
    "/v1/payment-links/{linkId}/qr": {
      "get": {
        "operationId": "getPaymentLinkQr",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "QR code (SVG) of the link page or its reusable Solana Pay request",
        "description": "`type=checkout` encodes `<base>/checkout/link.html?id=<plink>`; `type=solana-pay` encodes `solana:<base>/v1/payment-links/<plink>/solana-pay`, only for a live, fixed-amount Solana link. Same size rule and headers as the payment-request QR. Shared per-IP public limiter. No side effect.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentLinkIdPath"
          },
          {
            "$ref": "#/components/parameters/CheckoutQrType"
          },
          {
            "$ref": "#/components/parameters/CheckoutQrSize"
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "payment_link_not_found",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_qr_type",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_qr_size",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "qr_type_not_available",
            "envelope": "A",
            "when": "solana-pay on a non-Solana, sandbox or customer-entered-amount link"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The SVG.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "Cache-Control": {
                "description": "`private, max-age=300`.",
                "schema": {
                  "type": "string",
                  "const": "private, max-age=300"
                }
              }
            },
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "invalid_qr_type, invalid_qr_size (Shape A); invalid_request (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/CheckoutPaymentLinkNotFound"
          },
          "409": {
            "description": "qr_type_not_available (Shape A).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/payment-links/{linkId}/solana-pay": {
      "get": {
        "operationId": "getPaymentLinkSolanaPayLabel",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Solana Pay GET for a link - label and icon",
        "description": "`{label, icon}` for a Solana link, whatever its status or amount kind; 404 payment_link_not_found (Solana Pay body, CORS set) for an unknown or non-Solana link. Shared per-IP public limiter (its 429 has no CORS header). No side effect.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentLinkIdPath"
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "payment_link_not_found",
            "envelope": "A"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "Label and icon.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/CheckoutCorsAllowOriginAny"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayLabel"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutPathNotDecodable"
          },
          "404": {
            "description": "payment_link_not_found (Solana Pay body).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayError"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      },
      "post": {
        "operationId": "createPaymentLinkSolanaPayTransaction",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "Solana Pay POST for a link - opens a fresh payment request and returns its transaction",
        "description": "One printed code, any number of payers. Refusals: unknown or non-Solana link 404; sandbox link 409 payment_link_not_payable; customer-entered amount 409 payment_link_not_payable; `account` missing 400 invalid_payer_address. Then the link is OPENED (a new pending payment request, as POST.../sessions with no amount: a suspended tenant is 409 tenant_not_active and opens nothing; inactive, expired or exhausted is 409 payment_link_unavailable without `reason`) and the request's intent is built as on POST.../intent, in the Solana Pay body. The request is inserted before the intent is built, so a refused intent (503 service_paused, signing_busy,...) still leaves a new pending request, and so does an `account` that is no Solana key: only its presence is checked before the open. The payment is found by the detection sweep (the wallet never confirms). CORS-open. Shared per-IP public limiter.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentLinkIdPath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SolanaPayTransactionRequest"
              }
            }
          }
        },
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "payment_link_not_found",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "payment_link_not_payable",
            "envelope": "A",
            "when": "sandbox link, or no fixed amount"
          },
          {
            "status": 400,
            "code": "invalid_payer_address",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "A",
            "when": "account not a Solana public key",
            "note": "The same slug in Shape B answers a path parameter that is not valid percent-encoding and a body body-parser refused."
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "tenant_not_active",
            "envelope": "A",
            "when": "the link's tenant is suspended; checked when the link is opened, so no request is inserted"
          },
          {
            "status": 409,
            "code": "payment_link_unavailable",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "payment_request_not_payable",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "x402_settlement_in_progress",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "currency_not_allowlisted",
            "envelope": "A"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 503,
            "code": "service_paused",
            "envelope": "A"
          },
          {
            "status": 503,
            "code": "signing_busy",
            "envelope": "A"
          },
          {
            "status": 503,
            "code": "payment_authorizer_mismatch",
            "envelope": "A"
          },
          {
            "status": 503,
            "code": "authorizer_cluster_mismatch",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "A",
            "note": "A failure outside the route's own catch (its loader, the body parser) is the global handler's Shape B 500 instead."
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "The transaction for the new request.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/CheckoutCorsAllowOriginAny"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayTransactionResponse"
                }
              }
            }
          },
          "400": {
            "description": "invalid_payer_address, invalid_request (Solana Pay body); invalid_json (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/SolanaPayError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "payment_link_not_found (Solana Pay body).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayError"
                }
              }
            }
          },
          "409": {
            "description": "payment_link_not_payable, tenant_not_active, payment_link_unavailable, payment_request_not_payable, x402_settlement_in_progress, currency_not_allowlisted (Solana Pay body).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayError"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "description": "internal_error (Solana Pay body).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayError"
                }
              }
            }
          },
          "503": {
            "description": "service_paused, signing_busy (Retry-After), payment_authorizer_mismatch, authorizer_cluster_mismatch (Solana Pay body).",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolanaPayError"
                }
              }
            }
          }
        }
      }
    },
    "/v2/x402/supported": {
      "get": {
        "operationId": "getX402Supported",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "x402 facilitator - supported kinds and signers",
        "description": "The x402 v2 SupportedResponse: one kind per (scheme, enabled CAIP-2 network), with the scheme's extra (SVM: its fee payer), `extensions` empty, and the public signer addresses per CAIP-2 pattern (*.js). Built once from the configured chains: none configured answers `kinds: []`. the boot log says the facilitator is disabled. Shared per-IP public limiter. No authentication.",
        "security": [],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "The supported kinds.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402SupportedResponse"
                },
                "examples": {
                  "base": {
                    "value": {
                      "kinds": [
                        {
                          "x402Version": 2,
                          "scheme": "exact",
                          "network": "eip155:8453"
                        }
                      ],
                      "extensions": [],
                      "signers": {
                        "eip155:*": [
                          "0x3333333333333333333333333333333333333333"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CheckoutBodyRefused"
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v2/x402/verify": {
      "post": {
        "operationId": "verifyX402Payment",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "x402 facilitator - verify a payment payload",
        "description": "The seller's own tenant key (scope checkout:write). Order: authentication (the tenant Idempotency-Key layer is SKIPPED on /v2/x402/*); the per-tenant x402 limiter (default 1200, shared by verify and settle); a test key 409 x402_sandbox_not_supported (x402 has no sandbox); the body shape (400 with the x402 reason as the code); then the scheme for the requirements' network. Every verification outcome is HTTP 200 `{isValid, invalidReason?, payer?, invalidMessage?}` - a refusal is isValid false with an x402 reason (e.g. invalid_network, unsupported_scheme, facilitator_asset_not_supported, invalid_exact_evm_signature, unexpected_verify_error). Read-only on chain; not paused by the kill switch. No audit row.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/X402FacilitatorRequest"
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:write",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A",
            "when": "x402 per-tenant limiter"
          },
          {
            "status": 409,
            "code": "x402_sandbox_not_supported",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_payload",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_x402_version",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_payment_requirements",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "The verification (valid or not).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402VerifyResponse"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "isValid": false,
                      "invalidReason": "invalid_network"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_payload, invalid_x402_version, invalid_payment_requirements, invalid_json (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "409": {
            "description": "x402_sandbox_not_supported (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v2/x402/settle": {
      "post": {
        "operationId": "settleX402Payment",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "x402 facilitator - settle (broadcast) a payment",
        "description": "Same entry checks as verify. While the kill switch is on, a new settle answers `success:false, errorReason: facilitator_paused` (HTTP 200), a ledger replay as before. The settlement is recorded against the calling tenant; it does not touch any payment request (that is the seller route). Not covered by the tenant Idempotency-Key layer. No audit row.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/X402FacilitatorRequest"
              }
            }
          }
        },
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "checkout:write",
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "x402_sandbox_not_supported",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_payload",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_x402_version",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_payment_requirements",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body (aborted, or an invalid Content-Length)"
          }
        ],
        "responses": {
          "200": {
            "description": "The settlement (succeeded or not).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402SettleResponse"
                },
                "examples": {
                  "paused": {
                    "value": {
                      "success": false,
                      "errorReason": "facilitator_paused",
                      "transaction": "",
                      "network": "eip155:8453"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_payload, invalid_x402_version, invalid_payment_requirements, invalid_json (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/CheckoutTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CheckoutTenantForbidden"
          },
          "409": {
            "description": "x402_sandbox_not_supported (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/CheckoutInternalErrorShapeB"
          }
        }
      }
    },
    "/v1/x402/pay/{requestId}": {
      "get": {
        "operationId": "getX402PaymentRequired",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "x402 seller - pay a pending payment request over x402",
        "description": "Every pending payment request is payable by an x402 client. GET and POST behave identically (no body is read). Under a per-request in-process lock, in order: unknown id 404 (Shape B here); a paid request answers 200 with its recorded payment; expired, or fewer than the minimum seller timeout (15) left, 410 payment_request_expired; a link with no use left 409 payment_link_unavailable (Shape B, no reason); not x402-payable 409 x402_<reason> (sandbox_not_supported, network_not_enabled, native_currency_not_supported, currency_not_supported, recipient_token_account_missing, recipient_invalid, recipient_store_frozen:, {evm,svm,aptos}.js); the production host unset 500; a PAYMENT-SIGNATURE that is not a base64 x402 v2 PaymentPayload 400; an x402 settlement of it in flight 409 x402_settlement_in_progress (not broadcast) or 402 settlement_pending with its PAYMENT-RESPONSE (broadcast); a checkout intent served within its authorization window + 60 s 409 x402_checkout_intent_outstanding, after a 409 tenant_not_active for a suspended merchant; no PAYMENT-SIGNATURE: 402 with PAYMENT-REQUIRED (scheme exact, the CAIP-2 network, the atomic amount, the asset, payTo = the request's recipient, maxTimeoutSeconds = min(the maximum seller timeout 300, seconds left), extra with paymentFlow upfront). With it: the request is marked settling (checkout intents then refuse it), a checkout payment already landed is looked for first, the relay's own facilitator settles, and the payment is recorded like a confirmed checkout payment (payment_request.paid webhook; a cs_ session is completed and credited). A failed settle is 402 with the reason in error_slug and PAYMENT-RESPONSE. Kill switch: a new settle is 402 facilitator_paused. A recording failure after the transfer landed is a 500 that still carries PAYMENT-RESPONSE; the settlement sweep records it later, now also while x402 is disabled on every chain. Shared per-IP public limiter. Not covered by any Idempotency-Key layer; the settlement ledger dedupes a repeated payload.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentRequestIdPath"
          },
          {
            "$ref": "#/components/parameters/X402PaymentSignatureHeader"
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "payment_request_not_found",
            "envelope": "B"
          },
          {
            "status": 410,
            "code": "payment_request_expired",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payment_request_not_payable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payment_link_unavailable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_sandbox_not_supported",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_network_not_enabled",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_native_currency_not_supported",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_currency_not_supported",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_recipient_token_account_missing",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_recipient_invalid",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_recipient_store_frozen",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_settlement_in_progress",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "tenant_not_active",
            "envelope": "B",
            "when": "the merchant's tenant is suspended: nothing is advertised or settled (after the in-flight replay, before the checkout-intent window)"
          },
          {
            "status": 409,
            "code": "x402_checkout_intent_outstanding",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_payload",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_x402_version",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 402,
            "code": "payment_required",
            "envelope": "A",
            "when": "no PAYMENT-SIGNATURE",
            "note": "The x402 PaymentRequired body: `error` is a string, plus error_type payment_required and error_slug."
          },
          {
            "status": 402,
            "code": "settlement_pending",
            "envelope": "A",
            "note": "The x402 PaymentRequired body: `error` is a string, plus error_type payment_required and error_slug."
          },
          {
            "status": 402,
            "code": "facilitator_paused",
            "envelope": "A",
            "when": "the kill switch is on: a new settle is refused unsigned",
            "note": "The x402 PaymentRequired body: `error` is a string, plus error_type payment_required and error_slug."
          },
          {
            "status": 402,
            "code": "<x402 errorReason>",
            "envelope": "A",
            "when": "a failed settle: its errorReason, e.g. invalid_exact_evm_signature or insufficient_funds",
            "note": "The x402 PaymentRequired body: `error` is a string, plus error_type payment_required and error_slug."
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "Paid - now, earlier, or twice (duplicateSettlement). PAYMENT-RESPONSE is set when this call settled.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "PAYMENT-RESPONSE": {
                "$ref": "#/components/headers/X402PaymentResponse"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402PaidRequestBody"
                }
              }
            }
          },
          "400": {
            "description": "invalid_payload, invalid_x402_version (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Payment required, a settlement pending, or a failed settle (PAYMENT-RESPONSE carries the settle result).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "PAYMENT-REQUIRED": {
                "$ref": "#/components/headers/X402PaymentRequired"
              },
              "PAYMENT-RESPONSE": {
                "$ref": "#/components/headers/X402PaymentResponse"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402PaymentRequiredBody"
                },
                "examples": {
                  "required": {
                    "$ref": "#/components/examples/X402PaymentRequiredExample"
                  }
                }
              }
            }
          },
          "404": {
            "description": "payment_request_not_found (Shape B here, Shape A on the other public routes).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The 409 slugs above, tenant_not_active included (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "payment_request_expired (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "description": "internal_error (Shape B); carries PAYMENT-RESPONSE when the transfer landed but recording failed.",
            "headers": {
              "PAYMENT-RESPONSE": {
                "$ref": "#/components/headers/X402PaymentResponse"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "payX402PaymentRequest",
        "tags": [
          "Checkout and x402"
        ],
        "summary": "x402 seller - same flow as the GET",
        "description": "Identical to the GET; the request body is ignored.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentRequestIdPath"
          },
          {
            "$ref": "#/components/parameters/X402PaymentSignatureHeader"
          }
        ],
        "x-fastpay-credential": "public",
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "payment_request_not_found",
            "envelope": "B"
          },
          {
            "status": 410,
            "code": "payment_request_expired",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payment_request_not_payable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "payment_link_unavailable",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_sandbox_not_supported",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_network_not_enabled",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_native_currency_not_supported",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_currency_not_supported",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_recipient_token_account_missing",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_recipient_invalid",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_recipient_store_frozen",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "x402_settlement_in_progress",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "tenant_not_active",
            "envelope": "B",
            "when": "the merchant's tenant is suspended: nothing is advertised or settled (after the in-flight replay, before the checkout-intent window)"
          },
          {
            "status": 409,
            "code": "x402_checkout_intent_outstanding",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_payload",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_x402_version",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "when": "a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length)"
          },
          {
            "status": 402,
            "code": "payment_required",
            "envelope": "A",
            "when": "no PAYMENT-SIGNATURE",
            "note": "The x402 PaymentRequired body: `error` is a string, plus error_type payment_required and error_slug."
          },
          {
            "status": 402,
            "code": "settlement_pending",
            "envelope": "A",
            "note": "The x402 PaymentRequired body: `error` is a string, plus error_type payment_required and error_slug."
          },
          {
            "status": 402,
            "code": "facilitator_paused",
            "envelope": "A",
            "when": "the kill switch is on: a new settle is refused unsigned",
            "note": "The x402 PaymentRequired body: `error` is a string, plus error_type payment_required and error_slug."
          },
          {
            "status": 402,
            "code": "<x402 errorReason>",
            "envelope": "A",
            "when": "a failed settle: its errorReason, e.g. invalid_exact_evm_signature or insufficient_funds",
            "note": "The x402 PaymentRequired body: `error` is a string, plus error_type payment_required and error_slug."
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B",
            "when": "the body is sent as JSON but does not parse (body-parser runs before every route)"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "when": "body-parser refused the body: an unsupported charset or Content-Encoding"
          }
        ],
        "responses": {
          "200": {
            "description": "Paid - now, earlier, or twice (duplicateSettlement). PAYMENT-RESPONSE is set when this call settled.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "PAYMENT-RESPONSE": {
                "$ref": "#/components/headers/X402PaymentResponse"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402PaidRequestBody"
                }
              }
            }
          },
          "400": {
            "description": "invalid_payload, invalid_x402_version (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Payment required, a settlement pending, or a failed settle (PAYMENT-RESPONSE carries the settle result).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "PAYMENT-REQUIRED": {
                "$ref": "#/components/headers/X402PaymentRequired"
              },
              "PAYMENT-RESPONSE": {
                "$ref": "#/components/headers/X402PaymentResponse"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402PaymentRequiredBody"
                },
                "examples": {
                  "required": {
                    "$ref": "#/components/examples/X402PaymentRequiredExample"
                  }
                }
              }
            }
          },
          "404": {
            "description": "payment_request_not_found (Shape B here, Shape A on the other public routes).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The 409 slugs above, tenant_not_active included (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "payment_request_expired (Shape B).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/CheckoutBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/CheckoutBodyEncodingUnsupported"
          },
          "429": {
            "$ref": "#/components/responses/CheckoutRateLimited"
          },
          "500": {
            "description": "internal_error (Shape B); carries PAYMENT-RESPONSE when the transfer landed but recording failed.",
            "headers": {
              "PAYMENT-RESPONSE": {
                "$ref": "#/components/headers/X402PaymentResponse"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ecommerce/{platform}/webhook/{tenantId}": {
      "post": {
        "operationId": "receiveEcommercePlatformWebhook",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Inbound e-commerce platform webhook (called by the platform; HMAC over the raw body)",
        "description": "Public: the caller is the e-commerce platform, not a tenant; no API key, no rate limiter, no Idempotency-Key layer. Registered before the app-wide JSON parser, the error-field wrapper and the X-Request-ID middleware (vs), so: the body is read raw with express.raw for any Content-Type up to the request body limit (default 46,384 bytes = 50 x 600 + 16,384); no response carries X-Request-ID or X-API-Version; its own refusals are Shape A WITHOUT error_message and request_id (only error_type, error_slug, error). Steps, first failure answers: platform lower-cased and checked (404 invalid_platform); the tenant's stored settings for that platform looked up by the path tenantId - an unknown tenant, an unconnected platform and an empty stored secret all answer the same 404 not_connected; the platform header HMAC (timing-safe; a missing or malformed signature is a mismatch) - 401 invalid_signature (error_type auth_error); the raw body parsed as JSON (400 invalid_json, Shape A here, unlike the body-parser's Shape B invalid_json elsewhere); the adapter (stateless: it reads no tenant config) and its parseWebhookPayload (400 invalid_payload carrying the adapter's message, e.g. \"Invalid Shopify callback payload: id is required\"). On success the relay only writes an info log line (orderId and status) and answers 200 {received: true}: nothing is created, paid or changed, no audit row, no webhook. The tenant status is not checked: a suspended tenant's platform webhooks are still verified and logged. The sandbox/live mode does not apply: settings are per tenant and platform, one row for both modes. A body over the limit is refused by express.raw before the route: Shape B 413 request_body_too_large from the global handler, with request_id null. 400 signature_check_failed is coded but unreachable in practice: the check only throws for a body that is not a Buffer.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/EcommercePlatformPath"
          },
          {
            "$ref": "#/components/parameters/EcommerceWebhookTenantIdPath"
          },
          {
            "name": "x-shopify-hmac-sha256",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "shopify: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, base64-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-wc-webhook-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "woocommerce: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, base64-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-whmcs-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "whmcs: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, hex-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-whmcs-token",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "whmcs: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, hex-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-magento-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "magento: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, base64-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-magento-hmac-sha256",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "magento: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, base64-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-prestashop-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "prestashop: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, hex-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-wix-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "wix: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, hex-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-wix-webhook-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "wix: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, hex-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-squarespace-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "squarespace: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, hex-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-squarespace-webhook-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "squarespace: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, hex-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-bc-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "bigcommerce: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, hex-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-bigcommerce-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "bigcommerce: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, hex-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-oc-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "opencart: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, hex-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          },
          {
            "name": "x-opencart-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "opencart: HMAC-SHA256 of the exact raw body under the tenant's stored webhook_secret, hex-encoded (SIGNATURE_HEADERS and SIGNATURE_DIGEST). Only the headers of the path's platform are read, in the adapter's order, first non-empty wins."
          }
        ],
        "requestBody": {
          "required": false,
          "description": "The platform's own webhook body, any Content-Type, read as raw bytes; must be JSON once verified.",
          "content": {
            "*/*": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              },
              "examples": {
                "shopify": {
                  "value": {
                    "id": 1001,
                    "checkout_id": 2002,
                    "total_price": "49.90",
                    "financial_status": "paid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verified and parsed. Nothing is actioned (log line only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EcommerceWebhookReceived"
                },
                "examples": {
                  "ok": {
                    "value": {
                      "received": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape A without error_message/request_id: invalid_json (raw body not JSON), invalid_payload (adapter refused the payload), signature_check_failed (coded, unreachable). Shape B invalid_request for an undecodable path parameter (global handler, request_id null).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EcommerceWebhookError"
                },
                "examples": {
                  "invalidPayload": {
                    "value": {
                      "error_type": "request_error",
                      "error_slug": "invalid_payload",
                      "error": "Invalid Shopify callback payload: id is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Shape A invalid_signature, error_type auth_error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EcommerceWebhookError"
                },
                "examples": {
                  "badSig": {
                    "value": {
                      "error_type": "auth_error",
                      "error_slug": "invalid_signature",
                      "error": "Webhook signature verification failed."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Shape A invalid_platform (unsupported platform) or not_connected (no settings, or an empty secret, for this tenant and platform; the same answer for a tenant that does not exist). The settings routes answer an unsupported platform with 400, this one with 404.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EcommerceWebhookError"
                },
                "examples": {
                  "notConnected": {
                    "value": {
                      "error_type": "request_error",
                      "error_slug": "not_connected",
                      "error": "No such webhook endpoint."
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "Shape B request_body_too_large from express.raw via the global handler; request_id null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Shape B invalid_request: an unsupported Content-Encoding (express.raw inflates gzip and deflate only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Shape B internal_error (an unexpected throw, e.g. a database error), request_id null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 404,
            "code": "invalid_platform",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "not_connected",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "signature_check_failed",
            "envelope": "A",
            "note": "unreachable: rawBody is always a Buffer"
          },
          {
            "status": 401,
            "code": "invalid_signature",
            "envelope": "A",
            "note": "error_type auth_error"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "A",
            "note": "the route's own, Shape A"
          },
          {
            "status": 400,
            "code": "invalid_payload",
            "envelope": "A"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B",
            "note": "request_id null"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "other"
      }
    },
    "/v1/networks": {
      "get": {
        "operationId": "listCatalogNetworks",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Public network catalogue (checkout networks)",
        "description": "Static, public, unthrottled; no database or RPC read. One entry per CHECKOUT network (from): the 12 EVM networks, then solana, tron, aptos, sui, ton. NOT the payroll networks (no stacks, no bitcoin) and NOT the mass-payout/recipient networks (no bitcoin, litecoin). `live` is true for every entry at (LIVE_NETWORKS is exactly the checkout set): it means \"has a checkout adapter\", not \"RPC configured\", not \"payroll enabled\", not \"lane open\". List body, has_more always false; no query parameter is read.",
        "security": [],
        "responses": {
          "200": {
            "description": "Every checkout network.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogNetworkList"
                },
                "examples": {
                  "list": {
                    "$ref": "#/components/examples/CatalogNetworkListExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape B invalid_json only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload); the parser runs on every route, a GET with a JSON body included"
          }
        ],
        "x-fastpay-credential": "public"
      }
    },
    "/v1/currencies": {
      "get": {
        "operationId": "listCatalogCurrencies",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Public currency catalogue (per checkout network)",
        "description": "Static, public, unthrottled: one row per (network, currency) of the checkout networks: the native coin plus every stablecoin with a verified address on that chain. `decimals` is the token's own (BNB USDC is 18: getDecimalsForNetwork), `type` native|token, `live` as in GET /v1/networks. It says nothing about which currencies payroll allows on a network (payroll answers 409 currency_not_allowlisted from its own allow-list). Order: networks in catalogue order, currencies in set order.",
        "security": [],
        "responses": {
          "200": {
            "description": "Every (network, currency) pair.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogCurrencyList"
                },
                "examples": {
                  "list": {
                    "$ref": "#/components/examples/CatalogCurrencyListExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape B invalid_json only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload); the parser runs on every route, a GET with a JSON body included"
          }
        ],
        "x-fastpay-credential": "public"
      }
    },
    "/v1/reconciliation": {
      "get": {
        "operationId": "getReconciliationReport",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Reconciliation report of paid checkout payments over a window",
        "description": "Read-only cross-check, in the key's mode, of every checkout payment that reached a paid literal (paid or paid_after_expiry) inside [from, to] by its payment_status_history time, against its latest payment attempt and its payable ledger credit. Window: `to` defaults to now, `from` to the default window (30) before `to`; a date-only `to` runs to the end of that UTC day; at most the maximum window (92) apart. Each bound is ISO-8601 or 9-11 digit unix seconds. Refusals, Shape B 400: invalid_date (unreadable, empty string, repeated or bracketed parameter, or from later than to), window_too_large. Discrepancy types: chain_unconfirmed, chain_reorged, ledger_missing, ledger_amount_mismatch. Payroll batches, refunds and channel deposits are not reconciled here. Payments are ordered by paid time; totals are exact cents per settlement currency.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ReconciliationFrom"
          },
          {
            "$ref": "#/components/parameters/ReconciliationTo"
          }
        ],
        "responses": {
          "200": {
            "description": "The report.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationReport"
                },
                "examples": {
                  "oneMismatch": {
                    "$ref": "#/components/examples/ReconciliationReportExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape B invalid_date, window_too_large, invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "tooLarge": {
                    "$ref": "#/components/examples/ReconciliationWindowTooLargeExample"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbidden"
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/LedgerPayoutsInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_date",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "window_too_large",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "balance:read"
      }
    },
    "/v1/customers": {
      "post": {
        "operationId": "createCustomer",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Create a customer (a checkout/channel payer record)",
        "description": "Creates a customer in the key's mode. Validation order: email (required, a string of at most 254 characters matching ^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$, stored as sent, case kept: Shape B 400 parameter_missing), name (optional string, at most 200: 400 invalid_name), country (optional string, at most 64: 400 invalid_country). An empty name or country is stored as null. No uniqueness: the same email can be created again (a retry without Idempotency-Key makes a second customer). No audit row, no webhook. Customers are used by checkout sessions and payment channels only, never by payroll.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerCreateRequest"
              },
              "examples": {
                "basic": {
                  "value": {
                    "email": "payer@example.com",
                    "name": "Example Payer Ltd",
                    "country": "GB"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The customer.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                },
                "examples": {
                  "created": {
                    "$ref": "#/components/examples/CustomerExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape B parameter_missing (email missing or malformed), invalid_name, invalid_country, invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbidden"
          },
          "409": {
            "$ref": "#/components/responses/LedgerPayoutsIdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/LedgerPayoutsInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "note": "another API key of the tenant claimed this key on this path and mode; or the stored answer does not open with the calling key: one stored unsealed by a release before, until it expires (24 hours, 24 hours), neither replayed nor run again"
          },
          {
            "status": 400,
            "code": "parameter_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_name",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_country",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "customers:write"
      },
      "get": {
        "operationId": "listCustomers",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "List customers",
        "description": "Newest first by insertion (rowid), paged, in the key's mode; filters: created[gte|lte] only (no email or name search). An unknown cursor answers the first page.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "$ref": "#/components/parameters/CreatedGte"
          },
          {
            "$ref": "#/components/parameters/CreatedLte"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of customers.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerList"
                }
              }
            }
          },
          "400": {
            "description": "Shape B invalid_pagination_parameter, invalid_filter_parameter, invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbidden"
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/LedgerPayoutsInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_pagination_parameter",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_filter_parameter",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "customers:read"
      }
    },
    "/v1/customers/{id}": {
      "get": {
        "operationId": "retrieveCustomer",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Retrieve a customer",
        "description": "The customer in the key's mode; unknown, foreign or other-mode id: Shape B 404 resource_missing.",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CustomerIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "The customer.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                },
                "examples": {
                  "customer": {
                    "$ref": "#/components/examples/CustomerExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape B invalid_request (undecodable id) or invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbidden"
          },
          "404": {
            "description": "Shape B resource_missing \"No such customer.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/LedgerPayoutsInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "resource_missing",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "a path parameter that is not valid percent-encoding"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "customers:read"
      }
    },
    "/v1/accounting/{provider}/connect": {
      "get": {
        "operationId": "startAccountingConnection",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Start an accounting OAuth connection (authorization URL and state)",
        "description": "Steps: a sandbox key: Shape A 403 live_key_required; provider check (400 invalid_provider); configured = the provider's client id env var is set (XERO_CLIENT_ID, QUICKBOOKS_CLIENT_ID, ZOHO_CLIENT_ID, Sage's) - else Shape A 501 provider_not_configured, error_type configuration_error (the client secret is not checked). Then redirect_uri, required: absent or empty, Shape A 400 parameter_missing; not one https URL with a host, in printable ASCII without backslash, user info or fragment, at most 2048 characters (a repeated parameter is an array and fails too), Shape A 400 invalid_url. It is the integrator's own page: the provider sends the person's browser there with code and state in the query string, and that page POSTs both to POST /v1/accounting/{provider}/callback with the API key (the relay serves no GET for it). Register the same URL with the provider: it is stored with the state and sent to the provider exactly as given. Then a 32-hex state and a PKCE S256 verifier are generated and stored server-side for ACCOUNTING_OAUTH_STATE_TTL_MS (10 minutes) with the redirect URI: a GET that writes a row, reachable with integrations:read. Neither ACCOUNTING_TOKEN_ENCRYPTION_KEY nor the client secret is checked here, so a deployment missing either lets the person complete the provider's consent and fails only at the callback. Answers {provider, authorizationUrl, state}. No audit row. Accounting settings are per tenant and provider, ONE row for both modes, so since a sandbox (fp_test_) key is refused with Shape A 403 live_key_required (error_type permission_error) right after the key's own checks and the Idempotency-Key layer, before the provider check: only a live key reads or writes the connection, its OAuth states and the mapping. Provider in the path is lower-cased; only xero, quickbooks, zoho and sage; anything else is Shape A 400 invalid_provider. Needs integrations:*",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AccountingProviderPath"
          },
          {
            "$ref": "#/components/parameters/AccountingRedirectUriQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Where to send the person's browser.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountingConnectStart"
                },
                "examples": {
                  "xero": {
                    "$ref": "#/components/examples/AccountingConnectStartExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape A invalid_provider, parameter_missing (no redirect_uri), invalid_url (redirect_uri refused); Shape B invalid_request or invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbiddenLiveKey"
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/LedgerPayoutsInternalError"
          },
          "501": {
            "description": "Shape A provider_not_configured, error_type configuration_error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "notConfigured": {
                    "$ref": "#/components/examples/AccountingNotConfiguredExample"
                  }
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "live_key_required",
            "envelope": "A",
            "note": "error_type permission_error, a sandbox (fp_test_) key; runs after the key's own checks and the Idempotency-Key layer, before the route's own checks"
          },
          {
            "status": 400,
            "code": "invalid_provider",
            "envelope": "A"
          },
          {
            "status": 501,
            "code": "provider_not_configured",
            "envelope": "A",
            "note": "error_type configuration_error"
          },
          {
            "status": 400,
            "code": "parameter_missing",
            "envelope": "A",
            "note": "redirect_uri absent or empty"
          },
          {
            "status": 400,
            "code": "invalid_url",
            "envelope": "A",
            "note": "redirect_uri not one https URL with a host, printable ASCII, no backslash, user info or fragment, at most 2048 characters (a repeated parameter included)"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "a path parameter that is not valid percent-encoding"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "integrations:read"
      }
    },
    "/v1/accounting/{provider}/callback": {
      "post": {
        "operationId": "completeAccountingConnection",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Finish the OAuth code exchange (single-use state, PKCE)",
        "description": "Whole body in one try/catch: anything unexpected answers Shape A 500 oauth_exchange_failed, never the provider's text. Order: a sandbox key (403 live_key_required, before the try); provider (400 invalid_provider); code truthy (400 missing_code); state truthy (400 missing_state); the state row is read AND DELETED, then checked to be this tenant's, this provider's and unexpired (400 invalid_state) - a wrong-tenant attempt therefore burns the state; configured check (501 provider_not_configured, AFTER the state is consumed: the person must start again at connect); token exchange with the stored redirect URI and verifier; tokens stored AES-256-GCM-encrypted under ACCOUNTING_TOKEN_ENCRYPTION_KEY - with the key unset or malformed the store throws after the provider has issued the tokens: 500 oauth_exchange_failed, tokens discarded, state and code spent; audit row accounting.connected (not a safe write: if it fails the tokens are saved and the answer is 500 oauth_exchange_failed). Types of code and state are not checked: a number state is looked up as is (400 invalid_state); a non-string code is sent to the provider as text. Accounting settings are per tenant and provider, ONE row for both modes, so since a sandbox (fp_test_) key is refused with Shape A 403 live_key_required (error_type permission_error) right after the key's own checks and the Idempotency-Key layer, before the provider check: only a live key reads or writes the connection, its OAuth states and the mapping. Provider in the path is lower-cased; only xero, quickbooks, zoho and sage; anything else is Shape A 400 invalid_provider. Needs integrations:*",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AccountingProviderPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountingCallbackRequest"
              },
              "examples": {
                "callback": {
                  "value": {
                    "code": "EXAMPLE_AUTH_CODE",
                    "state": "0123456789abcdef0123456789abcdef"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Connected.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountingConnected"
                },
                "examples": {
                  "connected": {
                    "value": {
                      "success": true,
                      "provider": "xero",
                      "connected": true,
                      "expiresAt": "2026-10-24T11:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape A invalid_provider, missing_code, missing_state, invalid_state; Shape B invalid_request or invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbiddenLiveKey"
          },
          "409": {
            "$ref": "#/components/responses/LedgerPayoutsIdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "500": {
            "description": "Shape A oauth_exchange_failed (any failure inside the handler); Shape B internal_error outside it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "Shape A provider_not_configured (configuration_error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "note": "another API key of the tenant claimed this key on this path and mode; or the stored answer does not open with the calling key: one stored unsealed by a release before, until it expires (24 hours, 24 hours), neither replayed nor run again"
          },
          {
            "status": 403,
            "code": "live_key_required",
            "envelope": "A",
            "note": "error_type permission_error, a sandbox (fp_test_) key; runs after the key's own checks and the Idempotency-Key layer, before the route's own checks"
          },
          {
            "status": 400,
            "code": "invalid_provider",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "missing_code",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "missing_state",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_state",
            "envelope": "A"
          },
          {
            "status": 501,
            "code": "provider_not_configured",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "oauth_exchange_failed",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "a path parameter that is not valid percent-encoding"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "integrations:write"
      }
    },
    "/v1/accounting/{provider}/mappings": {
      "get": {
        "operationId": "getAccountingMappings",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Accounting connection status and GL mapping",
        "description": "Answers {provider, connected (an access token is stored), expiresAt (token expiry or null), mapping (the stored object, {} when none)}. Tokens are decrypted to compute connected: with stored tokens and no ACCOUNTING_TOKEN_ENCRYPTION_KEY the read throws, 500. No token is ever returned. Accounting settings are per tenant and provider, ONE row for both modes, so since a sandbox (fp_test_) key is refused with Shape A 403 live_key_required (error_type permission_error) right after the key's own checks and the Idempotency-Key layer, before the provider check: only a live key reads or writes the connection, its OAuth states and the mapping. Provider in the path is lower-cased; only xero, quickbooks, zoho and sage; anything else is Shape A 400 invalid_provider. Needs integrations:*",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AccountingProviderPath"
          }
        ],
        "responses": {
          "200": {
            "description": "Status and mapping.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountingMappingsView"
                },
                "examples": {
                  "connected": {
                    "$ref": "#/components/examples/AccountingMappingsViewExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape A invalid_provider; Shape B invalid_request or invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbiddenLiveKey"
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/LedgerPayoutsInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "live_key_required",
            "envelope": "A",
            "note": "error_type permission_error, a sandbox (fp_test_) key; runs after the key's own checks and the Idempotency-Key layer, before the route's own checks"
          },
          {
            "status": 400,
            "code": "invalid_provider",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "a path parameter that is not valid percent-encoding"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "integrations:read"
      },
      "post": {
        "operationId": "saveAccountingMappings",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Replace the GL mapping",
        "description": "Stores `mapping` as given (any JSON object, arrays included - typeof object), REPLACING the previous mapping, creating the settings row if needed; no key, size or value validation beyond the body limit. What the sync reads from it: disbursement/gateway/defaults sections and the provider ids xeroTenantId, quickbooksCompanyId, zohoOrgId; contact names are not read; a crypto asset needs currencyCode (ISO 4217) for a standalone gateway sync. Answers the mapping echoed. NO audit row (unlike the e-commerce settings write). Accounting settings are per tenant and provider, ONE row for both modes, so since a sandbox (fp_test_) key is refused with Shape A 403 live_key_required (error_type permission_error) right after the key's own checks and the Idempotency-Key layer, before the provider check: only a live key reads or writes the connection, its OAuth states and the mapping. Provider in the path is lower-cased; only xero, quickbooks, zoho and sage; anything else is Shape A 400 invalid_provider. Needs integrations:*",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AccountingProviderPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountingMappingsRequest"
              },
              "examples": {
                "mapping": {
                  "$ref": "#/components/examples/AccountingMappingsRequestExample"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountingMappingsSaved"
                }
              }
            }
          },
          "400": {
            "description": "Shape A invalid_provider, invalid_mapping (absent, null or not an object); Shape B invalid_request or invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbiddenLiveKey"
          },
          "409": {
            "$ref": "#/components/responses/LedgerPayoutsIdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/LedgerPayoutsInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "note": "another API key of the tenant claimed this key on this path and mode; or the stored answer does not open with the calling key: one stored unsealed by a release before, until it expires (24 hours, 24 hours), neither replayed nor run again"
          },
          {
            "status": 403,
            "code": "live_key_required",
            "envelope": "A",
            "note": "error_type permission_error, a sandbox (fp_test_) key; runs after the key's own checks and the Idempotency-Key layer, before the route's own checks"
          },
          {
            "status": 400,
            "code": "invalid_provider",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_mapping",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "a path parameter that is not valid percent-encoding"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "integrations:write"
      }
    },
    "/v1/accounting/{provider}/sync": {
      "post": {
        "operationId": "syncAccountingRecord",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Push one completed relay payout or one paid gateway payment to the accounting provider",
        "description": "Order: a sandbox key (403 live_key_required, before the try); provider (400 invalid_provider); type and id both truthy (Shape A 400 invalid_request - same slug as the global handler's Shape B invalid_request, different shape); connection (400 not_connected); configured (501 provider_not_configured, configuration_error); then by type: disbursement = a live relay mass payout by ALLOCATION_ID (404 payout_not_found; not completed with a tx hash 409 payout_not_completed; 404 job_not_found) pushed as a Bill + Payment; gateway = a paid live payment request or checkout session id (404 payment_request_not_found; 400 payment_not_paid) pushed as an Invoice + Payment; anything else 400 invalid_type (checked last, after the connection). A gateway sync with no fiat side needs currencyCode in the mapping (400 currency_not_mapped, nothing pushed). Payroll items have no sync type. Resumable and replay-safe per (tenant, provider, type, id): a completed sync answers its stored receipt again without pushing. A concurrent sync of the same record raises a 409-status AccountingAPIError that the route does not map: it answers 500 sync_failed. Any other failure: Shape A 500 sync_failed with the provider's message and apiResponse, URLs masked. Audit row accounting.synced {type, id} after the push (not a safe write). The service's two sandbox refusals, 409 sandbox_payout_not_synced and 409 sandbox_payment_not_synced, are not reachable through this route: a sandbox key is refused first, and a live key reads live records only, so a sandbox id answers 404 payout_not_found or payment_request_not_found. Tokens past expiry are refreshed before the push; a provider that answers the refresh without a new refresh token (Zoho) keeps the stored one. Accounting settings are per tenant and provider, ONE row for both modes, so since a sandbox (fp_test_) key is refused with Shape A 403 live_key_required (error_type permission_error) right after the key's own checks and the Idempotency-Key layer, before the provider check: only a live key reads or writes the connection, its OAuth states and the mapping. Provider in the path is lower-cased; only xero, quickbooks, zoho and sage; anything else is Shape A 400 invalid_provider. Needs integrations:*",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AccountingProviderPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountingSyncRequest"
              },
              "examples": {
                "gateway": {
                  "value": {
                    "type": "gateway",
                    "id": "cs_0000000000000000000000000000d001"
                  }
                },
                "disbursement": {
                  "value": {
                    "type": "disbursement",
                    "id": "example-allocation-0001"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The receipt (fresh or replayed).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountingSyncResult"
                },
                "examples": {
                  "gateway": {
                    "$ref": "#/components/examples/AccountingSyncResultExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape A invalid_provider, invalid_request, not_connected, payment_not_paid, invalid_type, currency_not_mapped; Shape B invalid_request (path) or invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbiddenLiveKey"
          },
          "404": {
            "description": "Shape A payout_not_found, job_not_found, payment_request_not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Shape A payout_not_completed; Shape B idempotency_* conflicts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "500": {
            "description": "Shape A sync_failed (with top-level apiResponse); Shape B internal_error outside the try.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "Shape A provider_not_configured (configuration_error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "note": "another API key of the tenant claimed this key on this path and mode; or the stored answer does not open with the calling key: one stored unsealed by a release before, until it expires (24 hours, 24 hours), neither replayed nor run again"
          },
          {
            "status": 403,
            "code": "live_key_required",
            "envelope": "A",
            "note": "error_type permission_error, a sandbox (fp_test_) key; runs after the key's own checks and the Idempotency-Key layer, before the route's own checks"
          },
          {
            "status": 400,
            "code": "invalid_provider",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "A",
            "note": "Shape A from the route when type or id is missing; the same status and slug come in Shape B from the global handler for a path parameter that is not valid percent-encoding"
          },
          {
            "status": 400,
            "code": "not_connected",
            "envelope": "A"
          },
          {
            "status": 501,
            "code": "provider_not_configured",
            "envelope": "A",
            "note": "configuration_error"
          },
          {
            "status": 404,
            "code": "payout_not_found",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "payout_not_completed",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "job_not_found",
            "envelope": "A"
          },
          {
            "status": 404,
            "code": "payment_request_not_found",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "payment_not_paid",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_type",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "currency_not_mapped",
            "envelope": "A"
          },
          {
            "status": 500,
            "code": "sync_failed",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "integrations:write"
      }
    },
    "/v1/ecommerce/{platform}/settings": {
      "post": {
        "operationId": "saveEcommerceSettings",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Save an e-commerce platform's webhook secret and config; returns the webhook URL to register",
        "description": "Order: a sandbox key (403 live_key_required); platform (400 invalid_platform); webhook_secret absent or a string (400 invalid_webhook_secret; null is refused; no length or strength rule; \"\" is accepted and leaves the platform not connected); config absent or a plain object (400 invalid_config). Then webhook_url = the production host + /v1/ecommerce/<platform>/webhook/<tenant id> is built BEFORE anything is written: with the variable unset the answer is Shape B 500 internal_error and nothing is saved (production sets https://relay.f-pay.com). Then the upsert (an absent field keeps the stored value), then audit row ecommerce.settings_updated (target the platform; no secret; not a safe write: if it fails the settings are saved and the answer is 500). The URL carries the tenant id. E-commerce settings are per tenant and platform, ONE row for both modes, so since a sandbox (fp_test_) key is refused with Shape A 403 live_key_required (error_type permission_error) right after the key's own checks (and, on the POST, the Idempotency-Key layer), before the platform check. The secret is stored as sent, in clear, and never returned. The config is stored and returned as given; no adapter reads it. Platform in the path is lower-cased; one of whmcs, shopify, woocommerce, magento, prestashop, wix, squarespace, bigcommerce, opencart; else Shape A 400 invalid_platform. Needs integrations:*",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/EcommercePlatformPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EcommerceSettingsRequest"
              },
              "examples": {
                "shopify": {
                  "value": {
                    "webhook_secret": "EXAMPLE_SHARED_SECRET_NOT_REAL",
                    "config": {
                      "shopDomain": "example.myshopify.com"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EcommerceSettingsSaved"
                },
                "examples": {
                  "saved": {
                    "value": {
                      "success": true,
                      "platform": "shopify",
                      "webhook_url": "https://relay.example.com/v1/ecommerce/shopify/webhook/00000000-0000-4000-8000-000000000000"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape A invalid_platform, invalid_webhook_secret, invalid_config; Shape B invalid_request or invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbiddenLiveKey"
          },
          "409": {
            "$ref": "#/components/responses/LedgerPayoutsIdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/LedgerPayoutsInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "note": "another API key of the tenant claimed this key on this path and mode; or the stored answer does not open with the calling key: one stored unsealed by a release before, until it expires (24 hours, 24 hours), neither replayed nor run again"
          },
          {
            "status": 403,
            "code": "live_key_required",
            "envelope": "A",
            "note": "error_type permission_error, a sandbox (fp_test_) key; runs after the key's own checks and the Idempotency-Key layer, before the route's own checks"
          },
          {
            "status": 400,
            "code": "invalid_platform",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_webhook_secret",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_config",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "a path parameter that is not valid percent-encoding"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "integrations:write"
      },
      "get": {
        "operationId": "getEcommerceSettings",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "E-commerce connection status, config and webhook URL (never the secret)",
        "description": "A sandbox key is refused first (403 live_key_required). Answers {platform, connected (a non-empty secret is stored), config ({} when none), webhook_url}. The webhook_url needs the production host (else Shape B 500). The config is returned in full: do not store secrets in it. E-commerce settings are per tenant and platform, ONE row for both modes, so since a sandbox (fp_test_) key is refused with Shape A 403 live_key_required (error_type permission_error) right after the key's own checks (and, on the POST, the Idempotency-Key layer), before the platform check. The secret is stored as sent, in clear, and never returned. Platform in the path is lower-cased; one of whmcs, shopify, woocommerce, magento, prestashop, wix, squarespace, bigcommerce, opencart; else Shape A 400 invalid_platform. Needs integrations:*",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/EcommercePlatformPath"
          }
        ],
        "responses": {
          "200": {
            "description": "Status.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EcommerceSettingsView"
                }
              }
            }
          },
          "400": {
            "description": "Shape A invalid_platform; Shape B invalid_request or invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbiddenLiveKey"
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/LedgerPayoutsInternalError"
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "live_key_required",
            "envelope": "A",
            "note": "error_type permission_error, a sandbox (fp_test_) key; runs after the key's own checks and the Idempotency-Key layer, before the route's own checks"
          },
          {
            "status": 400,
            "code": "invalid_platform",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "a path parameter that is not valid percent-encoding"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "integrations:read"
      }
    },
    "/v1/ecommerce/{platform}/orders": {
      "post": {
        "operationId": "createEcommerceOrderCheckoutSession",
        "tags": [
          "Ledger, payouts and integrations"
        ],
        "summary": "Turn platform order data into a checkout session",
        "description": "Order: the per-tenant rate limiter (Shape A 429 too_many_requests; requests_per_minute, default 60, ONE budget per tenant shared with POST /v1/payroll-batches and its intents, fee quotes, confirm and sponsor, the payer-wallet writes, checkout sessions, payment requests and links, and both modes); platform (400 invalid_platform); the adapter maps the body (the platform's field names, see EcommerceOrderRequest) - a missing id, amount, currency or success URL is Shape A 400 parameter_missing naming it; defaults: network ethereum, settlementCurrency USDC, cancel URL = success URL, merchant_reference = platform prefix + id; then the checkout field checks in Shape A with the message prefixed \"Order data does not make a valid checkout session: \" (invalid_amount: positive decimal, at most 2 decimals; invalid_currency: EUR or USD; invalid_url: http(s); invalid_network: a checkout network; invalid_settlement_currency: a quotable currency of that network, any case); the quote (503 rate_unavailable Shape B when no rate) and the session. The public base URL is resolved after the wallet lookup and the quote and BEFORE the session is written: with the production host unset (or, for a sandbox key, both base URLs unset) the answer is Shape B 500 internal_error and nothing is written (production sets the variable). No customer, no callback URL, no audit row from this route. Duplicate orders create duplicate sessions (merchant_reference is not unique): send an Idempotency-Key. The route reads no settings row in either mode: the order maps from the request alone. Sandbox key: a sandbox session (payment_url on the sandbox host when set).",
        "security": [
          {
            "TenantKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/EcommercePlatformPath"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EcommerceOrderRequest"
              },
              "examples": {
                "woocommerce": {
                  "value": {
                    "orderId": "1042",
                    "amount": "49.90",
                    "currency": "EUR",
                    "returnUrl": "https://shop.example.com/thanks",
                    "cancelUrl": "https://shop.example.com/cart",
                    "network": "base",
                    "settlementCurrency": "usdc"
                  }
                },
                "shopify": {
                  "value": {
                    "id": "gid-example-1",
                    "amount": 20,
                    "currency": "usd",
                    "redirect_url": "https://shop.example.com/done"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The checkout session.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "X-Cache-Lookup": {
                "$ref": "#/components/headers/CacheLookup"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EcommerceOrderCheckoutSession"
                },
                "examples": {
                  "created": {
                    "$ref": "#/components/examples/EcommerceOrderCheckoutSessionExample"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Shape A invalid_platform, parameter_missing, invalid_amount, invalid_currency, invalid_url, invalid_network, invalid_settlement_currency, missing_recipient; Shape B invalid_request or invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/LedgerPayoutsTenantUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/LedgerPayoutsTenantForbidden"
          },
          "409": {
            "$ref": "#/components/responses/LedgerPayoutsIdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/LedgerPayoutsBodyTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/LedgerPayoutsUnsupportedMediaType"
          },
          "429": {
            "$ref": "#/components/responses/LedgerPayoutsRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/LedgerPayoutsInternalError"
          },
          "503": {
            "description": "Shape B rate_unavailable (no live or fallback rate; global handler).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-fastpay-error-codes": [
          {
            "status": 401,
            "code": "missing_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "invalid_api_key",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "api_key_expired",
            "envelope": "A"
          },
          {
            "status": 401,
            "code": "member_suspended",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "sandbox_host_requires_test_key",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "ip_not_allowed",
            "envelope": "A"
          },
          {
            "status": 403,
            "code": "insufficient_scope",
            "envelope": "A"
          },
          {
            "status": 409,
            "code": "idempotency_parameter_mismatch",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_progress",
            "envelope": "B"
          },
          {
            "status": 409,
            "code": "idempotency_key_in_use",
            "envelope": "B",
            "note": "another API key of the tenant claimed this key on this path and mode; or the stored answer does not open with the calling key: one stored unsealed by a release before, until it expires (24 hours, 24 hours), neither replayed nor run again"
          },
          {
            "status": 429,
            "code": "too_many_requests",
            "envelope": "A",
            "note": "error_type rate_limit_error; Retry-After"
          },
          {
            "status": 400,
            "code": "invalid_platform",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "parameter_missing",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_amount",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_currency",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_url",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_network",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "invalid_settlement_currency",
            "envelope": "A"
          },
          {
            "status": 400,
            "code": "missing_recipient",
            "envelope": "A"
          },
          {
            "status": 503,
            "code": "rate_unavailable",
            "envelope": "B"
          },
          {
            "status": 400,
            "code": "invalid_request",
            "envelope": "B",
            "note": "a path parameter that is not valid percent-encoding"
          },
          {
            "status": 400,
            "code": "invalid_json",
            "envelope": "B"
          },
          {
            "status": 413,
            "code": "request_body_too_large",
            "envelope": "B"
          },
          {
            "status": 415,
            "code": "invalid_request",
            "envelope": "B",
            "note": "any other body-parser refusal keeps its own 4xx (e.g. 415 unsupported charset or content-encoding, 400 aborted upload)"
          },
          {
            "status": 500,
            "code": "internal_error",
            "envelope": "B"
          }
        ],
        "x-fastpay-credential": "tenant-key",
        "x-fastpay-scope": "integrations:write"
      }
    }
  },
  "components": {
    "securitySchemes": {
      "TenantKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "fp_live_<48 hex> | fp_test_<48 hex>",
        "description": "Member-bound tenant API key (fp_live_… or fp_test_…). Keep it in your own server-side secret store."
      }
    },
    "headers": {
      "RequestId": {
        "description": "X-Request-ID, `req_` + a UUID v4, on every response (mounted), except a body-parser refusal (raised by the parsers, before) and POST /v1/ecommerce/{platform}/webhook/{tenantId} (registered, before both).",
        "schema": {
          "type": "string",
          "example": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ApiVersion": {
        "description": "X-API-Version, the API revision `2026-09-03` (mounted); same exceptions as X-Request-ID.",
        "schema": {
          "type": "string",
          "const": "2026-09-03"
        }
      },
      "CacheLookup": {
        "description": "X-Cache-Lookup `HIT` on an Idempotency-Key replay.",
        "schema": {
          "type": "string",
          "const": "HIT"
        }
      },
      "RateLimitPolicy": {
        "description": "RateLimit-Policy `<limit>;w=60` on every route behind a per-minute limiter (draft-6 headers, no X-RateLimit-*).",
        "schema": {
          "type": "string"
        }
      },
      "RateLimitLimit": {
        "description": "RateLimit-Limit, the per-minute budget of this limiter.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "RateLimit-Remaining in the current one-minute window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "RateLimit-Reset, seconds until the window resets.",
        "schema": {
          "type": "integer"
        }
      },
      "CheckoutCorsAllowOriginAny": {
        "description": "Access-Control-Allow-Origin `*`.",
        "schema": {
          "type": "string",
          "const": "*"
        }
      },
      "RetryAfter": {
        "description": "Retry-After in whole seconds: on a rate-limiter 429 (express-rate-limit 8.6.2) and on refusals that know when to come back, e.g. 503 signing_busy, payroll_signing_busy, payroll_signing_rate_limited (the payroll routes). Not on payroll's 202 payer_proof_pending, whose details.retry_after_seconds says when to send the proof again (the error carries no retry hint of its own), nor on payroll's 429 sponsorship_daily_cap.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "X402PaymentResponse": {
        "description": "Base64 of the JSON settle result {success, errorReason?, transaction, network, payer?}.",
        "schema": {
          "type": "string",
          "contentEncoding": "base64"
        }
      },
      "X402PaymentRequired": {
        "description": "Base64 of the JSON x402 PaymentRequired object (the 402 body without the error triad).",
        "schema": {
          "type": "string",
          "contentEncoding": "base64"
        }
      }
    },
    "schemas": {
      "Member": {
        "type": "object",
        "description": "A team member: the person API keys are issued to.",
        "required": [
          "id",
          "object",
          "name",
          "email",
          "role",
          "status",
          "capabilities",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^mem_[0-9a-f]{32}$"
          },
          "object": {
            "const": "member"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "email": {
            "type": "string",
            "maxLength": 254,
            "description": "Lower case, unique in the tenant, never changes."
          },
          "role": {
            "$ref": "#/components/schemas/MemberRole"
          },
          "status": {
            "$ref": "#/components/schemas/MemberStatus"
          },
          "capabilities": {
            "$ref": "#/components/schemas/MemberCapabilities"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Moves only when a field was sent."
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "Every JSON error body. Each route answers in one of two envelopes and keeps it: Shape A, `error` is the message string (sent by sendFlat); Shape B, `error` is an object {type, code, message,...} (sent by sendShapeB, and the global handler's). On top of either, the res.json wrapper adds error_type, error_slug, error_message and request_id. Branch on error_slug only; never on `error`'s type or on message text. Exceptions: POST /v1/ecommerce/{platform}/webhook/{tenantId} runs before the wrapper, so its bodies are Shape A without error_message and request_id; a body-parser refusal (400 invalid_json, 413 request_body_too_large, any other 4xx invalid_request) carries request_id null; a path parameter that is not valid percent-encoding is 400 invalid_request; GET /health's 503 is {status: 'error', error: 'Database unavailable.'} with error_type and error_slug null; the x402 seller's 402 is the x402 PaymentRequired object plus error_type payment_required and error_slug; an unknown path is Express's HTML 404, not JSON (no catch-all route: the global error handler is the last middleware).",
        "required": [
          "error_type",
          "error_slug"
        ],
        "properties": {
          "error_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "Category. Shape A: request_error (4xx other than 403), permission_error (403), api_error (5xx), unless the route names its own: authentication_error (the 401s of the key and admin checks), auth_error (the e-commerce webhook's 401), rate_limit_error (the rate limiters' 429 too_many_requests), configuration_error (501 provider_not_configured) or payment_required (the x402 seller's 402). Shape B: invalid_request_error (4xx other than 403, payroll's 429 sponsorship_daily_cap included), permission_error (403), api_error (5xx); payroll's 202 payer_proof_pending is a Shape B error body under a 2xx status, typed invalid_request_error; idempotency_error is no route's type either."
          },
          "error_slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The stable, machine-readable reason; equals error.code on Shape B. The one field to branch on."
          },
          "error_message": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable text. Its wording may change between releases."
          },
          "request_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The X-Request-ID (`req_` + UUID v4) of the call that produced this body: normally this response's X-Request-ID. On an Idempotency-Key replay (X-Cache-Lookup: HIT) it is the original call's id, stored with the body, and differs from this response's X-Request-ID header, which is new on every response (the res.json wrapper adds it before the body is stored, the replay sends the stored body, an existing request_id is kept). null on a body-parser refusal.",
            "examples": [
              "req_00000000-0000-4000-8000-000000000000"
            ]
          },
          "error": {
            "description": "The route's own envelope, kept for compatibility.",
            "oneOf": [
              {
                "type": "string",
                "description": "Shape A - the message."
              },
              {
                "type": "object",
                "description": "Shape B.",
                "required": [
                  "type",
                  "code",
                  "message"
                ],
                "properties": {
                  "type": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  },
                  "param": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Global error handler only, its generic 500 included (the payroll routes' untyped 500)."
                  },
                  "request_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Global error handler (its generic 500 included) and the Idempotency-Key 409s only."
                  },
                  "details": {
                    "description": "Structured detail of a typed refusal (payroll, validation); every embedded URL masked when it comes from a typed error; the recipient routes' per-field details are sent as built."
                  }
                },
                "additionalProperties": true
              }
            ]
          },
          "details": {
            "description": "Shape A only - structured detail of a refusal when the route sends any (any JSON value)."
          }
        },
        "additionalProperties": {}
      },
      "MemberCreateRequest": {
        "type": "object",
        "required": [
          "name",
          "email",
          "role"
        ],
        "description": "Checked in this order: name, email, role, status. Other fields are ignored.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "pattern": "\\S",
            "description": "Trimmed; 1 to 200 UTF-16 code units after the trim, so a character outside the Basic Multilingual Plane (an emoji) counts 2 there while this schema's maxLength counts it 1."
          },
          "email": {
            "type": "string",
            "maxLength": 254,
            "pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
            "description": "Trimmed and lower-cased, then at most 254 characters matching ^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$. Unique per tenant across active and suspended members."
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "developer",
              "approver",
              "viewer"
            ],
            "description": "Trimmed and lower-cased before the check, so \" Owner\" is accepted. owner: scope ceiling *, approves, manages the team and the policy, grants owner. admin: ceiling *, approves, manages the team; not the policy, not owner. developer: ceiling *, no capability. approver: every:read scope, approves. viewer: every:read scope, no capability. Registering or verifying a payroll payer wallet needs owner or admin."
          },
          "status": {
            "not": {},
            "description": "Never send it, not even null: 400 invalid_status."
          }
        }
      },
      "MemberList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          }
        ]
      },
      "MemberUpdateRequest": {
        "type": "object",
        "description": "Only the fields sent change; an empty object answers 200 and still writes an audit row. A field sent as null is refused, never read as \"unchanged\".",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "pattern": "\\S",
            "description": "Trimmed; 1 to 200 UTF-16 code units after the trim; null or whitespace only is 400 invalid_name."
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "developer",
              "approver",
              "viewer"
            ],
            "description": "Trimmed and lower-cased before the check."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "suspended"
            ],
            "description": "Exact: not trimmed, case-sensitive."
          },
          "email": {
            "not": {},
            "description": "Never send it: any value is 400 invalid_email."
          }
        }
      },
      "PayoutApprovalPolicy": {
        "type": "object",
        "required": [
          "object",
          "mode",
          "approvals_required",
          "thresholds"
        ],
        "properties": {
          "object": {
            "const": "payout_approval_policy"
          },
          "mode": {
            "$ref": "#/components/schemas/PayoutApprovalPolicyMode"
          },
          "approvals_required": {
            "type": "integer",
            "minimum": 1,
            "maximum": 3,
            "description": "Distinct approvals a waiting payroll batch needs, copied onto it at creation."
          },
          "thresholds": {
            "type": "object",
            "maxProperties": 50,
            "propertyNames": {
              "pattern": "^[A-Z0-9.]{2,20}$"
            },
            "additionalProperties": {
              "type": "string",
              "pattern": "^\\d+(\\.\\d+)?$"
            },
            "description": "CURRENCY -> plain decimal amount string; read only under mode above, but stored under any mode."
          }
        }
      },
      "PayoutApprovalPolicyRequest": {
        "type": "object",
        "required": [
          "mode"
        ],
        "description": "The whole policy; omitted fields take their defaults. Other fields are ignored.",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "never",
              "always",
              "above"
            ],
            "description": "Trimmed and lower-cased before the check."
          },
          "approvals_required": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 3,
            "default": 1,
            "description": "Omitted or null: 1. Send a JSON integer from 1 to 3. The relay is laxer than this schema: it accepts any value whose Number is an integer from 1 to 3, so \"2\", [2], \"0x2\" and true (read as 1) pass; the schema lists only what a client should send."
          },
          "thresholds": {
            "type": [
              "object",
              "null"
            ],
            "maxProperties": 50,
            "description": "Omitted or null: {}. Keys trimmed and upper-cased, then ^[A-Z0-9.]{2,20}$; two keys that normalise alike keep the last value. Values: a decimal string, trimmed by the relay, then ^\\d+(\\.\\d+)?$. The relay also takes a JSON number whose String form matches (5000 passes; 1e-7 and 1e21 are refused), so this schema asks for strings only. Required, with at least one entry, for mode above.",
            "propertyNames": {
              "pattern": "^\\s*[A-Za-z0-9.]{2,20}\\s*$"
            },
            "additionalProperties": {
              "type": "string",
              "pattern": "^\\s*\\d+(\\.\\d+)?\\s*$"
            }
          }
        },
        "if": {
          "properties": {
            "mode": {
              "const": "above"
            }
          },
          "required": [
            "mode"
          ]
        },
        "then": {
          "required": [
            "thresholds"
          ],
          "properties": {
            "thresholds": {
              "type": "object",
              "minProperties": 1
            }
          }
        }
      },
      "SandboxSummary": {
        "type": "object",
        "required": [
          "object",
          "tenant_id",
          "counts",
          "mode"
        ],
        "properties": {
          "object": {
            "const": "sandbox"
          },
          "tenant_id": {
            "type": "string"
          },
          "counts": {
            "$ref": "#/components/schemas/SandboxCounts"
          },
          "mode": {
            "type": "string",
            "enum": [
              "test",
              "live"
            ],
            "description": "The calling key's mode; the counts are the sandbox's either way."
          }
        },
        "additionalProperties": false
      },
      "SandboxResetResult": {
        "type": "object",
        "required": [
          "object",
          "tenant_id",
          "counts",
          "deleted"
        ],
        "properties": {
          "object": {
            "const": "sandbox"
          },
          "tenant_id": {
            "type": "string"
          },
          "counts": {
            "$ref": "#/components/schemas/SandboxCounts"
          },
          "deleted": {
            "$ref": "#/components/schemas/SandboxCounts"
          }
        },
        "additionalProperties": false
      },
      "ApiKeyMemberMintRequest": {
        "type": "object",
        "description": "POST /v1/members/{id}/api-keys. Every field optional; unknown fields ignored.",
        "properties": {
          "test": {
            "description": "A JSON boolean when sent (anything else 400 invalid_test). Absent or null: the calling key's mode. false from a sandbox (fp_test_) key is 403 live_key_required; a live key may mint a sandbox key. A live key needs an approved tenant.",
            "type": [
              "boolean",
              "null"
            ]
          },
          "scopes": {
            "type": [
              "array",
              "null"
            ],
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/ApiKeyScope"
            },
            "description": "Must fit the ceiling (400 scopes_exceed_role)."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "A future ISO-8601 instant; wins over expires_in_days; no upper bound."
          },
          "expires_in_days": {
            "type": [
              "number",
              "null"
            ],
            "exclusiveMinimum": 0,
            "maximum": 3650,
            "description": "Days from now. Neither field = the key never expires."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 100
          },
          "ip_allowlist": {
            "type": [
              "array",
              "null"
            ],
            "maxItems": 50,
            "items": {
              "type": "string"
            },
            "description": "As on the admin mint."
          },
          "payroll_networks": {
            "type": [
              "array",
              "null"
            ],
            "minItems": 1,
            "uniqueItems": true,
            "items": {
              "$ref": "#/components/schemas/ApiKeyPayrollNetwork"
            },
            "description": "Judged against the body's scopes, then again against the scopes the key is minted with (the role default when none are sent)."
          }
        }
      },
      "ApiKeyMinted": {
        "description": "A freshly minted key.",
        "unevaluatedProperties": false,
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKey"
          },
          {
            "type": "object",
            "required": [
              "apiKey",
              "warning"
            ],
            "properties": {
              "apiKey": {
                "type": "string",
                "minLength": 56,
                "maxLength": 56,
                "pattern": "^fp_(live|test)_.{48}$",
                "description": "The raw key, `fp_live_` or `fp_test_` + 48 lower-case hex characters (24 random bytes). Shown once; only its SHA-256 is stored, and an Idempotency-Key answer that carries it is sealed. A replay within 24 h hands it out again (audited api_key.replayed): on /admin to the same credential, on /v1 to the same calling key."
              },
              "warning": {
                "type": "string",
                "description": "\"This key is shown once and cannot be retrieved again - store it now.\" on the admin mint; \"...- hand it to the member now.\" on the member route."
              }
            }
          }
        ]
      },
      "ApiKeyList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ApiKey"
                }
              }
            }
          }
        ]
      },
      "ApiKeyIntrospection": {
        "description": "GET /v1/api-keys/me.",
        "unevaluatedProperties": false,
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKey"
          },
          {
            "type": "object",
            "required": [
              "member"
            ],
            "properties": {
              "member": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/ApiKeyIntrospectionMember"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "ApiKeyRotationRequest": {
        "type": "object",
        "properties": {
          "grace_hours": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 168,
            "default": 24,
            "description": "Hours the old key keeps working (0 revokes it at once); absent or null = 24. Coerced with Number: \"\", false and [] read as 0 (immediate revoke), true as 1 - send a JSON number. On POST /v1/api-keys/rotate keep it above 0: a lost answer under grace 0 can never be replayed."
          }
        }
      },
      "ApiKeyRotated": {
        "description": "A rotation's successor.",
        "unevaluatedProperties": false,
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKey"
          },
          {
            "type": "object",
            "required": [
              "apiKey",
              "previous_key_id",
              "previous_key_expires_at",
              "warning"
            ],
            "properties": {
              "apiKey": {
                "type": "string",
                "minLength": 56,
                "maxLength": 56,
                "pattern": "^fp_(live|test)_.{48}$",
                "description": "The successor's raw key, shown once."
              },
              "previous_key_id": {
                "type": "string",
                "description": "The rotated key."
              },
              "previous_key_expires_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When the old key stops; null when grace_hours was 0 (the old key was revoked at once)."
              },
              "warning": {
                "type": "string"
              }
            }
          }
        ]
      },
      "RelayHealthOk": {
        "type": "object",
        "required": [
          "status",
          "paused"
        ],
        "additionalProperties": false,
        "properties": {
          "status": {
            "const": "ok"
          },
          "paused": {
            "type": "boolean",
            "description": "The kill switch: true while an operator has paused the relay."
          }
        }
      },
      "RelayHealthDatabaseUnavailable": {
        "type": "object",
        "required": [
          "status",
          "error",
          "error_type",
          "error_slug",
          "error_message",
          "request_id"
        ],
        "properties": {
          "status": {
            "const": "error"
          },
          "error": {
            "const": "Database unavailable."
          },
          "error_type": {
            "type": "null"
          },
          "error_slug": {
            "type": "null"
          },
          "error_message": {
            "const": "Database unavailable."
          },
          "request_id": {
            "type": "string",
            "examples": [
              "req_00000000-0000-4000-8000-000000000000"
            ]
          }
        }
      },
      "PayerWalletRegistrationBody": {
        "type": "object",
        "description": "Unknown fields are ignored. A body that is not a JSON object: 400 invalid_request.",
        "required": [
          "network"
        ],
        "properties": {
          "network": {
            "$ref": "#/components/schemas/PayrollNetworkName"
          },
          "address": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "description": "Required on every challenge_first network (all but TON), in any form the network's adapter canonicalises; the stored and challenged form is the adapter's display form. Ignored on TON (the address comes from the proof). A present non-string or empty string: 400 invalid_address."
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 100,
            "description": "Optional, 1-100 characters as JavaScript string length (UTF-16 code units), stored as sent (not trimmed); an empty string or a non-string is 400 invalid_request."
          },
          "chain_options": {
            "type": [
              "object",
              "null"
            ],
            "description": "Live Bitcoin only (a P2WSH vault MUST send chain_options.multisig {witness_script, signer_set, cosigners}, and a single-key Bitcoin address must send none); any other network, and every network with a sandbox (fp_test_) key (the mock has no registration hook), answers 400 invalid_chain_options. Not an object, or over 8192 bytes serialised: 400 invalid_chain_options. null counts as absent."
          }
        }
      },
      "PayerWalletWithChallenge": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PayrollPayerWallet"
          },
          {
            "$ref": "#/components/schemas/PayerWalletChallengeFields"
          }
        ]
      },
      "PayrollShapeBErrorBody": {
        "description": "A payroll route's Shape B error: the nested error {type, code, message, details?} plus the top-level canonical fields. details shapes per code are in x-fastpay-details.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "required": [
              "error"
            ],
            "properties": {
              "error": {
                "type": "object",
                "required": [
                  "type",
                  "code",
                  "message"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "invalid_request_error",
                      "permission_error",
                      "api_error"
                    ]
                  },
                  "code": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  },
                  "details": {}
                }
              }
            }
          }
        ]
      },
      "PayerWalletList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PayrollPayerWallet"
                }
              }
            }
          }
        ]
      },
      "PayrollPayerWallet": {
        "type": "object",
        "description": "A payer wallet. The row belongs to the tenant: any maker may name a verified payer of the network in a batch; only its holder can sign.",
        "required": [
          "id",
          "object",
          "network",
          "chain_ref",
          "address",
          "address_kind",
          "proof_timing",
          "status",
          "verification_method",
          "verified_at",
          "revoked_at",
          "reverification_reason",
          "label",
          "challenge",
          "challenge_expires_at",
          "created_by_member_id",
          "verified_by_member_id",
          "is_test",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^pyw_[0-9a-f]{32}$"
          },
          "object": {
            "const": "payroll_payer_wallet"
          },
          "network": {
            "$ref": "#/components/schemas/PayrollNetworkName"
          },
          "chain_ref": {
            "type": "string",
            "description": "'<namespace>:<reference>' of the adapter that registered it, e.g. solana:<genesis hash>; sandbox mock:<network>.",
            "examples": [
              "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
              "mock:solana"
            ]
          },
          "address": {
            "type": [
              "string",
              "null"
            ],
            "description": "The display form; null only while an at_connect (TON) wallet has not connected."
          },
          "address_kind": {
            "type": [
              "string",
              "null"
            ],
            "description": "The adapter's kind, written at verification (valid.kind) and NOT cleared when an intent or a fee quote sends the wallet back to verification. An open string, not an enum: the DDL comment is stale. Values the adapters write: EVM eoa, eoa_7702, safe; Tron tron_account or tron_multisig; Solana solana_ed25519; Aptos aptos_ed25519, aptos_single_key, aptos_multi_key; Sui sui_ed25519, sui_secp256k1, sui_secp256r1 or sui_multisig; TON v4r2 or w5r1, NOT ton_v4r2/ton_w5r1; Bitcoin p2wpkh or p2tr_bip86 and p2wsh_sortedmulti for a vault; Stacks stacks_p2pkh; every sandbox (fp_test_) wallet eoa, whatever the network. A client must accept a value not listed here.",
            "examples": [
              "solana_ed25519",
              "eoa",
              "safe",
              "v4r2",
              "p2wpkh"
            ]
          },
          "proof_timing": {
            "type": "string",
            "enum": [
              "challenge_first",
              "at_connect"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending_verification",
              "verified",
              "revoked"
            ]
          },
          "verification_method": {
            "type": [
              "string",
              "null"
            ],
            "description": "How control was proven, written at verification and NOT cleared by a send-back to verification. `sandbox` for every fp_test_ wallet. An open string, not an enum (the DDL comment is stale). Values the live adapters return: eip191 or eip1271 (EVM); tip191_v2 (Tron); solana_sign_message; aptos_sign_message; sui_sign_personal_message, NOT the DDL's sui_personal_message, which is Sui's challenge_format; ton_proof; stx_sign_structured_message or stacks_control_tx; bip322_simple, bip137 or self_send_opreturn (Bitcoin single key) and bip322_psbt, bip322_simple or self_send_opreturn (Bitcoin vault, every method). asigna_owner_sigs and stacks_multisig_sip018 are written by no adapter.",
            "examples": [
              "solana_sign_message",
              "sandbox",
              "eip191",
              "tip191_v2"
            ]
          },
          "verified_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set at verification; NOT cleared when an intent or a fee quote sends the wallet back to pending_verification, so a pending row may carry the earlier verified_at. Decide on status."
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "reverification_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why an intent or a fee quote sent a verified wallet back to verification (URLs masked)."
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 100
          },
          "challenge": {
            "type": [
              "string",
              "null"
            ],
            "description": "The text to sign, while pending_verification only; null on a pending row whose challenge was consumed by a verify attempt or cleared by a send-back."
          },
          "challenge_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_by_member_id": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^mem_[0-9a-f]{32}$",
            "description": "The member whose key registered it; null for a key bound to no member."
          },
          "verified_by_member_id": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^mem_[0-9a-f]{32}$",
            "description": "The member whose API key submitted the accepted proof; any owner/admin key of the tenant may submit a proof the holder signed elsewhere, so it is not proof of who holds the wallet's key. Null for a key bound to no member or before the first verification; not cleared by a send-back to verification."
          },
          "is_test": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PayerWalletRevoked": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PayrollPayerWallet"
          },
          {
            "type": "object",
            "required": [
              "live_authorizations"
            ],
            "properties": {
              "live_authorizations": {
                "type": "integer",
                "minimum": 0,
                "description": "This tenant's holding authorizations (issuing, live, flagged) for this address, network and mode. Each stays executable until its expires_at."
              }
            }
          }
        ]
      },
      "PayerWalletVerifyBody": {
        "description": "Either {proof: <the network's proof>} or the proof's fields at the top level. When the body's `proof` field holds an object, that object is the proof, so a TON proof, whose own fields are {account, proof}, must be wrapped: {proof: {account, proof}}; sent flat, only its inner proof is read and the account is missing (400 invalid_proof).",
        "anyOf": [
          {
            "type": "object",
            "required": [
              "proof"
            ],
            "properties": {
              "proof": {
                "$ref": "#/components/schemas/PayerWalletProof"
              }
            }
          },
          {
            "$ref": "#/components/schemas/PayerWalletProof"
          }
        ]
      },
      "PayrollBatchCreateRequest": {
        "type": "object",
        "description": "POST /v1/payroll-batches body. Refused in this order: not an object; network; currency; payer_wallet_id; items not a non-empty array; more items than 500 (items per batch); reference; callback_url; approval; then every item problem at once. Unknown fields are ignored.",
        "required": [
          "network",
          "currency",
          "payer_wallet_id",
          "items"
        ],
        "properties": {
          "network": {
            "$ref": "#/components/schemas/PayrollNetwork"
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Za-z0-9]{1,16}$",
            "description": "A token symbol, canonicalised to the token table's spelling ('usde' -> 'USDe', #canonicalCurrencyCode). The network's native coin is refused (400 native_currency_not_supported_for_payroll)",
            "examples": [
              "USDC"
            ]
          },
          "payer_wallet_id": {
            "type": "string",
            "pattern": "^pyw_[0-9a-f]+$",
            "description": "A `verified` payer wallet of this tenant, this key's mode and this network. Any maker may use another maker's wallet of the tenant; only that wallet can sign the payloads."
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 256,
            "pattern": "^[\\x20-\\x7E]+$",
            "description": "Optional batch reference, printable ASCII 1-256. Not unique; null or absent = none."
          },
          "callback_url": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2048,
            "description": "Optional https URL to a public host, at most 2048 characters. Every event of the batch is also sent there, signed with the TENANT webhook secret; for a LIVE batch the raw tenant secret also travels in the clear in x-webhook-secret, while a sandbox batch's delivery carries the signature only. FastPay's guidance is never to set it on a payroll batch. Use account webhook endpoints instead.",
            "format": "uri",
            "pattern": "^https://"
          },
          "approval": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "required",
              null
            ],
            "description": "`required` makes this batch wait for approval whatever the policy. Any other value is 400 invalid_approval."
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "description": "1 to 500 (items per batch) items (default 500). Position = array order.",
            "items": {
              "$ref": "#/components/schemas/PayrollBatchItemInput"
            }
          }
        }
      },
      "PayrollBatch": {
        "type": "object",
        "description": "A payroll batch. Statuses: awaiting_approval -> open | cancelled; open -> closing | completed | closed | cancelled; closing -> completed | closed | cancelled. Terminal: completed (every item completed), closed (finished with an item failed or cancelled and something paid or attempted), cancelled (nothing completed and nothing failed). There is no `expired` status: past `expires_at` the sweep closes the batch.",
        "required": [
          "id",
          "object",
          "status",
          "network",
          "chain_ref",
          "currency",
          "token_address",
          "payer_wallet_id",
          "payer_address",
          "item_count",
          "total_amount",
          "approvals_required",
          "approvals",
          "expires_at",
          "cancel_requested_at",
          "closed_at",
          "counts",
          "reference",
          "callback_url",
          "is_test",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "pyb_0123456789abcdef0123456789abcdef"
            ]
          },
          "object": {
            "const": "payroll_batch"
          },
          "status": {
            "type": "string",
            "enum": [
              "awaiting_approval",
              "open",
              "closing",
              "completed",
              "closed",
              "cancelled"
            ]
          },
          "network": {
            "$ref": "#/components/schemas/PayrollNetwork"
          },
          "chain_ref": {
            "type": "string",
            "description": "The chain the payer wallet was verified on, snapshotted; sandbox rows start with `mock:`."
          },
          "currency": {
            "type": "string"
          },
          "token_address": {
            "type": "string",
            "description": "The token (Solana mint) the batch pays."
          },
          "payer_wallet_id": {
            "type": "string"
          },
          "payer_address": {
            "type": "string",
            "description": "The payer wallet's address, snapshotted at creation."
          },
          "item_count": {
            "type": "integer",
            "minimum": 1
          },
          "total_amount": {
            "type": "string",
            "description": "The sum of the item amounts as a normalised display decimal (trailing zeros dropped)."
          },
          "approvals_required": {
            "type": "integer",
            "minimum": 0,
            "maximum": 3,
            "description": "0 for a batch created open; else the policy's approvals_required at creation."
          },
          "approvals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PayrollApprovalDecision"
            }
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Creation + the batch lifetime (default 7 days)."
          },
          "cancel_requested_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "closed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "counts": {
            "$ref": "#/components/schemas/PayrollItemCounts"
          },
          "reference": {
            "type": [
              "string",
              "null"
            ]
          },
          "callback_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_test": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PayrollBatchList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PayrollBatch"
                }
              }
            }
          }
        ]
      },
      "PayrollItemList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PayrollItem"
                }
              }
            }
          }
        ]
      },
      "PayrollReasonRequest": {
        "type": "object",
        "description": "Optional body of reject and cancel. An absent or non-object body counts as no reason.",
        "properties": {
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "description": "Free text, trimmed; all-space is null; over 500 characters or not a string is 400 invalid_reason (never cut)."
          }
        }
      },
      "PayrollBatchCancelResult": {
        "description": "The batch after a cancel, plus two fields.",
        "allOf": [
          {
            "$ref": "#/components/schemas/PayrollBatch"
          },
          {
            "type": "object",
            "required": [
              "closes_at",
              "note"
            ],
            "properties": {
              "closes_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "The latest expires_at among the batch's holding authorizations at the time of the call; null when none holds. Not a close time when one is flagged (it never expires by itself)."
              },
              "note": {
                "type": "string",
                "description": "Fixed text."
              }
            }
          }
        ]
      },
      "PayrollIntentCreateRequest": {
        "type": "object",
        "description": "POST.../intents body. Absent or null fields take their defaults; unknown fields are ignored.",
        "properties": {
          "valid_for_seconds": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 300,
            "maximum": 259200,
            "default": 3600,
            "description": "The authorization window: the minimum validity (300) to the payer kind's maximum, 86400 for a single-key wallet and up to 259200 (72 h) for a multisig kind. The expiry is also clamped to the batch's expires_at, and refused when that leaves less than 300 s. Must be a JSON integer."
          },
          "max_items": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "description": "At most this many pending items in this intent."
          },
          "chain_options": {
            "type": [
              "object",
              "null"
            ],
            "description": "Per network, judged by the adapter: solana {mode: single|batch} (batch needs the Solana payroll mode=batch, else 503); tron {permission_id: an Active permission id}; sui {transport}; ton {max_messages: 1..wallet cap}; stacks {fee_payment: self|sponsored} (sponsored refused 409 invalid_chain_options while sponsorship is closed); bitcoin {fee_rate_sat_vb, max_items, include_non_witness_utxo}; evm and aptos take none. Any other key is 400 invalid_chain_options.",
            "additionalProperties": true
          },
          "fee_quote_id": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^pfs_[0-9a-f]{32}$",
            "description": "Bitcoin stacks_prepaid only - a paid fee quote of this batch; it decides the intent's items. On every other network 409 payroll_fee_mode_mismatch."
          }
        }
      },
      "PayrollIntent": {
        "type": "object",
        "description": "The payroll_intent document.",
        "required": [
          "id",
          "object",
          "batch_id",
          "network",
          "chain_ref",
          "currency",
          "fee_bps",
          "expires_at",
          "payer",
          "token",
          "deployment",
          "pre_steps",
          "transactions",
          "totals",
          "warnings",
          "note",
          "is_test",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "pyx_0123456789abcdef0123456789abcdef"
            ]
          },
          "object": {
            "const": "payroll_intent"
          },
          "batch_id": {
            "type": "string"
          },
          "network": {
            "$ref": "#/components/schemas/PayrollNetwork"
          },
          "chain_ref": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "fee_bps": {
            "type": "integer"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "payer": {
            "type": "string"
          },
          "token": {
            "type": "string"
          },
          "deployment": {
            "type": "object",
            "properties": {
              "key": {
                "type": "string"
              },
              "disburser": {
                "type": "string"
              }
            }
          },
          "pre_steps": {
            "type": [
              "array",
              "null"
            ],
            "description": "Allowance pre-steps (exact-allowance families only); [] otherwise; null when they could not be computed (POST warning).",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "transactions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PayrollIntentAuthorization"
            }
          },
          "totals": {
            "$ref": "#/components/schemas/PayrollIntentTotals"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "note": {
            "type": "string",
            "description": "The co-signature note, or the adapter's own where FastPay signs nothing."
          },
          "is_test": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "chain_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "EVM only."
          },
          "allowance": {
            "type": "object",
            "description": "Exact-allowance families only.",
            "properties": {
              "current": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "required": {
                "type": "string"
              },
              "calls": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "fee_mode": {
            "type": "string",
            "enum": [
              "in_tx_debt",
              "stacks_prepaid"
            ],
            "description": "Bitcoin only."
          },
          "fee_settlement": {
            "type": [
              "object",
              "null"
            ],
            "description": "Bitcoin stacks_prepaid only."
          },
          "balances": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "POST.../intents only - the quote's balance reads; requote and GET answer without it."
          },
          "payer_balance": {
            "type": [
              "string",
              "null"
            ],
            "description": "POST.../intents only - the payer's token balance in base units."
          }
        }
      },
      "PayrollIntentList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PayrollIntentListEntry"
                }
              }
            }
          }
        ]
      },
      "PayrollRequoteRequest": {
        "type": "object",
        "required": [
          "fee_rate_sat_vb"
        ],
        "properties": {
          "fee_rate_sat_vb": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 100000,
            "description": "sat/vB; then the adapter's own bounds (minimum and maximum fee rate, default 2 and 500)."
          }
        }
      },
      "PayrollFeeQuoteCreateRequest": {
        "type": "object",
        "properties": {
          "max_items": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1
          },
          "chain_options": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "stacks_payer": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "The Stacks address that pays the fee in sBTC; trimmed; blank refused."
          }
        }
      },
      "PayrollFeeQuoteCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PayrollFeeQuote"
          },
          {
            "type": "object",
            "properties": {
              "required_confirmations": {
                "type": "integer"
              },
              "balances": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        ]
      },
      "PayrollFeeQuote": {
        "type": "object",
        "description": "A payroll_fee_quote.",
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "pfs_0123456789abcdef0123456789abcdef"
            ]
          },
          "object": {
            "const": "payroll_fee_quote"
          },
          "batch_id": {
            "type": "string"
          },
          "intent_id": {
            "type": "string"
          },
          "network": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "quoted",
              "paid",
              "issuing",
              "consumed",
              "expired",
              "abandoned"
            ],
            "description": "(the CHECK constraint)."
          },
          "mode": {
            "const": "stacks_prepaid"
          },
          "item_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "amount": {
            "type": "string"
          },
          "token": {
            "type": "string"
          },
          "settlement_network": {
            "const": "stacks"
          },
          "settlement_chain_id": {
            "type": "string",
            "examples": [
              "stacks:1"
            ],
            "description": "The Stacks chain the fee-settlement verifier is paired, as stacks:<chain id> (a TEXT NOT NULL column). Never an integer, never null."
          },
          "reference": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "call": {
            "type": "object",
            "additionalProperties": true
          },
          "paid_amount": {
            "type": [
              "string",
              "null"
            ]
          },
          "stacks_txids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "is_test": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PayrollConfirmRequest": {
        "type": "object",
        "description": "`tx_hash` (every network; the network's own transaction reference: Solana a base58 64-byte signature, EVM 0x+64 hex, Tron/Bitcoin 64 hex, Sui a base58 digest, TON `<lt>:<64 hex>`), on TON only, `boc` (the signed external message, one base64 BOC).",
        "properties": {
          "tx_hash": {
            "type": "string"
          },
          "boc": {
            "type": "string"
          }
        }
      },
      "PayrollConfirmation": {
        "type": "object",
        "description": "The payroll_confirmation document.",
        "required": [
          "object",
          "batch_id",
          "report_kind",
          "tx_hash",
          "status",
          "confirmations",
          "required_confirmations",
          "authorization_ids",
          "transactions",
          "items",
          "is_test"
        ],
        "properties": {
          "object": {
            "const": "payroll_confirmation"
          },
          "batch_id": {
            "type": "string"
          },
          "report_kind": {
            "type": "string",
            "enum": [
              "tx_hash",
              "boc"
            ]
          },
          "tx_hash": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "applied",
              "flagged",
              "confirming",
              "deferred",
              "reorged",
              "pending",
              "reverted",
              "unrelated"
            ],
            "description": "The aggregate verdict. pending = the chain does not know it yet (remembered for the sweep); unrelated = it pays nothing of this batch."
          },
          "confirmations": {
            "type": [
              "integer",
              "null"
            ]
          },
          "required_confirmations": {
            "type": [
              "integer",
              "null"
            ]
          },
          "authorization_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "transactions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "tx_ref": {
                  "type": "string"
                },
                "status": {
                  "type": "string"
                }
              }
            }
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "status": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "is_test": {
            "type": "boolean"
          }
        }
      },
      "PayrollSponsorRequest": {
        "type": "object",
        "required": [
          "authorization_id",
          "group_index",
          "signed_tx"
        ],
        "properties": {
          "authorization_id": {
            "type": "string",
            "minLength": 1
          },
          "group_index": {
            "type": "integer",
            "minimum": 0
          },
          "signed_tx": {
            "type": "string",
            "pattern": "^(0x)?([0-9a-fA-F]{2})+$",
            "description": "The payer-signed sponsored transaction, serialized, hex."
          }
        }
      },
      "PayrollSponsorship": {
        "type": "object",
        "properties": {
          "object": {
            "const": "payroll_sponsorship"
          },
          "id": {
            "type": "string",
            "examples": [
              "pys_0123456789abcdef0123456789abcdef"
            ]
          },
          "batch_id": {
            "type": "string"
          },
          "authorization_id": {
            "type": "string"
          },
          "group_index": {
            "type": "integer"
          },
          "tx_hash": {
            "type": "string"
          },
          "status": {
            "const": "broadcast"
          },
          "sponsor_address": {
            "type": "string"
          },
          "sponsor_nonce": {
            "type": "integer"
          },
          "fee_ustx": {
            "type": "string"
          },
          "confirmation": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PayrollConfirmation"
              },
              {
                "type": "null"
              }
            ]
          },
          "note": {
            "type": "string"
          },
          "is_test": {
            "type": "boolean"
          }
        }
      },
      "WebhookDeliveryList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          }
        ]
      },
      "WebhookDeliveryWithPayload": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookDelivery"
          },
          {
            "type": "object",
            "required": [
              "payload"
            ],
            "properties": {
              "payload": {
                "type": "object",
                "description": "The JSON body recorded for this row, event_id included (one of the webhooks section's event bodies).",
                "additionalProperties": true
              }
            }
          }
        ]
      },
      "WebhookEgressIpList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "required": [
              "configured"
            ],
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "type": "string",
                  "description": "One entry of the published egress list, trimmed; not validated as an IP address."
                }
              },
              "has_more": {
                "const": false
              },
              "configured": {
                "type": "boolean",
                "description": "true when at least one address is configured."
              }
            }
          }
        ]
      },
      "WebhookSecretRotateRequest": {
        "type": "object",
        "description": "Body optional.",
        "properties": {
          "expires_in_hours": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 72,
            "default": 24,
            "description": "Hours the previous secret keeps signing (0 = none). Read with Number: a numeric string (\"12\"), a fraction (1.5) and true (1) pass, and \"\" or false read as 0, dropping the previous secret at once. null or absent - 24. Send a JSON integer. Out of range or not numeric - invalid_expires_in_hours."
          }
        }
      },
      "WebhookSecretRotation": {
        "type": "object",
        "required": [
          "object",
          "secret",
          "previous_secret_expires_at"
        ],
        "properties": {
          "object": {
            "const": "webhook_secret"
          },
          "secret": {
            "type": "string",
            "pattern": "^whsec_[0-9a-f]{48}$"
          },
          "previous_secret_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "null when expires_in_hours was 0."
          }
        }
      },
      "WebhookEndpointCreateRequest": {
        "type": "object",
        "description": "Unknown fields are ignored. Checked in this order.",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "pattern": "^[Hh][Tt][Tt][Pp][Ss]://",
            "description": "Required non-empty string (else parameter_missing). Checked in this order: at most 2048 characters; parses as a URL and starts with https:// in any letter case; no username or password; then the SSRF check: a host other than localhost that resolves NOW, every resolved address public - not 0/8, 10/8, 127/8, 169.254/16, 172.16/12, 192.168/16, 100.64/10,::1,::, fe80::/10, fc00::/7 or an IPv4-mapped private address (lists the ranges it misses); an IPv4 literal resolves to itself and is checked the same way. Each failure is invalid_url; the SSRF messages say \"callbackUrl\". An IPv6 literal host reaches dns.lookup with its brackets; The url is stored and audited as sent, query string included."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "description": "At most 500 characters; null or omitted stores null (else invalid_description)."
          },
          "enabled_events": {
            "description": "Omitted or null - [\"*\"] (every event, all 31 types). An empty array or an unknown type - invalid_enabled_events.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/WebhookEnabledEvents"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "enabled",
              "disabled"
            ],
            "description": "Validated (invalid_status) and then IGNORED on create - always created enabled. Do not send it."
          }
        }
      },
      "WebhookEndpointWithSecret": {
        "description": "The endpoint plus its current signing secret - only in a create's 201 and a rotate-secret's 200.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEndpoint"
          },
          {
            "type": "object",
            "required": [
              "secret"
            ],
            "properties": {
              "secret": {
                "type": "string",
                "pattern": "^whsec_[0-9a-f]{48}$",
                "description": "whsec_ + 24 random bytes in hex, 54 characters. Shown once; a same-key Idempotency-Key replay shows it again."
              }
            }
          }
        ]
      },
      "WebhookEndpointList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          }
        ]
      },
      "WebhookEndpoint": {
        "type": "object",
        "description": "An account-level endpoint, never with its secret.",
        "required": [
          "id",
          "object",
          "url",
          "description",
          "enabled_events",
          "status",
          "is_test",
          "previous_secret_expires_at",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^we_[0-9a-f]{32}$"
          },
          "object": {
            "const": "webhook_endpoint"
          },
          "url": {
            "type": "string",
            "description": "The URL as given (scheme, path and query kept). Since 2026-10-28 every create or url update requires https, at most 2048 characters and no user info; an endpoint stored before may still hold an http url or user info. Deliveries already recorded keep the URL they were recorded."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500
          },
          "enabled_events": {
            "$ref": "#/components/schemas/WebhookEnabledEvents"
          },
          "status": {
            "type": "string",
            "enum": [
              "enabled",
              "disabled"
            ]
          },
          "is_test": {
            "type": "boolean",
            "description": "The mode fixed at creation; true receives only sandbox events."
          },
          "previous_secret_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set only while a rotation overlap is still open."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEndpointUpdateRequest": {
        "type": "object",
        "description": "Only the fields present change; an empty body bumps updated_at and is audited.",
        "properties": {
          "url": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "pattern": "^[Hh][Tt][Tt][Pp][Ss]://",
            "description": "As on create, checked again even when it is the current url. null or \"\" - parameter_missing. Absent: the stored url is kept and not checked (an http url stored before 2026-10-28 stays)."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "description": "null clears it."
          },
          "enabled_events": {
            "description": "null RESETS the list to [\"*\"] (every event); [] - invalid_enabled_events.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/WebhookEnabledEvents"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "enabled",
              "disabled"
            ],
            "description": "disabled stops new fan-out and lets the sweep close the endpoint's pending rows as superseded. null - invalid_status."
          }
        }
      },
      "WebhookEndpointDeleted": {
        "type": "object",
        "description": "The answer of both deletes, the tenant route's and the operator's.",
        "required": [
          "id",
          "object",
          "deleted"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "const": "webhook_endpoint"
          },
          "deleted": {
            "const": true
          }
        }
      },
      "TonConnectManifest": {
        "type": "object",
        "required": [
          "url",
          "name",
          "iconUrl"
        ],
        "additionalProperties": false,
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "name": {
            "type": "string",
            "const": "FastPay Checkout"
          },
          "iconUrl": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "ExchangeRate": {
        "type": "object",
        "required": [
          "object",
          "from",
          "to",
          "rate",
          "source",
          "fetched_at"
        ],
        "additionalProperties": false,
        "properties": {
          "object": {
            "const": "rate"
          },
          "from": {
            "type": "string",
            "description": "Canonical code."
          },
          "to": {
            "type": "string"
          },
          "rate": {
            "type": "string",
            "description": "Units of `to` per one `from`. Up to 10 decimals as fetched or inverted; \"1.0\" for identity."
          },
          "source": {
            "type": "string",
            "enum": [
              "live",
              "live_stale",
              "static_fallback",
              "identity"
            ]
          },
          "fetched_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the live table was fetched; null for static_fallback."
          }
        }
      },
      "PaymentRequestCreateRequest": {
        "type": "object",
        "description": "(camelCase body). Every refusal is 400 invalid_payment_request (Shape A) except a missing recipient with no tenant wallet (missing_recipient) and a non-public callbackUrl (invalid_callback_url). Unknown fields are ignored.",
        "required": [
          "reference",
          "network",
          "currency",
          "amount",
          "expiresAt"
        ],
        "properties": {
          "reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "The merchant's reference; at most 200 characters as sent, not blank, stored trimmed. Shown to the payer and in the Solana Pay message."
          },
          "network": {
            "$ref": "#/components/schemas/CheckoutNetwork"
          },
          "currency": {
            "type": "string",
            "description": "The network's native coin or one of its configured stablecoins, any letter case, stored canonical (USDe). Per network: ethereum ETH USDC USDT DAI USDe USDS USD1 FDUSD EURC PYUSD USDG AUSD; base ETH USDC DAI USDe USDS EURC AUSD; polygon POL USDC DAI AUSD; arbitrum ETH USDC DAI USDe USDS PYUSD AUSD; bnb BNB USDC USDT DAI USDe USD1 FDUSD AUSD; avalanche AVAX USDC USDT USDe EURC AUSD; robinhood ETH USDe USDG; linea ETH USDC USDe; optimism ETH USDC USDT USDe; monad MON USDC USDe AUSD; megaeth ETH USDe; hyperevm HYPE USDC USDe; solana SOL USDC USDT USDe USDS USD1 FDUSD EURC PYUSD USDG AUSD; tron TRX USDC USDT USD1; aptos APT USDC USDT USDe; sui SUI USDC FDUSD AUSD; ton TON USDT USDe FDUSD. Accepted here is not payable: the intent refuses a token the deployment or the disburser does not allow (409 currency_not_allowlisted)."
          },
          "amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CheckoutDecimalAmount"
              }
            ],
            "description": "Positive; at most the token's decimals on that network (e.g. USDC 6, but 18 on bnb; SOL 9; TRX 6) and 78 digits in all."
          },
          "recipient": {
            "type": "string",
            "description": "The merchant's receiving address on the network; When sent it wins over the registered wallet, whatever address it is. Validated and stored canonical: EVM checksummed, Solana base58 of 32 bytes, Tron base58, Aptos/Sui 0x + 64 lower-case hex, TON user-friendly. Must be a string."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "Strictly in the future and at most the payment request maximum expiry (7 days) (7 days) ahead; parsed with Date.parse, stored as ISO-8601 UTC. Send a Z-suffixed ISO-8601 value."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "A JSON object (null and arrays refused). Stored key-sorted; returned to the tenant. Only metadata.redirect_url (http or https) is shown to the payer, as redirectUrl."
          },
          "callbackUrl": {
            "type": "string",
            "format": "uri",
            "description": "http or https (null refused). Must resolve to a public address now. Receives the paid webhooks signed with the tenant webhook secret."
          }
        }
      },
      "PaymentRequestTenantView": {
        "type": "object",
        "description": "The stored row in camelCase plus checkoutUrl and qr. There is no `object` field.",
        "required": [
          "id",
          "tenantId",
          "reference",
          "network",
          "currency",
          "amount",
          "recipient",
          "status",
          "expiresAt",
          "metadata",
          "idempotencyKey",
          "callbackUrl",
          "payer",
          "settledPayer",
          "txHash",
          "isTest",
          "paymentLinkId",
          "createdAt",
          "updatedAt",
          "checkoutUrl",
          "qr"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "pr_ + 32 hex, or cs_ + 32 hex for a session's settlement row."
          },
          "tenantId": {
            "type": "string",
            "format": "uuid"
          },
          "reference": {
            "type": "string"
          },
          "network": {
            "$ref": "#/components/schemas/CheckoutNetwork"
          },
          "currency": {
            "type": "string"
          },
          "amount": {
            "type": "string"
          },
          "recipient": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "paid",
              "paid_after_expiry"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "idempotencyKey": {
            "type": [
              "string",
              "null"
            ]
          },
          "callbackUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "payer": {
            "type": [
              "string",
              "null"
            ],
            "description": "The wallet the latest intent was built for. Never verified; never a destination."
          },
          "settledPayer": {
            "type": [
              "string",
              "null"
            ],
            "description": "The payer verified on chain at settlement; the default refund destination."
          },
          "txHash": {
            "type": [
              "string",
              "null"
            ],
            "description": "The settling hash (TON: the message hash of the reported BOC)."
          },
          "isTest": {
            "type": "boolean"
          },
          "paymentLinkId": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "checkoutUrl": {
            "type": "string",
            "format": "uri"
          },
          "qr": {
            "$ref": "#/components/schemas/CheckoutQrPayloads"
          }
        }
      },
      "PaymentRequestList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PaymentRequestTenantView"
                }
              }
            }
          }
        ]
      },
      "PaymentRequestPublicView": {
        "type": "object",
        "required": [
          "id",
          "reference",
          "network",
          "currency",
          "amount",
          "recipient",
          "status",
          "expiresAt",
          "txHash",
          "isTest",
          "redirectUrl",
          "checkoutUrl",
          "qr"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string"
          },
          "reference": {
            "type": "string"
          },
          "network": {
            "$ref": "#/components/schemas/CheckoutNetwork"
          },
          "currency": {
            "type": "string"
          },
          "amount": {
            "type": "string"
          },
          "recipient": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "paid",
              "paid_after_expiry"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "txHash": {
            "type": [
              "string",
              "null"
            ]
          },
          "isTest": {
            "type": "boolean"
          },
          "chainId": {
            "type": "integer",
            "description": "EVM networks only."
          },
          "walletChain": {
            "type": "string",
            "description": "Sui only: sui:<network>."
          },
          "redirectUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "checkoutUrl": {
            "type": "string",
            "format": "uri"
          },
          "qr": {
            "$ref": "#/components/schemas/CheckoutQrPayloads"
          }
        }
      },
      "CheckoutIntentRequest": {
        "type": "object",
        "required": [
          "payerAddress"
        ],
        "properties": {
          "payerAddress": {
            "type": "string",
            "minLength": 1,
            "description": "The connected wallet. A non-empty string (else 400 invalid_payer_address); then the network's own address check (400 invalid_request). It binds the authorization: the disburser rejects a transaction sent by another address. On a sandbox request any non-blank string passes."
          }
        }
      },
      "CheckoutIntent": {
        "description": "The intent, by the request's network; a sandbox request always gets CheckoutIntentSandbox.",
        "anyOf": [
          {
            "$ref": "#/components/schemas/CheckoutIntentEvm"
          },
          {
            "$ref": "#/components/schemas/CheckoutIntentTron"
          },
          {
            "$ref": "#/components/schemas/CheckoutIntentSolana"
          },
          {
            "$ref": "#/components/schemas/CheckoutIntentAptos"
          },
          {
            "$ref": "#/components/schemas/CheckoutIntentSui"
          },
          {
            "$ref": "#/components/schemas/CheckoutIntentTon"
          },
          {
            "$ref": "#/components/schemas/CheckoutIntentSandbox"
          }
        ]
      },
      "PaymentLinkUnavailableError": {
        "description": "Shape A 409 payment_link_unavailable with the top-level reason.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "required": [
              "reason"
            ],
            "properties": {
              "reason": {
                "type": "string",
                "enum": [
                  "inactive",
                  "expired",
                  "exhausted"
                ]
              }
            }
          }
        ]
      },
      "PaymentRequestConfirmRequest": {
        "type": "object",
        "description": "txHash, or for TON the boc.",
        "properties": {
          "txHash": {
            "type": "string",
            "description": "Format per network: EVM 0x + 64 hex (0x optional is also accepted); Tron 64 hex, 0x optional; Solana base58 of a 64-byte signature; Sui base58 of 32 bytes; Aptos 0x + 64 hex; TON 64 hex or 43-44 base64/base64url characters of 32 bytes."
          },
          "boc": {
            "type": "string",
            "description": "TON only: the base64 BOC TonConnect returned. Required for a TON jetton payment; on another network it is ignored."
          }
        }
      },
      "PaymentRequestConfirmResult": {
        "type": "object",
        "required": [
          "status",
          "txHash"
        ],
        "additionalProperties": false,
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "paid",
              "paid_after_expiry",
              "pending",
              "failed"
            ]
          },
          "txHash": {
            "type": "string",
            "description": "The reported hash, or the recorded one when the request was already paid."
          },
          "reason": {
            "type": "string",
            "description": "failed only. EVM: tx_reverted, wrong_contract, no_disbursed_event, amount_mismatch, recipient_mismatch, token_mismatch; Tron, Solana, Aptos, Sui: tx_reverted, no_disbursed_event, amount_mismatch, recipient_mismatch, token_mismatch; sandbox: sandbox_forced_failure; TON never fails. A failed confirm writes nothing - the request stays pending.",
            "enum": [
              "tx_reverted",
              "wrong_contract",
              "no_disbursed_event",
              "amount_mismatch",
              "recipient_mismatch",
              "token_mismatch",
              "sandbox_forced_failure"
            ]
          }
        }
      },
      "SolanaPayLabel": {
        "type": "object",
        "required": [
          "label",
          "icon"
        ],
        "additionalProperties": false,
        "properties": {
          "label": {
            "type": "string",
            "description": "The tenant's name, or FastPay."
          },
          "icon": {
            "type": "string",
            "format": "uri",
            "description": "<base>/checkout/fastpay-icon.svg"
          }
        }
      },
      "SolanaPayError": {
        "description": "Shape A plus the Solana Pay spec's top-level `message`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "required": [
              "message"
            ],
            "properties": {
              "message": {
                "type": "string"
              }
            }
          }
        ]
      },
      "SolanaPayTransactionRequest": {
        "type": "object",
        "required": [
          "account"
        ],
        "properties": {
          "account": {
            "type": "string",
            "minLength": 1,
            "description": "The scanning wallet's public key (base58)."
          }
        }
      },
      "SolanaPayTransactionResponse": {
        "type": "object",
        "required": [
          "transaction",
          "message"
        ],
        "additionalProperties": false,
        "properties": {
          "transaction": {
            "type": "string",
            "contentEncoding": "base64"
          },
          "message": {
            "type": "string",
            "description": "Pay <amount> <currency> for <reference"
          }
        }
      },
      "CheckoutSessionCreateRequest": {
        "type": "object",
        "description": "(snake_case). Every refusal is Shape B 400 with its slug. Unknown fields are ignored.",
        "required": [
          "amount",
          "currency",
          "success_url",
          "cancel_url",
          "network",
          "settlement_currency"
        ],
        "properties": {
          "amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CheckoutDecimalAmount"
              }
            ],
            "description": "Fiat, positive, at most 2 decimals (cents) and 78 digits."
          },
          "currency": {
            "type": "string",
            "pattern": "^([Ee][Uu][Rr]|[Uu][Ss][Dd])$",
            "description": "EUR or USD in any letter case; stored upper case."
          },
          "success_url": {
            "type": "string",
            "format": "uri",
            "description": "http or https (format only, no DNS check)."
          },
          "cancel_url": {
            "type": "string",
            "format": "uri",
            "description": "http or https (format only)."
          },
          "network": {
            "$ref": "#/components/schemas/CheckoutNetwork"
          },
          "settlement_currency": {
            "type": "string",
            "description": "The crypto the payer pays: the network's currency set that the static rate table can quote (at every currency of PaymentRequestCreateRequest.currency); any letter case, answered canonical."
          },
          "callback_url": {
            "type": "string",
            "format": "uri",
            "description": "Optional; http or https and public (DNS checked): receives checkout_session.paid and the refund events. null is refused (400 invalid_url), unlike a payment link's callback_url."
          },
          "customer": {
            "type": "object",
            "description": "Optional. Only customer.id is read and stored as is; it is not checked against GET /v1/customers.",
            "properties": {
              "id": {
                "type": "string"
              }
            }
          },
          "merchant_reference": {
            "type": "string",
            "description": "Optional; stored on the payment and used as the payment request's reference (else the session id). Not validated: no length cap; a non-string answers 500."
          }
        }
      },
      "CheckoutSessionCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CheckoutSession"
          },
          {
            "type": "object",
            "required": [
              "rate_source",
              "rate_fetched_at"
            ],
            "properties": {
              "rate_source": {
                "type": "string",
                "enum": [
                  "live",
                  "live_stale",
                  "static_fallback",
                  "identity"
                ],
                "description": "Not stored - on the create response only."
              },
              "rate_fetched_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          }
        ]
      },
      "CheckoutSessionList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CheckoutSession"
                }
              }
            }
          }
        ]
      },
      "CheckoutSession": {
        "type": "object",
        "description": "- one shape for create, retrieve, list and expire.",
        "required": [
          "id",
          "object",
          "status",
          "payment_status",
          "amount",
          "currency",
          "network",
          "payment_amount",
          "payment_currency",
          "exchange_rate",
          "success_url",
          "cancel_url",
          "payment_url",
          "expires_at",
          "is_test",
          "created_at",
          "payments"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "const": "checkout_session"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "completed",
              "expired"
            ],
            "description": "The stored status; open past expires_at until expired by hand."
          },
          "payment_status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "created",
              "paid",
              "expired",
              null
            ]
          },
          "amount": {
            "type": "string",
            "description": "Fiat."
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR",
              "USD"
            ]
          },
          "network": {
            "type": [
              "string",
              "null"
            ]
          },
          "payment_amount": {
            "type": [
              "string",
              "null"
            ],
            "description": "The crypto amount the payer pays (truncated to the token's decimals)."
          },
          "payment_currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "exchange_rate": {
            "type": [
              "string",
              "null"
            ],
            "description": "Crypto units per fiat unit, locked at creation."
          },
          "success_url": {
            "type": "string"
          },
          "cancel_url": {
            "type": "string"
          },
          "payment_url": {
            "type": "string",
            "format": "uri"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "is_test": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "payments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CheckoutSessionPaymentSummary"
            }
          }
        }
      },
      "CheckoutPaymentList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CheckoutPayment"
                }
              }
            }
          }
        ]
      },
      "CheckoutPayment": {
        "type": "object",
        "description": "plus object. Mixed camelCase, as served.",
        "required": [
          "object",
          "id",
          "status",
          "isTest",
          "lifecycle",
          "financial",
          "blockchain",
          "related"
        ],
        "properties": {
          "object": {
            "const": "payment"
          },
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "paid",
              "expired"
            ]
          },
          "isTest": {
            "type": "boolean"
          },
          "lifecycle": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "status",
                "at"
              ],
              "properties": {
                "status": {
                  "type": "string",
                  "enum": [
                    "created",
                    "paid",
                    "paid_after_expiry",
                    "expired"
                  ]
                },
                "at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "financial": {
            "type": "object",
            "properties": {
              "originalAmount": {
                "type": "string"
              },
              "originalCurrency": {
                "type": "string"
              },
              "paymentAmount": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "paymentCurrency": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "exchangeRate": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "gatewayFee": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Fiat cents string."
              },
              "networkFee": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "conversionFee": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "netSettlement": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "settlementCurrency": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "settlementType": {
                "type": "string"
              },
              "refundedAmount": {
                "type": "string",
                "description": "Pending + confirmed refunds, in the payment currency."
              },
              "refundableAmount": {
                "type": "string"
              }
            }
          },
          "blockchain": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "network": {
                "type": "string"
              },
              "asset": {
                "type": "string"
              },
              "address": {
                "type": "string",
                "description": "The merchant's receiving address (and the refund source)."
              },
              "txHash": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "confirmations": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "related": {
            "type": "object",
            "properties": {
              "checkoutSessionId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "customerId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "refundIds": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "ledgerTransactionIds": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "CheckoutBlockchainTransactionList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CheckoutBlockchainTransaction"
                }
              },
              "has_more": {
                "const": false
              }
            }
          }
        ]
      },
      "CheckoutRefundCreateRequest": {
        "type": "object",
        "required": [
          "amount"
        ],
        "properties": {
          "amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CheckoutDecimalAmount"
              }
            ],
            "description": "In the payment's CRYPTO currency (payment_currency), positive, at most the token's decimals, at most what is still refundable."
          },
          "destination": {
            "type": "string",
            "description": "Where the refund goes; default the payer verified at settlement. Required for TON, sandbox and pre-2026-10-18 payments. Live: validated and canonicalised for the network."
          },
          "reason": {
            "type": "string",
            "description": "Free text stored on the refund. Not validated: no length cap; a non-string object answers 500."
          }
        }
      },
      "CheckoutRefundCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CheckoutRefund"
          },
          {
            "type": "object",
            "required": [
              "transaction"
            ],
            "properties": {
              "transaction": {
                "$ref": "#/components/schemas/CheckoutRefundTransaction"
              }
            }
          }
        ]
      },
      "CheckoutRefundList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CheckoutRefund"
                }
              }
            }
          }
        ]
      },
      "CheckoutRefund": {
        "type": "object",
        "required": [
          "id",
          "object",
          "payment_id",
          "status",
          "amount",
          "currency",
          "destination",
          "reason",
          "is_test",
          "tx_hash",
          "failure_reason",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "const": "refund"
          },
          "payment_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "confirmed",
              "failed",
              "expired"
            ]
          },
          "amount": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "destination": {
            "type": "string"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_test": {
            "type": "boolean"
          },
          "tx_hash": {
            "type": [
              "string",
              "null"
            ]
          },
          "failure_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "tx_reverted, build_failed or expired."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CheckoutRefundConfirmRequest": {
        "type": "object",
        "required": [
          "txHash"
        ],
        "properties": {
          "txHash": {
            "type": "string",
            "description": "The refund network's format, as PaymentRequestConfirmRequest.txHash."
          }
        }
      },
      "CheckoutRefundStatus": {
        "type": "object",
        "required": [
          "id",
          "object",
          "status",
          "tx_hash",
          "failure_reason"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "const": "refund"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "confirmed",
              "failed",
              "expired"
            ]
          },
          "tx_hash": {
            "type": [
              "string",
              "null"
            ],
            "description": "null on a pending verdict."
          },
          "failure_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "tx_reverted (terminal), build_failed, expired; or a mismatch with the refund still pending: reverted_not_this_refund, no_matching_transfer, sender_mismatch, not_a_transaction_hash."
          }
        }
      },
      "PaymentLinkCreateRequest": {
        "type": "object",
        "description": "(snake_case). Each refusal is Shape A 400 with its own slug. Unknown fields are ignored.",
        "required": [
          "network",
          "currency"
        ],
        "properties": {
          "network": {
            "$ref": "#/components/schemas/CheckoutNetwork"
          },
          "currency": {
            "type": "string",
            "description": "As PaymentRequestCreateRequest.currency (invalid_currency)."
          },
          "amount": {
            "type": [
              "string",
              "null"
            ],
            "description": "A fixed amount (positive, token decimals); omit for a customer-entered link (invalid_amount)."
          },
          "min_amount": {
            "type": [
              "string",
              "null"
            ],
            "description": "Customer-entered links only (invalid_min_amount; with amount: invalid_amount)."
          },
          "max_amount": {
            "type": [
              "string",
              "null"
            ],
            "description": "Customer-entered links only; not below min_amount (invalid_max_amount / invalid_amount)."
          },
          "recipient": {
            "type": "string",
            "description": "As PaymentRequestCreateRequest.recipient; optional with a tenant payout wallet (invalid_recipient)."
          },
          "reference_prefix": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Za-z0-9._-]{1,100}$",
            "description": "Requests get `<prefix>-<8 hex>`; default the link id (invalid_reference_prefix)."
          },
          "session_ttl_seconds": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 60,
            "maximum": 604800,
            "description": "How long each opened request lives; default the session lifetime (1800); the maximum is the payment request maximum expiry (7 days) in seconds (invalid_session_ttl)."
          },
          "max_uses": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 1000000,
            "description": "Paid requests allowed; null = unlimited (invalid_max_uses)."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "In the future; no upper bound (invalid_expires_at)."
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Copied to every opened request (invalid_metadata)."
          },
          "callback_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "http(s), public; copied to every opened request (invalid_callback_url)."
          }
        }
      },
      "PaymentLinkTenantView": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PaymentLinkCommon"
          },
          {
            "type": "object",
            "required": [
              "reference_prefix",
              "session_ttl_seconds",
              "max_uses",
              "uses",
              "metadata",
              "callback_url",
              "created_at",
              "updated_at"
            ],
            "properties": {
              "reference_prefix": {
                "type": "string"
              },
              "session_ttl_seconds": {
                "type": "integer"
              },
              "max_uses": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "uses": {
                "type": "integer",
                "description": "Paid requests of this link (both paid literals)."
              },
              "metadata": {
                "type": "object",
                "additionalProperties": true
              },
              "callback_url": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "PaymentLinkList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PaymentLinkTenantView"
                }
              }
            }
          }
        ]
      },
      "PaymentLinkPublicView": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PaymentLinkCommon"
          },
          {
            "type": "object",
            "required": [
              "merchant_name"
            ],
            "properties": {
              "merchant_name": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        ]
      },
      "PaymentLinkSessionRequest": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "string",
            "description": "Customer-entered links: required, positive, token decimals, within min/max. Fixed links: optional and must equal the fixed amount in base units."
          }
        }
      },
      "PaymentLinkSessionCreated": {
        "type": "object",
        "required": [
          "payment_request_id",
          "payment_link_id",
          "checkout_url",
          "expires_at"
        ],
        "additionalProperties": false,
        "properties": {
          "payment_request_id": {
            "type": "string"
          },
          "payment_link_id": {
            "type": "string"
          },
          "checkout_url": {
            "type": "string",
            "format": "uri"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "X402SupportedResponse": {
        "type": "object",
        "required": [
          "kinds",
          "extensions",
          "signers"
        ],
        "properties": {
          "kinds": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "x402Version",
                "scheme",
                "network"
              ],
              "properties": {
                "x402Version": {
                  "const": 2
                },
                "scheme": {
                  "type": "string"
                },
                "network": {
                  "type": "string"
                },
                "extra": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "extensions": {
            "type": "array",
            "maxItems": 0
          },
          "signers": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }
      },
      "X402FacilitatorRequest": {
        "type": "object",
        "required": [
          "x402Version",
          "paymentPayload",
          "paymentRequirements"
        ],
        "properties": {
          "x402Version": {
            "const": 2
          },
          "paymentPayload": {
            "$ref": "#/components/schemas/X402PaymentPayload"
          },
          "paymentRequirements": {
            "$ref": "#/components/schemas/X402PaymentRequirements"
          }
        }
      },
      "X402VerifyResponse": {
        "type": "object",
        "required": [
          "isValid"
        ],
        "additionalProperties": false,
        "properties": {
          "isValid": {
            "type": "boolean"
          },
          "invalidReason": {
            "type": "string",
            "description": "An x402 reason (and the per-chain families next to each scheme)."
          },
          "payer": {
            "type": "string"
          },
          "invalidMessage": {
            "type": "string"
          }
        }
      },
      "X402SettleResponse": {
        "type": "object",
        "required": [
          "success",
          "transaction",
          "network"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "errorReason": {
            "type": "string"
          },
          "transaction": {
            "type": "string",
            "description": "Empty unless something was broadcast."
          },
          "network": {
            "type": "string"
          },
          "payer": {
            "type": "string"
          }
        }
      },
      "X402PaidRequestBody": {
        "type": "object",
        "required": [
          "id",
          "status",
          "txHash",
          "payer"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "paid",
              "paid_after_expiry"
            ]
          },
          "txHash": {
            "type": [
              "string",
              "null"
            ]
          },
          "payer": {
            "type": [
              "string",
              "null"
            ]
          },
          "network": {
            "type": "string",
            "description": "Set when this call settled."
          },
          "duplicateSettlement": {
            "type": "object",
            "description": "The customer paid twice - this settlement is real and not recorded (refund it).",
            "properties": {
              "transaction": {
                "type": "string"
              },
              "payer": {
                "type": "string"
              }
            }
          }
        }
      },
      "X402PaymentRequiredBody": {
        "type": "object",
        "description": "The x402 PaymentRequired object plus error_type payment_required and error_slug; the res.json wrapper adds error_message (= error) and request_id.",
        "required": [
          "x402Version",
          "error",
          "resource",
          "accepts",
          "extensions",
          "error_type",
          "error_slug"
        ],
        "properties": {
          "x402Version": {
            "const": 2
          },
          "error": {
            "type": "string"
          },
          "resource": {
            "$ref": "#/components/schemas/X402ResourceInfo"
          },
          "accepts": {
            "type": "array",
            "minItems": 1,
            "maxItems": 1,
            "items": {
              "$ref": "#/components/schemas/X402PaymentRequirements"
            }
          },
          "extensions": {
            "type": "object"
          },
          "error_type": {
            "const": "payment_required"
          },
          "error_slug": {
            "type": "string",
            "description": "payment_required, settlement_pending, or the settle errorReason."
          },
          "error_message": {
            "type": "string"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "EcommerceWebhookReceived": {
        "type": "object",
        "required": [
          "received"
        ],
        "additionalProperties": false,
        "properties": {
          "received": {
            "const": true
          }
        }
      },
      "EcommerceWebhookError": {
        "type": "object",
        "required": [
          "error_type",
          "error_slug",
          "error"
        ],
        "description": "The inbound webhook route's Shape A, written before the res.json wrapper exists: no error_message, no request_id (vs). A body-parser or global-handler answer on this route is the shared Error (Shape B) with request_id null.",
        "properties": {
          "error_type": {
            "type": "string",
            "enum": [
              "request_error",
              "auth_error"
            ]
          },
          "error_slug": {
            "type": "string"
          },
          "error": {
            "type": "string"
          }
        }
      },
      "CatalogNetworkList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CatalogNetwork"
                }
              },
              "has_more": {
                "const": false
              }
            }
          }
        ]
      },
      "CatalogCurrencyList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CatalogCurrency"
                }
              },
              "has_more": {
                "const": false
              }
            }
          }
        ]
      },
      "ReconciliationReport": {
        "type": "object",
        "required": [
          "object",
          "tenant_id",
          "period",
          "summary",
          "discrepancies"
        ],
        "properties": {
          "object": {
            "const": "reconciliation_report"
          },
          "tenant_id": {
            "type": "string",
            "format": "uuid"
          },
          "period": {
            "type": "object",
            "required": [
              "from",
              "to"
            ],
            "properties": {
              "from": {
                "type": "string",
                "format": "date-time"
              },
              "to": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "summary": {
            "type": "object",
            "required": [
              "payments_paid",
              "total_settlement_amount_by_currency",
              "payments_with_discrepancy"
            ],
            "properties": {
              "payments_paid": {
                "type": "integer",
                "minimum": 0
              },
              "total_settlement_amount_by_currency": {
                "type": "object",
                "additionalProperties": {
                  "type": "string",
                  "pattern": "^-?[0-9]+\\.[0-9]{2}$"
                }
              },
              "payments_with_discrepancy": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "discrepancies": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReconciliationDiscrepancy"
            },
            "description": "A payment can appear twice (one chain and one ledger discrepancy)."
          }
        }
      },
      "CustomerCreateRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "additionalProperties": true,
        "description": "Unknown fields are ignored.",
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 254,
            "pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
            "description": "Stored as sent (case kept). Missing or malformed: 400 parameter_missing."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "Empty string or null stores null. Else 400 invalid_name."
          },
          "country": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64,
            "description": "Free text (a code or a name). Empty string or null stores null. Else 400 invalid_country."
          }
        }
      },
      "Customer": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "object",
          "email",
          "name",
          "country",
          "is_test",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^cus_[0-9a-f]{32}$"
          },
          "object": {
            "const": "customer"
          },
          "email": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_test": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CustomerList": {
        "allOf": [
          {
            "$ref": "#/components/schemas/List"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          }
        ]
      },
      "AccountingConnectStart": {
        "type": "object",
        "required": [
          "provider",
          "authorizationUrl",
          "state"
        ],
        "additionalProperties": false,
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "xero",
              "quickbooks",
              "zoho",
              "sage"
            ]
          },
          "authorizationUrl": {
            "type": "string",
            "format": "uri",
            "description": "The provider's authorize URL with response_type, client_id, redirect_uri (the caller's, as sent), scope, state, code_challenge (S256)."
          },
          "state": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "Single use, 10 minutes."
          }
        }
      },
      "AccountingCallbackRequest": {
        "type": "object",
        "required": [
          "code",
          "state"
        ],
        "properties": {
          "code": {
            "type": "string",
            "minLength": 1
          },
          "state": {
            "type": "string",
            "minLength": 1
          }
        }
      },
      "AccountingConnected": {
        "type": "object",
        "required": [
          "success",
          "provider",
          "connected",
          "expiresAt"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "provider": {
            "type": "string"
          },
          "connected": {
            "const": true
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "AccountingMappingsView": {
        "type": "object",
        "required": [
          "provider",
          "connected",
          "expiresAt",
          "mapping"
        ],
        "properties": {
          "provider": {
            "type": "string"
          },
          "connected": {
            "type": "boolean"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "mapping": {
            "$ref": "#/components/schemas/AccountingMapping"
          }
        }
      },
      "AccountingMappingsRequest": {
        "type": "object",
        "required": [
          "mapping"
        ],
        "properties": {
          "mapping": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AccountingMapping"
              },
              {
                "type": "array",
                "description": "Accepted (typeof object) but useless."
              }
            ]
          }
        }
      },
      "AccountingMappingsSaved": {
        "type": "object",
        "required": [
          "success",
          "provider",
          "mapping"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "provider": {
            "type": "string"
          },
          "mapping": {
            "description": "The mapping as sent."
          }
        }
      },
      "AccountingSyncRequest": {
        "type": "object",
        "required": [
          "type",
          "id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "disbursement",
              "gateway"
            ],
            "description": "disbursement = a relay mass payout (ALLOCATION_ID); gateway = a paid payment request or checkout session (cs_...). No payroll type."
          },
          "id": {
            "type": "string",
            "minLength": 1
          }
        }
      },
      "AccountingSyncResult": {
        "type": "object",
        "required": [
          "success",
          "receipt"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "receipt": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AccountingDisbursementReceipt"
              },
              {
                "$ref": "#/components/schemas/AccountingGatewayReceipt"
              }
            ],
            "description": "A replay returns the stored receipt of the first completed sync."
          }
        }
      },
      "EcommerceSettingsRequest": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "webhook_secret": {
            "type": "string",
            "description": "Absent keeps the stored secret; null is refused (400 invalid_webhook_secret); \"\" stores an empty secret (not connected). Stored in clear."
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "description": "Absent keeps the stored config; must be a plain object (400 invalid_config). Returned in full by GET; no adapter reads it."
          }
        }
      },
      "EcommerceSettingsSaved": {
        "type": "object",
        "required": [
          "success",
          "platform",
          "webhook_url"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "platform": {
            "$ref": "#/components/schemas/EcommercePlatform"
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "description": "Register this exact URL on the platform; it carries the tenant id."
          }
        }
      },
      "EcommerceSettingsView": {
        "type": "object",
        "required": [
          "platform",
          "connected",
          "config",
          "webhook_url"
        ],
        "properties": {
          "platform": {
            "$ref": "#/components/schemas/EcommercePlatform"
          },
          "connected": {
            "type": "boolean"
          },
          "config": {
            "type": "object"
          },
          "webhook_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "EcommerceOrderRequest": {
        "type": "object",
        "additionalProperties": true,
        "description": "Platform-specific field names (* CHECKOUT_SESSION_SPEC): id key / success-URL key / cancel-URL key / reference prefix = shopify: id / redirect_url / cancel_url / SHOPIFY-TX-; woocommerce: orderId / returnUrl / cancelUrl / WOO-ORDER-; whmcs: invoiceId / returnUrl / cancelUrl / WHMCS-INV-; magento: orderId / successUrl / cancelUrl / MAGENTO-ORDER-; prestashop: orderId / successUrl / cancelUrl / PRESTASHOP-ORDER-; wix: orderId / returnUrl / cancelUrl / WIX-ORDER-; squarespace: orderId / returnUrl / cancelUrl / SQSP-ORDER-; bigcommerce: orderId / returnUrl / cancelUrl / BC-ORDER-; opencart: orderId / returnUrl / cancelUrl / OC-ORDER-. Every platform also takes amount (required, truthy; sent on as String(amount)), currency (required; stringified and upper-cased, then it must be EUR or USD), network (default ethereum), settlementCurrency (default USDC). The id, amount, currency and success URL must be truthy (0 or \"\" is parameter_missing).",
        "properties": {
          "id": {
            "description": "Shopify transaction id."
          },
          "orderId": {
            "description": "Order id (woocommerce, magento, prestashop, wix, squarespace, bigcommerce, opencart)."
          },
          "invoiceId": {
            "description": "WHMCS invoice id."
          },
          "amount": {
            "description": "Fiat amount: a positive decimal with at most 2 decimals once stringified (invalid_amount)."
          },
          "currency": {
            "description": "EUR or USD, any case."
          },
          "redirect_url": {
            "description": "Shopify success URL."
          },
          "returnUrl": {
            "description": "Success URL (woocommerce, whmcs, wix, squarespace, bigcommerce, opencart)."
          },
          "successUrl": {
            "description": "Success URL (magento, prestashop)."
          },
          "cancel_url": {
            "description": "Shopify cancel URL; default the success URL."
          },
          "cancelUrl": {
            "description": "Cancel URL (the others); default the success URL."
          },
          "network": {
            "description": "A checkout network, exactly as the catalogue spells it (lower case; the lookup is case-sensitive), else 400 invalid_network; default ethereum."
          },
          "settlementCurrency": {
            "description": "A quotable currency of the network, any case (default USDC)."
          }
        }
      },
      "EcommerceOrderCheckoutSession": {
        "type": "object",
        "description": "The checkout_session object of POST /v1/checkout/sessions plus the quote provenance.",
        "required": [
          "id",
          "object",
          "status",
          "payment_status",
          "amount",
          "currency",
          "network",
          "payment_amount",
          "payment_currency",
          "exchange_rate",
          "success_url",
          "cancel_url",
          "payment_url",
          "expires_at",
          "is_test",
          "created_at",
          "payments",
          "rate_source",
          "rate_fetched_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^cs_[0-9a-f]{32}$"
          },
          "object": {
            "const": "checkout_session"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "completed",
              "expired"
            ]
          },
          "payment_status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "created",
              "paid",
              "expired",
              null
            ]
          },
          "amount": {
            "type": "string"
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR",
              "USD"
            ]
          },
          "network": {
            "type": [
              "string",
              "null"
            ]
          },
          "payment_amount": {
            "type": [
              "string",
              "null"
            ]
          },
          "payment_currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "exchange_rate": {
            "type": [
              "string",
              "null"
            ]
          },
          "success_url": {
            "type": "string"
          },
          "cancel_url": {
            "type": "string"
          },
          "payment_url": {
            "type": "string",
            "format": "uri"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "is_test": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "payments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "status": {
                  "type": "string"
                },
                "merchant_reference": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "expires_at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          },
          "rate_source": {
            "type": "string"
          },
          "rate_fetched_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "MemberRole": {
        "type": "string",
        "enum": [
          "owner",
          "admin",
          "developer",
          "approver",
          "viewer"
        ],
        "description": "owner: scope ceiling *, approves, manages the team and the policy, grants owner. admin: ceiling *, approves, manages the team; not the policy, not owner. developer: ceiling *, no capability. approver: every:read scope, approves. viewer: every:read scope, no capability. Registering or verifying a payroll payer wallet needs owner or admin."
      },
      "MemberStatus": {
        "type": "string",
        "enum": [
          "active",
          "suspended"
        ]
      },
      "MemberCapabilities": {
        "type": "object",
        "required": [
          "can_approve",
          "can_manage_team",
          "can_set_approval_policy"
        ],
        "description": "The role's capabilities, whatever the member's status.",
        "properties": {
          "can_approve": {
            "type": "boolean",
            "description": "owner, admin, approver."
          },
          "can_manage_team": {
            "type": "boolean",
            "description": "owner, admin."
          },
          "can_set_approval_policy": {
            "type": "boolean",
            "description": "owner."
          }
        }
      },
      "List": {
        "type": "object",
        "description": "Every list body. has_more is always false on the lists that are not paged. An operation narrows `data` with allOf",
        "required": [
          "object",
          "data",
          "has_more"
        ],
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {}
          },
          "has_more": {
            "type": "boolean"
          }
        }
      },
      "PayoutApprovalPolicyMode": {
        "type": "string",
        "enum": [
          "never",
          "always",
          "above"
        ]
      },
      "SandboxCounts": {
        "type": "object",
        "description": "Sandbox (is_test = 1) row counts per object.",
        "required": [
          "refunds",
          "deposits",
          "payments",
          "checkout_sessions",
          "payment_requests",
          "payment_links",
          "payment_channels",
          "customers",
          "payroll_tx_watch",
          "payroll_authorizations",
          "payroll_items",
          "payroll_batches",
          "payroll_payer_wallets",
          "recipients",
          "jobs",
          "ledger_accounts",
          "webhook_deliveries"
        ],
        "properties": {
          "refunds": {
            "type": "integer",
            "minimum": 0
          },
          "deposits": {
            "type": "integer",
            "minimum": 0
          },
          "payments": {
            "type": "integer",
            "minimum": 0
          },
          "checkout_sessions": {
            "type": "integer",
            "minimum": 0
          },
          "payment_requests": {
            "type": "integer",
            "minimum": 0
          },
          "payment_links": {
            "type": "integer",
            "minimum": 0
          },
          "payment_channels": {
            "type": "integer",
            "minimum": 0
          },
          "customers": {
            "type": "integer",
            "minimum": 0
          },
          "payroll_tx_watch": {
            "type": "integer",
            "minimum": 0
          },
          "payroll_authorizations": {
            "type": "integer",
            "minimum": 0
          },
          "payroll_items": {
            "type": "integer",
            "minimum": 0
          },
          "payroll_batches": {
            "type": "integer",
            "minimum": 0
          },
          "payroll_payer_wallets": {
            "type": "integer",
            "minimum": 0
          },
          "recipients": {
            "type": "integer",
            "minimum": 0
          },
          "jobs": {
            "type": "integer",
            "minimum": 0
          },
          "ledger_accounts": {
            "type": "integer",
            "minimum": 0
          },
          "webhook_deliveries": {
            "type": "integer",
            "minimum": 0
          }
        },
        "additionalProperties": false
      },
      "ApiKeyScope": {
        "type": "string",
        "description": "One scope: `*` (everything) or `<family>:read` / `<family>:write`, write implying read. An unmapped /v1 path needs `*`; /v1/api-keys/* needs nothing. The path is matched in ASCII lower case, as Express routes it, so a capitalised path needs the same scope as the lower-case one (since 2026-10-28 in production). A member-bound key exercises its scopes narrowed to its member's role ceiling on every request.",
        "enum": [
          "*",
          "checkout:read",
          "checkout:write",
          "payouts:read",
          "payouts:write",
          "refunds:read",
          "refunds:write",
          "customers:read",
          "customers:write",
          "balance:read",
          "balance:write",
          "webhooks:read",
          "webhooks:write",
          "integrations:read",
          "integrations:write",
          "team:read",
          "team:write",
          "sandbox:read",
          "sandbox:write",
          "payroll:read",
          "payroll:write"
        ]
      },
      "ApiKeyPayrollNetwork": {
        "type": "string",
        "description": "A payroll network name, lower case: the twelve EVM networks by name, then each non-EVM payroll family. Not families: `ethereum`, not `evm`. Litecoin is no payroll network. Whether a network can actually pay depends, for a live key, on its family switch and the tenant's lane; production switches on the Solana family only and its lane ceiling is solana only.",
        "enum": [
          "ethereum",
          "base",
          "polygon",
          "arbitrum",
          "bnb",
          "avalanche",
          "robinhood",
          "linea",
          "optimism",
          "monad",
          "megaeth",
          "hyperevm",
          "tron",
          "solana",
          "aptos",
          "sui",
          "ton",
          "stacks",
          "bitcoin"
        ]
      },
      "ApiKey": {
        "type": "object",
        "description": "The api_key object, exactly these fields (left open here only so the mint, rotate and introspection answers can extend it). Never the key material or its hash.",
        "required": [
          "id",
          "object",
          "prefix",
          "name",
          "scopes",
          "status",
          "is_test",
          "expires_at",
          "ip_allowlist",
          "rotated_from_key_id",
          "member_id",
          "payroll_networks",
          "created_at",
          "revoked_at",
          "last_used_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "crypto.randomUUID."
          },
          "object": {
            "const": "api_key"
          },
          "prefix": {
            "type": "string",
            "enum": [
              "fp_live_",
              "fp_test_"
            ],
            "description": "The key's mode only, the same for every key of that mode. Never a per-key prefix."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 100,
            "description": "Optional and never unique; empty string allowed."
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKeyScope"
            },
            "description": "As minted or updated, deduplicated; [\"*\"] collapses everything; a row with no scopes reads [\"*\"]. Not narrowed by the role here (see effective_scopes on /v1/api-keys/me)."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "expired",
              "revoked"
            ],
            "description": "Computed at read time - revoked if revoked_at is set, else expired if expires_at is at or before now, else active."
          },
          "is_test": {
            "type": "boolean",
            "description": "true for an fp_test_ key."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO-8601 UTC; null = never expires."
          },
          "ip_allowlist": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Normalised entries (a single address without its /32 or /128); null = no restriction. An entry stored before 2026-10-28 may still be a /0 or an IPv6 range holding every IPv4 client and is enforced as stored. FastPay's read-only listing of such entries on production after the 2026-10-28 deploy (recorded 12:33 UTC that day) printed none."
          },
          "rotated_from_key_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The predecessor of a rotation successor."
          },
          "member_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The member the key is issued to; null for an unbound (legacy or admin-minted) key, which holds no capability (except creating the first members of a tenant that has none)."
          },
          "payroll_networks": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/ApiKeyPayrollNetwork"
            },
            "description": "The payroll networks the key may act on; null = every network. Set at mint only, copied by rotations."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Written on a successful authentication, before the IP, sandbox-host and scope checks, at most every 5 minutes (5 min). Not per route or network."
          }
        }
      },
      "ApiKeyIntrospectionMember": {
        "type": "object",
        "required": [
          "id",
          "name",
          "role",
          "effective_scopes"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "role": {
            "$ref": "#/components/schemas/ApiKeyRole"
          },
          "effective_scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKeyScope"
            },
            "description": "The key's scopes narrowed to the role's current ceiling."
          }
        }
      },
      "PayrollNetworkName": {
        "type": "string",
        "description": "The 19 payroll networks in PAYROLL_NETWORKS order: the 12 EVM chains, then the non-EVM families. Every payroll route accepts all 19; which may carry live payroll is the switches', the ceiling's and the lanes' business.",
        "enum": [
          "ethereum",
          "base",
          "polygon",
          "arbitrum",
          "bnb",
          "avalanche",
          "robinhood",
          "linea",
          "optimism",
          "monad",
          "megaeth",
          "hyperevm",
          "tron",
          "solana",
          "aptos",
          "sui",
          "ton",
          "stacks",
          "bitcoin"
        ]
      },
      "PayerWalletChallengeFields": {
        "type": "object",
        "description": "The challenge view the register and challenge routes add. challenge_format is the envelope's format or kind: personal_sign (EVM; EIP-191, a Safe through EIP-1271), tip191_v2 (Tron, TronLink signMessageV2), solana_sign_message, aptos_sign_message, sui_personal_message, ton_proof, stx_sign_structured_message (Stacks), bitcoin_message. challenge_extra is the rest of the envelope: live Solana {encoding: utf8, envelopes: [raw, solana_offchain_v0]}; Sui {encoding: utf8}; Aptos {nonce, address: true, chainId: true}; TON {payload, network}; Stacks {message_type, domain, network, control_tx}; Bitcoin {encoding, methods, self_send_op_return_hex} or a vault's envelope; EVM and Tron {}; every sandbox wallet {} (the mock sends kind, format and message only).",
        "required": [
          "challenge",
          "challenge_format",
          "challenge_extra",
          "challenge_expires_at",
          "chain_ref"
        ],
        "properties": {
          "challenge": {
            "type": "string"
          },
          "challenge_format": {
            "type": [
              "string",
              "null"
            ]
          },
          "challenge_extra": {
            "type": "object",
            "additionalProperties": true
          },
          "challenge_expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "chain_ref": {
            "type": "string"
          },
          "nonce": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "TON (at_connect) only: the nonce the wallet signs as its ton_proof payload."
          },
          "ton_proof_payload": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "TON only; same value as nonce."
          },
          "accepted_domains": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "TON only: the domains a ton_proof may carry (also in sandbox, possibly empty)."
          }
        }
      },
      "PayerWalletProof": {
        "description": "The proof's shape is decided by the wallet's network (and sandbox keys always use the sandbox shape), not by a field in the body.",
        "anyOf": [
          {
            "$ref": "#/components/schemas/EvmPayerProof"
          },
          {
            "$ref": "#/components/schemas/TronPayerProof"
          },
          {
            "$ref": "#/components/schemas/SolanaPayerProof"
          },
          {
            "$ref": "#/components/schemas/AptosPayerProof"
          },
          {
            "$ref": "#/components/schemas/SuiPayerProof"
          },
          {
            "$ref": "#/components/schemas/TonPayerProof"
          },
          {
            "$ref": "#/components/schemas/StacksPayerProof"
          },
          {
            "$ref": "#/components/schemas/BitcoinPayerProof"
          },
          {
            "$ref": "#/components/schemas/SandboxPayerProof"
          }
        ]
      },
      "PayrollNetwork": {
        "type": "string",
        "description": "Every payroll network the relay knows, 19 (= the 12 EVM names, megaeth and hyperevm included, then the non-EVM families; PAYROLL_NETWORKS is the same list). Live availability is per family switch and per tenant lane: production opens Solana only. The sandbox accepts every one.",
        "enum": [
          "ethereum",
          "base",
          "polygon",
          "arbitrum",
          "bnb",
          "avalanche",
          "robinhood",
          "linea",
          "optimism",
          "monad",
          "megaeth",
          "hyperevm",
          "tron",
          "solana",
          "aptos",
          "sui",
          "ton",
          "stacks",
          "bitcoin"
        ]
      },
      "PayrollBatchItemInput": {
        "type": "object",
        "description": "One line. recipient_address or recipient_id is required; both may be sent, and then the address must equal the recipient's registered address on the network (400 recipient_wallet_mismatch). Every recipient is canonicalised with the network's strict rule and refused when it is the batch's payer, the zero address, the disburser, a token contract or another FastPay deployment address. Unknown fields are ignored.",
        "required": [
          "reference",
          "amount"
        ],
        "properties": {
          "reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "pattern": "^[\\x20-\\x7E]+$",
            "description": "Printable ASCII 1-256. Held tenant-wide, per mode, on every network and across makers while its item is pending, authorized or completed; released only when the item ends failed or cancelled. Repeated inside one batch: 409 duplicate_item_reference.",
            "examples": [
              "example-2026-10-sol-emp-001"
            ]
          },
          "recipient_address": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "description": "The recipient's address on the batch network, in a spelling the network's strict input rule accepts (Solana base58, exactly as typed, 32-44 characters)."
          },
          "recipient_id": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^rcp_[A-Za-z0-9]+$"
          },
          "amount": {
            "type": "string",
            "pattern": "^\\d+(\\.\\d+)?$",
            "description": "A positive decimal STRING in whole token units (a JSON number is refused): at least one non-zero digit, at most the per-item maximum whole units (default MAX_AMOUNT_PER_PAYMENT 1,000,000, exactly the ceiling is allowed), and no more decimal places than the token (USDC 6), trailing zeros counted: '1500.0000000' is refused for USDC although it equals 1500 - else 400 invalid_items with details {position} for the first such item. Stored and echoed as sent.",
            "examples": [
              "1500.00"
            ]
          }
        }
      },
      "PayrollApprovalDecision": {
        "type": "object",
        "description": "One approve or reject decision, oldest first.",
        "required": [
          "member_id",
          "member_name",
          "decision",
          "reason",
          "created_at"
        ],
        "properties": {
          "member_id": {
            "type": "string"
          },
          "member_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The member's name, joined for display."
          },
          "decision": {
            "type": "string",
            "enum": [
              "approve",
              "reject"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "A reject's reason; null for an approve."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PayrollItemCounts": {
        "type": "object",
        "description": "Item counts per status, computed on read.",
        "required": [
          "pending",
          "authorized",
          "completed",
          "failed",
          "cancelled"
        ],
        "properties": {
          "pending": {
            "type": "integer",
            "minimum": 0
          },
          "authorized": {
            "type": "integer",
            "minimum": 0
          },
          "completed": {
            "type": "integer",
            "minimum": 0
          },
          "failed": {
            "type": "integer",
            "minimum": 0
          },
          "cancelled": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "PayrollItem": {
        "type": "object",
        "description": "One item.",
        "required": [
          "id",
          "object",
          "batch_id",
          "position",
          "reference",
          "recipient",
          "recipient_id",
          "amount",
          "fee",
          "status",
          "allocation_id",
          "authorization_id",
          "attempt_count",
          "last_failure_reason",
          "tx_hash",
          "chain_position",
          "completed_at",
          "is_test",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "pyi_0123456789abcdef0123456789abcdef"
            ]
          },
          "object": {
            "const": "payroll_item"
          },
          "batch_id": {
            "type": "string"
          },
          "position": {
            "type": "integer",
            "minimum": 0
          },
          "reference": {
            "type": "string"
          },
          "recipient": {
            "type": "string",
            "description": "The display form of the canonical recipient address."
          },
          "recipient_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "amount": {
            "type": "string",
            "description": "The amount as sent."
          },
          "fee": {
            "type": [
              "string",
              "null"
            ],
            "description": "The FastPay fee charged, in token units; null until completed."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "authorized",
              "completed",
              "failed",
              "cancelled"
            ],
            "description": "pending -> authorized | cancelled | failed; authorized -> completed | pending."
          },
          "allocation_id": {
            "type": "string",
            "pattern": "^0x[0-9a-f]{64}$",
            "description": "The on-chain allocation id, keccak256 over a domain and the item id."
          },
          "authorization_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The authorization holding (or last holding) the item."
          },
          "attempt_count": {
            "type": "integer",
            "minimum": 0
          },
          "last_failure_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Masked of any URL."
          },
          "tx_hash": {
            "type": [
              "string",
              "null"
            ]
          },
          "chain_position": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "is_test": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PayrollIntentAuthorization": {
        "type": "object",
        "description": "One authorization of an intent. The list omits payloads.",
        "required": [
          "authorization_id",
          "chunk_index",
          "status",
          "auth_scheme",
          "entry_match",
          "expires_at",
          "groups",
          "items",
          "totals"
        ],
        "properties": {
          "authorization_id": {
            "type": "string",
            "examples": [
              "pya_0123456789abcdef0123456789abcdef"
            ]
          },
          "chunk_index": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "issuing",
              "live",
              "flagged",
              "settled",
              "expired",
              "void",
              "deployment_retired"
            ]
          },
          "auth_scheme": {
            "type": "string",
            "enum": [
              "batch",
              "per_entry",
              "none"
            ]
          },
          "entry_match": {
            "type": "string",
            "enum": [
              "all_entries",
              "independent"
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "groups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PayrollIntentSignedGroup"
            }
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PayrollIntentAuthorizationItem"
            }
          },
          "totals": {
            "$ref": "#/components/schemas/PayrollIntentTotals"
          },
          "flag_kind": {
            "type": "string",
            "enum": [
              "suspect",
              "trust_mismatch",
              "incomplete_group",
              "deployment_mismatch"
            ]
          },
          "flagged_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "payloads": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PayrollIntentPayload"
            }
          },
          "envelope_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "cancel_payload": {
            "$ref": "#/components/schemas/PayrollIntentCancelPayload"
          },
          "from": {
            "type": "string",
            "description": "EVM single evm_tx copies."
          },
          "to": {
            "type": "string"
          },
          "data": {
            "type": "string"
          },
          "value": {
            "type": "string"
          },
          "gas_limit": {
            "type": [
              "string",
              "integer"
            ]
          },
          "digest": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "PayrollIntentTotals": {
        "type": "object",
        "required": [
          "amount",
          "fee",
          "total"
        ],
        "properties": {
          "amount": {
            "type": "string",
            "description": "Token units (display decimals)."
          },
          "fee": {
            "type": "string"
          },
          "total": {
            "type": "string"
          }
        }
      },
      "PayrollIntentListEntry": {
        "type": "object",
        "required": [
          "id",
          "object",
          "batch_id",
          "network",
          "chain_ref",
          "currency",
          "fee_bps",
          "expires_at",
          "flagged",
          "transactions",
          "is_test",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "const": "payroll_intent"
          },
          "batch_id": {
            "type": "string"
          },
          "network": {
            "$ref": "#/components/schemas/PayrollNetwork"
          },
          "chain_ref": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "fee_bps": {
            "type": "integer"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "flagged": {
            "type": "boolean"
          },
          "transactions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PayrollIntentAuthorization"
            }
          },
          "is_test": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "description": "One outbox row (superseded_at is stored but not serialized).",
        "required": [
          "id",
          "object",
          "event_id",
          "allocation_id",
          "job_id",
          "action",
          "status",
          "callback_url",
          "attempts",
          "last_error",
          "response_status",
          "response_body",
          "webhook_endpoint_id",
          "is_test",
          "delivered",
          "delivered_at",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "The outbox's integer id as text; the list cursor."
          },
          "object": {
            "const": "webhook_delivery"
          },
          "event_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "This row's delivery id, also inside the payload. Differs per target; the event's own id is payload.id."
          },
          "allocation_id": {
            "type": "string",
            "description": "The outbox key (see the allocation_id filter)."
          },
          "job_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The job id (payment.*, payout_job.*), the payroll batch id (payroll_* events of a batch) or the payer wallet id; null for checkout, refund and deposit events."
          },
          "action": {
            "type": "string",
            "description": "payload.action, always callback."
          },
          "status": {
            "type": "string",
            "description": "payload.status: the event envelope status (paid, completed, open, attempt_failed,...), not a delivery state."
          },
          "callback_url": {
            "type": "string",
            "description": "The URL this row is sent to, fixed when recorded."
          },
          "attempts": {
            "type": "integer",
            "minimum": 0,
            "description": "Failed rounds recorded on the row: each is an exhausted in-process budget (up to 5 POSTs), a refused URL or a send with no secret."
          },
          "last_error": {
            "type": [
              "string",
              "null"
            ],
            "description": "The last failure (\"Initial delivery exhausted retries for status <status>.\", \"Redelivery attempt exhausted retries (previous attempts: <n).\", \"callback_url no longer resolves to a public address...\") or the supersede reason: \"webhook endpoint <id> was deleted - delivery superseded.\" / \"... is disabled - delivery superseded.\" or \"Superseded by a later delivered payment.* webhook...\". Match \"superseded\" case-insensitively. A later failed manual redelivery overwrites it; a later success clears only a supersede text, any other text stays."
          },
          "response_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The last attempt's HTTP status; null for a transport failure, a refused URL or no attempt yet. A superseded row keeps its last failure's status."
          },
          "response_body": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 4096,
            "description": "The first the stored body cap characters of the last attempt's answer, of which the relay reads at most 64 KiB (a JSON answer re-serialised compactly)."
          },
          "webhook_endpoint_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set for an account-endpoint row; null for a per-object callback_url row."
          },
          "is_test": {
            "type": "boolean"
          },
          "delivered": {
            "type": "boolean",
            "description": "delivered_at is set. TRUE ALSO FOR A SUPERSEDED ROW (closed unsent); a failed manual redelivery of a delivered row leaves it true."
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it was delivered or closed; a successful manual redelivery stamps it again, restarting its 90-day retention."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEnabledEvents": {
        "type": "array",
        "minItems": 1,
        "items": {
          "$ref": "#/components/schemas/WebhookEnabledEventsEntry"
        },
        "description": "Stored deduplicated in the order sent; any \"*\" collapses the list to [\"*\"]. An endpoint whose list names types receives only those; [\"*\"] includes types added to the catalogue later."
      },
      "CheckoutNetwork": {
        "type": "string",
        "description": "The 17 checkout networks: every EVM network of CHAINS plus tron, solana, aptos, sui, ton. Bitcoin is not a checkout network.",
        "enum": [
          "ethereum",
          "base",
          "polygon",
          "arbitrum",
          "bnb",
          "avalanche",
          "robinhood",
          "linea",
          "optimism",
          "monad",
          "megaeth",
          "hyperevm",
          "solana",
          "tron",
          "aptos",
          "sui",
          "ton"
        ]
      },
      "CheckoutDecimalAmount": {
        "type": "string",
        "pattern": "^\\d+(\\.\\d+)?$",
        "description": "A plain decimal string: digits, optional fraction, no sign, no exponent. A JSON number is refused."
      },
      "CheckoutQrPayloads": {
        "type": "object",
        "required": [
          "checkout"
        ],
        "additionalProperties": false,
        "description": "solanaPay only for a live Solana request (or a live fixed-amount Solana link).",
        "properties": {
          "checkout": {
            "$ref": "#/components/schemas/CheckoutQrPayload"
          },
          "solanaPay": {
            "$ref": "#/components/schemas/CheckoutQrPayload"
          }
        }
      },
      "CheckoutIntentEvm": {
        "description": "Send `approval` first when present (ERC-20 allowance short), then {to, data, value}.",
        "allOf": [
          {
            "$ref": "#/components/schemas/CheckoutIntentCommon"
          },
          {
            "type": "object",
            "required": [
              "chainId",
              "to",
              "data",
              "value",
              "approval"
            ],
            "properties": {
              "chainId": {
                "type": "integer"
              },
              "to": {
                "type": "string",
                "description": "The FastPayDisburser."
              },
              "data": {
                "type": "string",
                "description": "disburse or disburseToken calldata carrying FastPay's authorization."
              },
              "value": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Hex quantity (amount + fee) on a native payment; null for a token."
              },
              "approval": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "to": {
                    "type": "string"
                  },
                  "data": {
                    "type": "string"
                  }
                }
              }
            }
          }
        ]
      },
      "CheckoutIntentTron": {
        "description": "approvalOnly true = only `approval` (TRC20 allowance short): broadcast it, wait, call the intent again. transactionExpiresAt is the broadcast window of the next transaction to sign (the Tron transaction validity, 10 min, bounded by the authorization).",
        "allOf": [
          {
            "$ref": "#/components/schemas/CheckoutIntentCommon"
          },
          {
            "type": "object",
            "required": [
              "chainId",
              "to",
              "transaction",
              "approval",
              "approvalOnly",
              "transactionExpiresAt"
            ],
            "properties": {
              "chainId": {
                "type": [
                  "integer",
                  "string"
                ]
              },
              "to": {
                "type": "string"
              },
              "transaction": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "TronWeb unsigned transaction (disburse)."
              },
              "approval": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "TronWeb unsigned approve transaction."
              },
              "approvalOnly": {
                "type": "boolean"
              },
              "transactionExpiresAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "CheckoutIntentSolana": {
        "description": "transaction is a base64 unsigned legacy transaction (verify + disburse instructions, recipient/treasury token accounts created idempotently at the payer's cost); reference is the Solana Pay reference key it carries.",
        "allOf": [
          {
            "$ref": "#/components/schemas/CheckoutIntentCommon"
          },
          {
            "type": "object",
            "required": [
              "transaction",
              "reference"
            ],
            "properties": {
              "transaction": {
                "type": "string",
                "contentEncoding": "base64"
              },
              "reference": {
                "type": "string"
              }
            }
          }
        ]
      },
      "CheckoutIntentAptos": {
        "description": "transaction is an AIP-62 entry-function payload.",
        "allOf": [
          {
            "$ref": "#/components/schemas/CheckoutIntentCommon"
          },
          {
            "type": "object",
            "required": [
              "chainId",
              "transaction"
            ],
            "properties": {
              "chainId": {
                "type": "integer"
              },
              "transaction": {
                "type": "object",
                "required": [
                  "function",
                  "typeArguments",
                  "functionArguments"
                ],
                "properties": {
                  "function": {
                    "type": "string"
                  },
                  "typeArguments": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "functionArguments": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "CheckoutIntentSui": {
        "description": "transaction is the awaited Transaction.toJSON of the PTB, a JSON string: @mysten/sui 2.27.1 declares toJSON: Promise<string>. The client does Transaction.from(transaction) and the wallet resolves gas and objects. expiresAt has millisecond precision.",
        "allOf": [
          {
            "$ref": "#/components/schemas/CheckoutIntentCommon"
          },
          {
            "type": "object",
            "required": [
              "transaction"
            ],
            "properties": {
              "transaction": {
                "type": "string",
                "contentMediaType": "application/json"
              }
            }
          }
        ]
      },
      "CheckoutIntentTon": {
        "description": "transaction is TonConnect's sendTransaction request (messages[].amount in nanotons). attachedValue is that same native TON as a decimal string in TON with 9 decimals, not nanotons (toAmountString): the payment plus the gas reserve on a native payment, gas only on a jetton, so it is not comparable to total.",
        "allOf": [
          {
            "$ref": "#/components/schemas/CheckoutIntentCommon"
          },
          {
            "type": "object",
            "required": [
              "transaction",
              "tonNetwork",
              "attachedValue"
            ],
            "properties": {
              "transaction": {
                "type": "object",
                "required": [
                  "validUntil",
                  "messages"
                ],
                "properties": {
                  "validUntil": {
                    "type": "integer",
                    "description": "Epoch seconds (the authorization expiry)."
                  },
                  "messages": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "address",
                        "amount",
                        "payload"
                      ],
                      "properties": {
                        "address": {
                          "type": "string"
                        },
                        "amount": {
                          "type": "string",
                          "description": "Nanotons."
                        },
                        "payload": {
                          "type": "string",
                          "contentEncoding": "base64",
                          "description": "BOC of the disburse body."
                        }
                      }
                    }
                  }
                }
              },
              "tonNetwork": {
                "type": "string",
                "description": "TonConnect chain id (239 mainnet, -3 testnet)."
              },
              "attachedValue": {
                "type": "string",
                "pattern": "^\\d+\\.\\d{9}$",
                "description": "Native TON attached, a decimal string with 9 decimals (e.g. \"0.150000000\")."
              }
            }
          }
        ]
      },
      "CheckoutIntentSandbox": {
        "description": "- a sandbox request on any network: nothing is signed or built.",
        "allOf": [
          {
            "$ref": "#/components/schemas/CheckoutIntentCommon"
          },
          {
            "type": "object",
            "required": [
              "simulated",
              "chainId",
              "to",
              "data",
              "value",
              "approval",
              "sandboxTxHashes"
            ],
            "properties": {
              "simulated": {
                "const": true
              },
              "chainId": {
                "const": 0
              },
              "to": {
                "const": "0x0000000000000000000000000000000000000001"
              },
              "data": {
                "const": "0x"
              },
              "value": {
                "type": "null"
              },
              "approval": {
                "type": "null"
              },
              "sandboxTxHashes": {
                "type": "object",
                "properties": {
                  "failed": {
                    "const": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
                  },
                  "pending": {
                    "const": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
                  }
                }
              }
            }
          }
        ]
      },
      "CheckoutSessionPaymentSummary": {
        "type": "object",
        "required": [
          "id",
          "status",
          "merchant_reference",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "pay_ + 32 hex."
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "paid",
              "expired"
            ]
          },
          "merchant_reference": {
            "type": [
              "string",
              "null"
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CheckoutBlockchainTransaction": {
        "type": "object",
        "required": [
          "id",
          "object",
          "payment_id",
          "network",
          "currency",
          "amount",
          "address",
          "status",
          "tx_hash",
          "confirmations",
          "required_confirmations",
          "detected_at",
          "confirmed_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "att_ + 32 hex."
          },
          "object": {
            "const": "blockchain_transaction"
          },
          "payment_id": {
            "type": "string"
          },
          "network": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "amount": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "confirmed",
              "reorged"
            ]
          },
          "tx_hash": {
            "type": [
              "string",
              "null"
            ]
          },
          "confirmations": {
            "type": [
              "integer",
              "null"
            ]
          },
          "required_confirmations": {
            "type": [
              "integer",
              "null"
            ]
          },
          "detected_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "confirmed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CheckoutRefundTransaction": {
        "description": "The unsigned refund, per network (* buildRefund): EVM {chainId, to, data, value}; Tron {transaction}; Solana {transaction: base64}; Aptos {transaction: AIP-62 payload, sender}; Sui {transaction}; TON {transaction: {validUntil?, messages}}; sandbox {simulated, chainId 0, to, data, value, currency, amount, sandboxTxHashes}. Sui's transaction is JSON text (await tx.toJSON).",
        "anyOf": [
          {
            "type": "object",
            "required": [
              "chainId",
              "from",
              "to",
              "data",
              "value"
            ],
            "properties": {
              "chainId": {
                "type": "integer"
              },
              "from": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "to": {
                "type": "string"
              },
              "data": {
                "type": "string"
              },
              "value": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "simulated": {
                "const": true
              },
              "currency": {
                "type": "string",
                "description": "Sandbox only."
              },
              "amount": {
                "type": "string",
                "description": "Sandbox only."
              },
              "sandboxTxHashes": {
                "type": "object"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "transaction"
            ],
            "properties": {
              "transaction": {
                "type": [
                  "object",
                  "string"
                ]
              },
              "sender": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        ]
      },
      "PaymentLinkCommon": {
        "type": "object",
        "required": [
          "id",
          "object",
          "network",
          "currency",
          "amount",
          "min_amount",
          "max_amount",
          "recipient",
          "status",
          "available",
          "unavailable_reason",
          "expires_at",
          "is_test",
          "url",
          "qr"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "const": "payment_link"
          },
          "network": {
            "$ref": "#/components/schemas/CheckoutNetwork"
          },
          "currency": {
            "type": "string"
          },
          "amount": {
            "type": [
              "string",
              "null"
            ]
          },
          "min_amount": {
            "type": [
              "string",
              "null"
            ]
          },
          "max_amount": {
            "type": [
              "string",
              "null"
            ]
          },
          "recipient": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ]
          },
          "available": {
            "type": "boolean"
          },
          "unavailable_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "inactive",
              "expired",
              "exhausted",
              null
            ]
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "is_test": {
            "type": "boolean"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "<base>/checkout/link.html?id=<plink"
          },
          "qr": {
            "$ref": "#/components/schemas/CheckoutQrPayloads"
          }
        }
      },
      "X402PaymentPayload": {
        "type": "object",
        "description": "`payload` is the scheme's own object (EIP-3009 authorization and signature, a signed Solana transaction,...).",
        "required": [
          "x402Version",
          "accepted",
          "payload"
        ],
        "properties": {
          "x402Version": {
            "const": 2
          },
          "accepted": {
            "$ref": "#/components/schemas/X402PaymentRequirements"
          },
          "payload": {
            "type": "object",
            "additionalProperties": true
          },
          "resource": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/X402ResourceInfo"
              },
              {
                "type": "null"
              }
            ]
          },
          "extensions": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "X402PaymentRequirements": {
        "type": "object",
        "required": [
          "scheme",
          "network",
          "amount",
          "asset",
          "payTo",
          "maxTimeoutSeconds"
        ],
        "properties": {
          "scheme": {
            "type": "string",
            "minLength": 1,
            "examples": [
              "exact"
            ]
          },
          "network": {
            "type": "string",
            "minLength": 1,
            "description": "CAIP-2, e.g. eip155:8453, solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp, aptos:1."
          },
          "amount": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,77}$",
            "description": "Atomic units, one decimal spelling."
          },
          "asset": {
            "type": "string",
            "minLength": 1
          },
          "payTo": {
            "type": "string",
            "minLength": 1
          },
          "maxTimeoutSeconds": {
            "type": "integer",
            "minimum": 1
          },
          "extra": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "X402ResourceInfo": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "description": {
            "type": "string"
          },
          "mimeType": {
            "type": "string"
          }
        }
      },
      "CatalogNetwork": {
        "type": "object",
        "required": [
          "object",
          "network",
          "live"
        ],
        "additionalProperties": false,
        "properties": {
          "object": {
            "const": "network"
          },
          "network": {
            "type": "string",
            "enum": [
              "ethereum",
              "base",
              "polygon",
              "arbitrum",
              "bnb",
              "avalanche",
              "robinhood",
              "linea",
              "optimism",
              "monad",
              "megaeth",
              "hyperevm",
              "solana",
              "tron",
              "aptos",
              "sui",
              "ton"
            ]
          },
          "live": {
            "type": "boolean",
            "description": "Has a checkout adapter; true for every entry."
          }
        }
      },
      "CatalogCurrency": {
        "type": "object",
        "required": [
          "object",
          "currency",
          "network",
          "decimals",
          "type",
          "live"
        ],
        "additionalProperties": false,
        "properties": {
          "object": {
            "const": "currency"
          },
          "currency": {
            "type": "string",
            "description": "Symbol as the network's set spells it (e.g. USDC, USDe, SOL)."
          },
          "network": {
            "type": "string",
            "enum": [
              "ethereum",
              "base",
              "polygon",
              "arbitrum",
              "bnb",
              "avalanche",
              "robinhood",
              "linea",
              "optimism",
              "monad",
              "megaeth",
              "hyperevm",
              "solana",
              "tron",
              "aptos",
              "sui",
              "ton"
            ]
          },
          "decimals": {
            "type": "integer",
            "minimum": 0
          },
          "type": {
            "type": "string",
            "enum": [
              "native",
              "token"
            ]
          },
          "live": {
            "type": "boolean"
          }
        }
      },
      "ReconciliationDiscrepancy": {
        "type": "object",
        "required": [
          "payment_id",
          "type",
          "detail"
        ],
        "properties": {
          "payment_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "chain_unconfirmed",
              "chain_reorged",
              "ledger_missing",
              "ledger_amount_mismatch"
            ]
          },
          "detail": {
            "type": "string",
            "description": "Human text; may quote a tx hash and amounts. Wording may change."
          }
        }
      },
      "AccountingMapping": {
        "type": "object",
        "additionalProperties": true,
        "description": "Free-form; what the sync reads: disbursement and gateway: objects keyed \"<SCOPE>:<CURRENCY>\" (chain code for disbursement, network for gateway, case-insensitive) or \"<CURRENCY>\", each {contactId, bankAccountId|bankAccountCode, expenseAccountCode|revenueAccountCode|accountCode, taxCode, feeAccountCode, currencyCode (ISO 4217)}; defaults: {disbursementContactId, gatewayContactId, bankAccountId|bankAccountCode, expenseAccountCode, revenueAccountCode, taxCode, feeAccountCode}; xeroTenantId, quickbooksCompanyId, zohoOrgId for those providers. Contact names (contactName, disbursementContactName, gatewayContactName) are stored but not read. Unset values fall back to FastPay defaults (DEFAULT_GL_CODES).",
        "properties": {
          "disbursement": {
            "type": "object"
          },
          "gateway": {
            "type": "object"
          },
          "defaults": {
            "type": "object"
          },
          "xeroTenantId": {
            "type": "string"
          },
          "quickbooksCompanyId": {
            "type": "string"
          },
          "zohoOrgId": {
            "type": "string"
          }
        }
      },
      "AccountingDisbursementReceipt": {
        "type": "object",
        "required": [
          "success",
          "allocationId",
          "externalBillId",
          "externalPaymentId",
          "syncedAt"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "allocationId": {
            "type": "string"
          },
          "externalBillId": {
            "type": "string"
          },
          "externalPaymentId": {
            "type": "string"
          },
          "syncedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AccountingGatewayReceipt": {
        "type": "object",
        "required": [
          "success",
          "paymentRequestId",
          "externalInvoiceId",
          "externalPaymentId",
          "syncedAt"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "paymentRequestId": {
            "type": "string"
          },
          "externalInvoiceId": {
            "type": "string"
          },
          "externalPaymentId": {
            "type": "string"
          },
          "syncedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EcommercePlatform": {
        "type": "string",
        "enum": [
          "whmcs",
          "shopify",
          "woocommerce",
          "magento",
          "prestashop",
          "wix",
          "squarespace",
          "bigcommerce",
          "opencart"
        ]
      },
      "ApiKeyRole": {
        "type": "string",
        "description": "A member role and what it does to keys: owner and admin (ceiling `*`, approve, manage the team; only owner sets the approval policy and assigns owner), developer (ceiling `*`, no approve, no team), approver (ceiling: every family's:read; approves), viewer (every family's:read).",
        "enum": [
          "owner",
          "admin",
          "developer",
          "approver",
          "viewer"
        ]
      },
      "EvmPayerProof": {
        "title": "EVM (personal_sign / EIP-1271)",
        "type": "object",
        "required": [
          "signature"
        ],
        "properties": {
          "signature": {
            "type": "string",
            "pattern": "^0x([0-9a-fA-F]{2})*$"
          }
        }
      },
      "TronPayerProof": {
        "title": "Tron (TronLink signMessageV2)",
        "type": "object",
        "description": "signature for a plain account, or signatures (one per key of a multisig, 1-5, no repeat).",
        "properties": {
          "signature": {
            "type": "string",
            "pattern": "^(0x)?[0-9a-fA-F]{130}$"
          },
          "signatures": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "pattern": "^(0x)?[0-9a-fA-F]{130}$"
            }
          }
        }
      },
      "SolanaPayerProof": {
        "title": "Solana (signMessage)",
        "type": "object",
        "required": [
          "signature"
        ],
        "description": "A 64-byte ed25519 signature over the exact UTF-8 challenge, raw or in the Solana off-chain message v0 envelope, as base58, base64 or 0x hex. address, if sent, is ignored for the check.",
        "properties": {
          "signature": {
            "type": "string",
            "minLength": 1
          },
          "address": {
            "type": "string"
          }
        }
      },
      "AptosPayerProof": {
        "title": "Aptos (AIP-62 signMessage)",
        "type": "object",
        "required": [
          "full_message",
          "signature",
          "public_key"
        ],
        "properties": {
          "full_message": {
            "type": "string",
            "minLength": 1
          },
          "signature": {
            "type": "string",
            "pattern": "^(0x)?([0-9a-fA-F]{2})+$"
          },
          "public_key": {
            "type": "string",
            "pattern": "^(0x)?([0-9a-fA-F]{2})+$"
          },
          "scheme": {
            "type": "string",
            "enum": [
              "ed25519",
              "single_key",
              "multi_key"
            ],
            "description": "Required unless a 32-byte key with a 64-byte signature (then ed25519)."
          }
        }
      },
      "SuiPayerProof": {
        "title": "Sui (signPersonalMessage)",
        "type": "object",
        "required": [
          "signature"
        ],
        "description": "The wallet's serialized signature, base64.",
        "properties": {
          "signature": {
            "type": "string",
            "minLength": 1
          },
          "address": {
            "type": "string"
          }
        }
      },
      "TonPayerProof": {
        "title": "TON (TonConnect ton_proof)",
        "type": "object",
        "required": [
          "account",
          "proof"
        ],
        "description": "TonConnect's wallet.account and the ton_proof item; the address comes from the proof.",
        "properties": {
          "account": {
            "type": "object",
            "required": [
              "address"
            ],
            "properties": {
              "address": {
                "type": "string",
                "minLength": 1
              },
              "chain": {
                "type": "string"
              },
              "walletStateInit": {
                "type": "string"
              },
              "publicKey": {
                "type": "string"
              }
            }
          },
          "proof": {
            "type": "object",
            "required": [
              "signature",
              "domain",
              "timestamp",
              "payload"
            ],
            "properties": {
              "timestamp": {
                "type": [
                  "integer",
                  "string"
                ]
              },
              "domain": {
                "type": "object",
                "required": [
                  "lengthBytes",
                  "value"
                ],
                "properties": {
                  "lengthBytes": {
                    "type": "integer"
                  },
                  "value": {
                    "type": "string"
                  }
                }
              },
              "payload": {
                "type": "string",
                "description": "The challenge nonce (ton_proof_payload)."
              },
              "signature": {
                "type": "string",
                "minLength": 1
              }
            }
          }
        }
      },
      "StacksPayerProof": {
        "title": "Stacks (SIP-018 or prove-control transaction)",
        "type": "object",
        "description": "{signature, public_key, kind?: stx_sign_structured_message} or {kind: stacks_control_tx, tx_id: 0x + 64 hex}.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "stx_sign_structured_message",
              "stacks_control_tx"
            ]
          },
          "signature": {
            "type": "string"
          },
          "public_key": {
            "type": "string",
            "pattern": "^(0x)?[0-9a-fA-F]+$"
          },
          "tx_id": {
            "type": "string",
            "pattern": "^(0x)?[0-9a-fA-F]{64}$"
          }
        }
      },
      "BitcoinPayerProof": {
        "title": "Bitcoin (BIP-322, BIP-137, self-send, vault PSBT)",
        "type": "object",
        "description": "method defaults to bip322_simple; self_send_opreturn may answer 202 payer_proof_pending until confirmed.",
        "properties": {
          "method": {
            "type": "string",
            "enum": [
              "bip322_simple",
              "bip137",
              "self_send_opreturn",
              "bip322_psbt"
            ],
            "default": "bip322_simple"
          },
          "signature": {
            "type": "string"
          },
          "internal_key": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^(0x)?(([0-9a-fA-F]{64})|(0[23][0-9a-fA-F]{64}))$"
          },
          "txid": {
            "type": "string",
            "pattern": "^[0-9a-fA-F]{64}$"
          },
          "psbt": {
            "type": "string"
          },
          "psbts": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            }
          }
        }
      },
      "SandboxPayerProof": {
        "title": "Sandbox (fp_test_ keys, every network)",
        "type": "object",
        "required": [
          "signature"
        ],
        "description": "Any non-empty signature proves control except the literal 'invalid' (400 invalid_payer_signature); on TON the address comes from proof.address.",
        "properties": {
          "signature": {
            "type": "string",
            "minLength": 1
          },
          "address": {
            "type": "string"
          },
          "public_key": {
            "type": "string"
          }
        }
      },
      "PayrollIntentSignedGroup": {
        "type": "object",
        "required": [
          "group_index",
          "item_ids",
          "digest",
          "entry_digests"
        ],
        "properties": {
          "group_index": {
            "type": "integer"
          },
          "item_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "digest": {
            "type": [
              "string",
              "null"
            ]
          },
          "entry_digests": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          }
        }
      },
      "PayrollIntentAuthorizationItem": {
        "type": "object",
        "required": [
          "id",
          "position",
          "reference",
          "recipient",
          "amount",
          "fee",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "position": {
            "type": [
              "integer",
              "null"
            ]
          },
          "reference": {
            "type": [
              "string",
              "null"
            ]
          },
          "recipient": {
            "type": "string"
          },
          "amount": {
            "type": "string"
          },
          "fee": {
            "type": "string",
            "description": "The quoted per-item fee."
          },
          "status": {
            "type": "string",
            "description": "The item's status, or `released` when this authorization no longer holds it."
          }
        }
      },
      "PayrollIntentPayload": {
        "type": "object",
        "description": "One unsigned payload: the grouping fields in snake_case, then the chain fields as the adapter built them. `format` per family: solana_tx {transaction (base64), blockhash, last_valid_block_height, size_bytes, envelope_valid_for_ms}; evm_tx {transaction: {walletChain, to, data, value, gas_limit}, digest}; tron_tx; aptos_entry_function; sui_tx; ton_connect; stacks_contract_call; Bitcoin psbt_v0_base64 (confidential fee keys stripped).",
        "required": [
          "group_index",
          "item_ids",
          "format",
          "warnings"
        ],
        "properties": {
          "group_index": {
            "type": [
              "integer",
              "null"
            ]
          },
          "item_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "format": {
            "type": [
              "string",
              "null"
            ]
          },
          "wallet_chain": {
            "type": [
              "string",
              "integer"
            ],
            "description": "Solana: solana:mainnet | solana:devnet | solana:testnet."
          },
          "tx_ref": {
            "type": "string",
            "description": "Present only when the chain makes the hash predictable (not Solana)."
          },
          "envelope_expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this payload's envelope stops being valid (Solana about a minute)."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "additionalProperties": true
      },
      "PayrollIntentCancelPayload": {
        "type": "object",
        "description": "Bitcoin only - spends the reserved inputs back to the payer.",
        "properties": {
          "version": {
            "type": "integer"
          },
          "psbt_base64": {
            "type": "string"
          },
          "unsigned_txid": {
            "type": "string"
          },
          "fee_rate_sat_vb": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "WebhookEnabledEventsEntry": {
        "description": "A catalogue type, or \"*\" for every event.",
        "anyOf": [
          {
            "$ref": "#/components/schemas/WebhookEventTypeName"
          },
          {
            "const": "*"
          }
        ]
      },
      "CheckoutQrPayload": {
        "type": "object",
        "required": [
          "payload",
          "svgUrl"
        ],
        "additionalProperties": false,
        "properties": {
          "payload": {
            "type": "string",
            "description": "The text the QR encodes."
          },
          "svgUrl": {
            "type": "string",
            "format": "uri",
            "description": "The relay URL serving it as SVG."
          }
        }
      },
      "CheckoutIntentCommon": {
        "type": "object",
        "required": [
          "paymentRequestId",
          "network",
          "currency",
          "amount",
          "fee",
          "feeCurrency",
          "total",
          "expiresAt"
        ],
        "properties": {
          "paymentRequestId": {
            "type": "string"
          },
          "network": {
            "$ref": "#/components/schemas/CheckoutNetwork"
          },
          "currency": {
            "type": "string"
          },
          "amount": {
            "type": "string"
          },
          "fee": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CheckoutTokenAmount"
              }
            ],
            "description": "FastPay's protocol fee (the disburser's feeBps), in the payment's own token."
          },
          "feeCurrency": {
            "type": "string"
          },
          "total": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CheckoutTokenAmount"
              }
            ],
            "description": "amount + fee."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "The signed authorization's expiry (whole seconds, Sui: milliseconds): the sooner of the request's expiresAt and 15 min from now."
          }
        }
      },
      "WebhookEventTypeName": {
        "type": "string",
        "description": "One of the 31 catalogue types, in the catalogue's order. payment.in_process, payout_job.awaiting_approval and payout_job.approved have no sender.",
        "enum": [
          "payment.in_process",
          "payment.completed",
          "payment.failed",
          "payment.cancelled",
          "payment_request.paid",
          "payment_request.paid_after_expiry",
          "checkout_session.paid",
          "checkout_session.paid_after_expiry",
          "refund.confirmed",
          "refund.failed",
          "refund.expired",
          "payment_channel.deposit",
          "payout_job.awaiting_approval",
          "payout_job.approved",
          "payout_job.rejected",
          "payroll_batch.awaiting_approval",
          "payroll_batch.approved",
          "payroll_batch.rejected",
          "payroll_batch.authorization_expired",
          "payroll_batch.closed",
          "payroll_item.completed",
          "payroll_item.attempt_failed",
          "payroll_item.failed",
          "payroll_item.cancelled",
          "payroll_authorization.flagged",
          "payroll_authorization.fee_overcharge",
          "payroll_authorization.resolved",
          "payroll_payer_wallet.reverification_required",
          "payroll_authorization.fee_shortfall",
          "payroll_fee_settlement.paid",
          "payroll_item.released"
        ]
      },
      "CheckoutTokenAmount": {
        "type": "string",
        "pattern": "^\\d+(\\.\\d+)?$",
        "description": "An amount in the token's own units, formatted with the token's full decimals (e.g. \"1.500000\" USDC; toAmountString)."
      }
    },
    "examples": {
      "TenantsTeamMemberOwner": {
        "summary": "A maker (role owner)",
        "value": {
          "id": "mem_00000000000000000000000000000001",
          "object": "member",
          "name": "Example Maker",
          "email": "maker@example.com",
          "role": "owner",
          "status": "active",
          "capabilities": {
            "can_approve": true,
            "can_manage_team": true,
            "can_set_approval_policy": true
          },
          "created_at": "2026-10-24T09:05:00.000Z",
          "updated_at": "2026-10-24T09:05:00.000Z"
        }
      },
      "TenantsTeamErrorMissingApiKey": {
        "summary": "401 missing_api_key (Shape A)",
        "value": {
          "error_type": "authentication_error",
          "error_slug": "missing_api_key",
          "error": "Missing or malformed Authorization header - expected \"Bearer <api key>\".",
          "error_message": "Missing or malformed Authorization header - expected \"Bearer <api key>\".",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamErrorInsufficientScope": {
        "summary": "403 insufficient_scope (Shape A)",
        "value": {
          "error_type": "permission_error",
          "error_slug": "insufficient_scope",
          "error": "This API key lacks the team:write scope required for POST /v1/members.",
          "error_message": "This API key lacks the team:write scope required for POST /v1/members.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamErrorKeyHasNoMember": {
        "summary": "404 resource_missing for a memberless key (Shape B)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "resource_missing",
            "message": "This API key is not issued to a team member."
          },
          "error_type": "invalid_request_error",
          "error_slug": "resource_missing",
          "error_message": "This API key is not issued to a team member.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamErrorInternal": {
        "summary": "500 internal_error (Shape B, masked)",
        "value": {
          "error": {
            "type": "api_error",
            "code": "internal_error",
            "message": "Internal server error.",
            "param": null,
            "request_id": "req_00000000-0000-4000-8000-000000000000"
          },
          "error_type": "api_error",
          "error_slug": "internal_error",
          "error_message": "Internal server error.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamMemberCreateMaker": {
        "summary": "Add a maker",
        "value": {
          "name": "Example Maker",
          "email": "maker@example.com",
          "role": "owner"
        }
      },
      "TenantsTeamMemberCreateApprover": {
        "summary": "Add an approver",
        "value": {
          "name": "Example Approver",
          "email": "approver@example.com",
          "role": "approver"
        }
      },
      "TenantsTeamMemberApprover": {
        "summary": "An approver",
        "value": {
          "id": "mem_00000000000000000000000000000002",
          "object": "member",
          "name": "Example Approver",
          "email": "approver@example.com",
          "role": "approver",
          "status": "active",
          "capabilities": {
            "can_approve": true,
            "can_manage_team": false,
            "can_set_approval_policy": false
          },
          "created_at": "2026-10-24T09:06:00.000Z",
          "updated_at": "2026-10-24T09:06:00.000Z"
        }
      },
      "TenantsTeamErrorInvalidName": {
        "summary": "400 invalid_name (Shape B)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_name",
            "message": "name is required and must be at most 200 characters."
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_name",
          "error_message": "name is required and must be at most 200 characters.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamErrorEmailInUse": {
        "summary": "400 email_in_use (Shape B)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "email_in_use",
            "message": "A member with the email maker@example.com already exists for this tenant."
          },
          "error_type": "invalid_request_error",
          "error_slug": "email_in_use",
          "error_message": "A member with the email maker@example.com already exists for this tenant.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamErrorMemberRequired": {
        "summary": "403 member_required (Shape B)",
        "value": {
          "error": {
            "type": "permission_error",
            "code": "member_required",
            "message": "This API key is not issued to a team member with that capability. Ask an owner or admin (or the operator) to create a member and issue a key to it."
          },
          "error_type": "permission_error",
          "error_slug": "member_required",
          "error_message": "This API key is not issued to a team member with that capability. Ask an owner or admin (or the operator) to create a member and issue a key to it.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamErrorCapabilityRequired": {
        "summary": "403 capability_required (Shape B)",
        "value": {
          "error": {
            "type": "permission_error",
            "code": "capability_required",
            "message": "Role \"developer\" cannot do this."
          },
          "error_type": "permission_error",
          "error_slug": "capability_required",
          "error_message": "Role \"developer\" cannot do this.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamErrorOwnerRoleRequired": {
        "summary": "403 owner_role_required (Shape B)",
        "value": {
          "error": {
            "type": "permission_error",
            "code": "owner_role_required",
            "message": "Only an owner may create another owner."
          },
          "error_type": "permission_error",
          "error_slug": "owner_role_required",
          "error_message": "Only an owner may create another owner.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamErrorIdempotencyMismatch": {
        "summary": "409 idempotency_parameter_mismatch (Shape B, request_id inside error)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "idempotency_parameter_mismatch",
            "message": "Idempotency key parameter mismatch.",
            "request_id": "req_00000000-0000-4000-8000-000000000000"
          },
          "error_type": "invalid_request_error",
          "error_slug": "idempotency_parameter_mismatch",
          "error_message": "Idempotency key parameter mismatch.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamErrorIdempotencyUnopened": {
        "summary": "409 idempotency_key_in_use for an answer stored unsealed before the first 2026-10-28 deploy (Shape B, request_id inside error)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "idempotency_key_in_use",
            "message": "The first request under this Idempotency-Key already ran, and its stored answer cannot be replayed: look up its result before you send the request again under a new key.",
            "request_id": "req_00000000-0000-4000-8000-000000000000"
          },
          "error_type": "invalid_request_error",
          "error_slug": "idempotency_key_in_use",
          "error_message": "The first request under this Idempotency-Key already ran, and its stored answer cannot be replayed: look up its result before you send the request again under a new key.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamMemberList": {
        "summary": "One page of members",
        "value": {
          "object": "list",
          "data": [
            {
              "id": "mem_00000000000000000000000000000002",
              "object": "member",
              "name": "Example Approver",
              "email": "approver@example.com",
              "role": "approver",
              "status": "active",
              "capabilities": {
                "can_approve": true,
                "can_manage_team": false,
                "can_set_approval_policy": false
              },
              "created_at": "2026-10-24T09:06:00.000Z",
              "updated_at": "2026-10-24T09:06:00.000Z"
            },
            {
              "id": "mem_00000000000000000000000000000001",
              "object": "member",
              "name": "Example Maker",
              "email": "maker@example.com",
              "role": "owner",
              "status": "active",
              "capabilities": {
                "can_approve": true,
                "can_manage_team": true,
                "can_set_approval_policy": true
              },
              "created_at": "2026-10-24T09:05:00.000Z",
              "updated_at": "2026-10-24T09:05:00.000Z"
            }
          ],
          "has_more": false
        }
      },
      "TenantsTeamErrorInvalidPagination": {
        "summary": "400 invalid_pagination_parameter (Shape B)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_pagination_parameter",
            "message": "limit must be an integer between 1 and 100 (got \"0\")."
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_pagination_parameter",
          "error_message": "limit must be an integer between 1 and 100 (got \"0\").",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamErrorMemberMissing": {
        "summary": "404 resource_missing (Shape B)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "resource_missing",
            "message": "No such member."
          },
          "error_type": "invalid_request_error",
          "error_slug": "resource_missing",
          "error_message": "No such member.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamMemberUpdateSuspend": {
        "summary": "Suspend a member",
        "value": {
          "status": "suspended"
        }
      },
      "TenantsTeamMemberUpdateRole": {
        "summary": "Change a role",
        "value": {
          "role": "approver"
        }
      },
      "TenantsTeamMemberSuspended": {
        "summary": "A suspended approver",
        "value": {
          "id": "mem_00000000000000000000000000000002",
          "object": "member",
          "name": "Example Approver",
          "email": "approver@example.com",
          "role": "approver",
          "status": "suspended",
          "capabilities": {
            "can_approve": true,
            "can_manage_team": false,
            "can_set_approval_policy": false
          },
          "created_at": "2026-10-24T09:06:00.000Z",
          "updated_at": "2026-11-02T08:00:00.000Z"
        }
      },
      "TenantsTeamErrorLastOwner": {
        "summary": "409 last_owner (Shape B)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "last_owner",
            "message": "This is the tenant's last active owner - promote another member to owner first."
          },
          "error_type": "invalid_request_error",
          "error_slug": "last_owner",
          "error_message": "This is the tenant's last active owner - promote another member to owner first.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "TenantsTeamPolicyDefault": {
        "summary": "Never written (one-person client)",
        "value": {
          "object": "payout_approval_policy",
          "mode": "never",
          "approvals_required": 1,
          "thresholds": {}
        }
      },
      "TenantsTeamPolicyAlwaysOne": {
        "summary": "A maker and an approver",
        "value": {
          "object": "payout_approval_policy",
          "mode": "always",
          "approvals_required": 1,
          "thresholds": {}
        }
      },
      "TenantsTeamPolicyRequestAlwaysOne": {
        "value": {
          "mode": "always",
          "approvals_required": 1
        }
      },
      "TenantsTeamPolicyRequestNever": {
        "summary": "Lowering to never (admin key only)",
        "value": {
          "mode": "never"
        }
      },
      "TenantsTeamPolicyRequestAbove": {
        "summary": "Thresholds (admin key only; not a client choice)",
        "value": {
          "mode": "above",
          "approvals_required": 1,
          "thresholds": {
            "USDC": "5000"
          }
        }
      },
      "TenantsTeamErrorInvalidPolicyMode": {
        "summary": "400 invalid_policy_mode (Shape B)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_policy_mode",
            "message": "mode must be one of never, always, above."
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_policy_mode",
          "error_message": "mode must be one of never, always, above.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "SandboxSummaryExample": {
        "summary": "After a payroll rehearsal",
        "value": {
          "object": "sandbox",
          "tenant_id": "00000000-0000-4000-8000-000000000000",
          "counts": {
            "refunds": 0,
            "deposits": 0,
            "payments": 0,
            "checkout_sessions": 0,
            "payment_requests": 0,
            "payment_links": 0,
            "payment_channels": 0,
            "customers": 0,
            "payroll_tx_watch": 2,
            "payroll_authorizations": 1,
            "payroll_items": 2,
            "payroll_batches": 1,
            "payroll_payer_wallets": 1,
            "recipients": 0,
            "jobs": 0,
            "ledger_accounts": 0,
            "webhook_deliveries": 6
          },
          "mode": "test"
        }
      },
      "ErrInvalidJsonExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_json",
            "message": "Request body is not valid JSON.",
            "param": null,
            "request_id": null
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_json",
          "error_message": "Request body is not valid JSON.",
          "request_id": null
        }
      },
      "ErrInsufficientScopeSandboxReadExample": {
        "value": {
          "error_type": "permission_error",
          "error_slug": "insufficient_scope",
          "error": "This API key lacks the sandbox:read scope required for GET /v1/sandbox.",
          "error_message": "This API key lacks the sandbox:read scope required for GET /v1/sandbox.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ErrIpNotAllowedExample": {
        "value": {
          "error_type": "permission_error",
          "error_slug": "ip_not_allowed",
          "error": "This API key may not be used from this IP address.",
          "error_message": "This API key may not be used from this IP address.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "SandboxResetExample": {
        "value": {
          "object": "sandbox",
          "tenant_id": "00000000-0000-4000-8000-000000000000",
          "counts": {
            "refunds": 0,
            "deposits": 0,
            "payments": 0,
            "checkout_sessions": 0,
            "payment_requests": 0,
            "payment_links": 0,
            "payment_channels": 0,
            "customers": 0,
            "payroll_tx_watch": 0,
            "payroll_authorizations": 0,
            "payroll_items": 0,
            "payroll_batches": 0,
            "payroll_payer_wallets": 0,
            "recipients": 0,
            "jobs": 0,
            "ledger_accounts": 0,
            "webhook_deliveries": 0
          },
          "deleted": {
            "refunds": 0,
            "deposits": 0,
            "payments": 0,
            "checkout_sessions": 0,
            "payment_requests": 0,
            "payment_links": 0,
            "payment_channels": 0,
            "customers": 0,
            "payroll_tx_watch": 2,
            "payroll_authorizations": 1,
            "payroll_items": 2,
            "payroll_batches": 1,
            "payroll_payer_wallets": 1,
            "recipients": 0,
            "jobs": 0,
            "ledger_accounts": 0,
            "webhook_deliveries": 6
          }
        }
      },
      "ErrSandboxKeyRequiredExample": {
        "value": {
          "error": {
            "type": "permission_error",
            "code": "sandbox_key_required",
            "message": "This route acts on the sandbox - call it with an fp_test_ API key."
          },
          "error_type": "permission_error",
          "error_slug": "sandbox_key_required",
          "error_message": "This route acts on the sandbox - call it with an fp_test_ API key.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ErrInsufficientScopeSandboxExample": {
        "value": {
          "error_type": "permission_error",
          "error_slug": "insufficient_scope",
          "error": "This API key lacks the sandbox:write scope required for POST /v1/sandbox/reset.",
          "error_message": "This API key lacks the sandbox:write scope required for POST /v1/sandbox/reset.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ErrIdempotencyMismatchExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "idempotency_parameter_mismatch",
            "message": "Idempotency key parameter mismatch.",
            "request_id": "req_00000000-0000-4000-8000-000000000000"
          },
          "error_type": "invalid_request_error",
          "error_slug": "idempotency_parameter_mismatch",
          "error_message": "Idempotency key parameter mismatch.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ApiKeysErrIdempotencyUnopenedExample": {
        "summary": "A repeat under a key whose stored answer does not open (for example stored unsealed before the 2026-10-28 deploy)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "idempotency_key_in_use",
            "message": "The first request under this Idempotency-Key already ran, and its stored answer cannot be replayed: look up its result before you send the request again under a new key.",
            "request_id": "req_00000000-0000-4000-8000-000000000000"
          },
          "error_type": "invalid_request_error",
          "error_slug": "idempotency_key_in_use",
          "error_message": "The first request under this Idempotency-Key already ran, and its stored answer cannot be replayed: look up its result before you send the request again under a new key.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ApiKeyMemberMintRequestExample": {
        "summary": "An owner's sandbox key mints a developer's sandbox key with the role default scopes and a 30-day life",
        "value": {
          "test": true,
          "name": "example-dev-sandbox",
          "expires_in_days": 30
        }
      },
      "ApiKeyMemberMinted201Example": {
        "summary": "The member route's 201",
        "value": {
          "id": "00000000-0000-4000-8000-0000000000b2",
          "object": "api_key",
          "prefix": "fp_test_",
          "name": "example-dev-sandbox",
          "scopes": [
            "*"
          ],
          "status": "active",
          "is_test": true,
          "expires_at": "2026-11-27T09:00:00.000Z",
          "ip_allowlist": null,
          "rotated_from_key_id": null,
          "member_id": "mem_00000000000000000000000000000003",
          "payroll_networks": null,
          "created_at": "2026-10-28T09:00:00.000Z",
          "revoked_at": null,
          "last_used_at": null,
          "apiKey": "fp_test_EXAMPLE00000000000000000000000000000000000000000",
          "warning": "This key is shown once and cannot be retrieved again - hand it to the member now."
        }
      },
      "ErrScopesExceedRoleExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "scopes_exceed_role",
            "message": "scopes [\"payroll:write\"] exceed what the approver role allows (checkout:read, payouts:read, refunds:read, customers:read, balance:read, webhooks:read, integrations:read, team:read, sandbox:read, payroll:read)."
          },
          "error_type": "invalid_request_error",
          "error_slug": "scopes_exceed_role",
          "error_message": "scopes [\"payroll:write\"] exceed what the approver role allows (checkout:read, payouts:read, refunds:read, customers:read, balance:read, webhooks:read, integrations:read, team:read, sandbox:read, payroll:read).",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ErrUnsupportedNetworkExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "unsupported_network",
            "message": "payroll_networks entries must be payroll networks: ethereum, base, polygon, arbitrum, bnb, avalanche, robinhood, linea, optimism, monad, megaeth, hyperevm, tron, solana, aptos, sui, ton, stacks, bitcoin.",
            "details": {
              "networks": [
                "ethereum",
                "base",
                "polygon",
                "arbitrum",
                "bnb",
                "avalanche",
                "robinhood",
                "linea",
                "optimism",
                "monad",
                "megaeth",
                "hyperevm",
                "tron",
                "solana",
                "aptos",
                "sui",
                "ton",
                "stacks",
                "bitcoin"
              ]
            }
          },
          "error_type": "invalid_request_error",
          "error_slug": "unsupported_network",
          "error_message": "payroll_networks entries must be payroll networks: ethereum, base, polygon, arbitrum, bnb, avalanche, robinhood, linea, optimism, monad, megaeth, hyperevm, tron, solana, aptos, sui, ton, stacks, bitcoin.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ApiKeysErrInvalidTestExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_test",
            "message": "test must be a JSON boolean (true or false), not a string or a number."
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_test",
          "error_message": "test must be a JSON boolean (true or false), not a string or a number.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ApiKeysErrInvalidIpAllowlistExample": {
        "summary": "An entry with an empty prefix (accepted as /0 before 2026-10-28)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_ip_allowlist",
            "message": "ip_allowlist entry \"203.0.113.7/\" is not a valid IP address or CIDR range: send an address, or address/prefix with a prefix of 1-32 (IPv4) or 1-128 (IPv6) in digits."
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_ip_allowlist",
          "error_message": "ip_allowlist entry \"203.0.113.7/\" is not a valid IP address or CIDR range: send an address, or address/prefix with a prefix of 1-32 (IPv4) or 1-128 (IPv6) in digits.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ErrCapabilityRequiredExample": {
        "value": {
          "error": {
            "type": "permission_error",
            "code": "capability_required",
            "message": "Role \"developer\" cannot do this."
          },
          "error_type": "permission_error",
          "error_slug": "capability_required",
          "error_message": "Role \"developer\" cannot do this.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ApiKeysErrLiveKeyRequiredExample": {
        "summary": "A sandbox key asking POST /v1/members/{id}/api-keys for a live key (test false)",
        "value": {
          "error": {
            "type": "permission_error",
            "code": "live_key_required",
            "message": "This action changes live data: use a live (fp_live_) API key."
          },
          "error_type": "permission_error",
          "error_slug": "live_key_required",
          "error_message": "This action changes live data: use a live (fp_live_) API key.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ErrNoMemberTenantRouteExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "resource_missing",
            "message": "No such member."
          },
          "error_type": "invalid_request_error",
          "error_slug": "resource_missing",
          "error_message": "No such member.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ErrMemberSuspendedMintExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "member_suspended",
            "message": "Cannot issue a key to a suspended member."
          },
          "error_type": "invalid_request_error",
          "error_slug": "member_suspended",
          "error_message": "Cannot issue a key to a suspended member.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ApiKeyList200Example": {
        "summary": "A page with a live key and a revoked sandbox key",
        "value": {
          "object": "list",
          "has_more": false,
          "data": [
            {
              "id": "00000000-0000-4000-8000-0000000000a1",
              "object": "api_key",
              "prefix": "fp_live_",
              "name": "example-maker-payroll-live-1",
              "scopes": [
                "payroll:write"
              ],
              "status": "active",
              "is_test": false,
              "expires_at": "2027-01-26T09:00:00.000Z",
              "ip_allowlist": null,
              "rotated_from_key_id": null,
              "member_id": "mem_00000000000000000000000000000001",
              "payroll_networks": [
                "solana"
              ],
              "created_at": "2026-10-28T09:00:00.000Z",
              "revoked_at": null,
              "last_used_at": "2026-10-28T09:01:00.000Z"
            },
            {
              "id": "00000000-0000-4000-8000-0000000000a0",
              "object": "api_key",
              "prefix": "fp_test_",
              "name": "example-maker-payroll-test-1",
              "scopes": [
                "payroll:write"
              ],
              "status": "revoked",
              "is_test": true,
              "expires_at": "2026-11-20T09:00:00.000Z",
              "ip_allowlist": null,
              "rotated_from_key_id": null,
              "member_id": "mem_00000000000000000000000000000001",
              "payroll_networks": [
                "solana"
              ],
              "created_at": "2026-10-21T09:00:00.000Z",
              "revoked_at": "2026-10-30T09:00:00.000Z",
              "last_used_at": "2026-10-22T10:00:00.000Z"
            }
          ]
        }
      },
      "ErrInvalidPaginationExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_pagination_parameter",
            "message": "limit must be an integer between 1 and 100 (got \"500\")."
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_pagination_parameter",
          "error_message": "limit must be an integer between 1 and 100 (got \"500\").",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ApiKeyIntrospection200Example": {
        "summary": "A live Solana maker key introspecting itself",
        "value": {
          "id": "00000000-0000-4000-8000-0000000000a1",
          "object": "api_key",
          "prefix": "fp_live_",
          "name": "example-maker-payroll-live-1",
          "scopes": [
            "payroll:write"
          ],
          "status": "active",
          "is_test": false,
          "expires_at": "2027-01-26T09:00:00.000Z",
          "ip_allowlist": null,
          "rotated_from_key_id": null,
          "member_id": "mem_00000000000000000000000000000001",
          "payroll_networks": [
            "solana"
          ],
          "created_at": "2026-10-28T09:00:00.000Z",
          "revoked_at": null,
          "last_used_at": "2026-10-28T09:00:05.000Z",
          "member": {
            "id": "mem_00000000000000000000000000000001",
            "name": "Example Maker",
            "role": "owner",
            "effective_scopes": [
              "payroll:write"
            ]
          }
        }
      },
      "ApiKeyRotateRequestExample": {
        "summary": "Keep the old key one more hour",
        "value": {
          "grace_hours": 1
        }
      },
      "ApiKeyRotated201Example": {
        "summary": "A rotation's successor - note expires_at null (never expires) and the shared name",
        "value": {
          "id": "00000000-0000-4000-8000-0000000000c3",
          "object": "api_key",
          "prefix": "fp_test_",
          "name": "example-integration",
          "scopes": [
            "*"
          ],
          "status": "active",
          "is_test": true,
          "expires_at": null,
          "ip_allowlist": null,
          "rotated_from_key_id": "00000000-0000-4000-8000-0000000000c2",
          "member_id": null,
          "payroll_networks": null,
          "created_at": "2026-10-28T09:00:00.000Z",
          "revoked_at": null,
          "last_used_at": null,
          "apiKey": "fp_test_EXAMPLE00000000000000000000000000000000000000000",
          "previous_key_id": "00000000-0000-4000-8000-0000000000c2",
          "previous_key_expires_at": "2026-10-28T10:00:00.000Z",
          "warning": "This key is shown once and cannot be retrieved again - store it now."
        }
      },
      "ErrInvalidGraceHoursExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_grace_hours",
            "message": "grace_hours must be a number between 0 and 168 (0 revokes the previous key immediately)."
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_grace_hours",
          "error_message": "grace_hours must be a number between 0 and 168 (0 revokes the previous key immediately).",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ErrInsufficientScopeRotateExample": {
        "value": {
          "error_type": "permission_error",
          "error_slug": "insufficient_scope",
          "error": "Only a full-scope key (scopes: [\"*\"]) may rotate itself; ask your operator to rotate a restricted key.",
          "error_message": "Only a full-scope key (scopes: [\"*\"]) may rotate itself; ask your operator to rotate a restricted key.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ErrApiKeyNotRotatableExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "api_key_not_rotatable",
            "message": "This key is no longer active."
          },
          "error_type": "invalid_request_error",
          "error_slug": "api_key_not_rotatable",
          "error_message": "This key is no longer active.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "PayerWalletRegisteredSolanaLive": {
        "summary": "201 - live Solana wallet registered",
        "value": {
          "id": "pyw_00000000000000000000000000000001",
          "object": "payroll_payer_wallet",
          "network": "solana",
          "chain_ref": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
          "address": "EXAMPLEPayerWa11et1111111111111111111111111",
          "address_kind": null,
          "proof_timing": "challenge_first",
          "status": "pending_verification",
          "verification_method": null,
          "verified_at": null,
          "revoked_at": null,
          "reverification_reason": null,
          "label": "Treasury payer",
          "challenge": "FastPay payroll payer wallet registration\nTenant: 00000000-0000-4000-8000-000000000000\nNetwork: solana (chain solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d)\nAddress: EXAMPLEPayerWa11et1111111111111111111111111\nNonce: 00000000000000000000000000000000\nExpires: 2026-10-24T09:15:00.000Z\nSigning proves control of this wallet. It authorizes no payment.",
          "challenge_expires_at": "2026-10-24T09:15:00.000Z",
          "created_by_member_id": "mem_00000000000000000000000000000001",
          "verified_by_member_id": null,
          "is_test": false,
          "created_at": "2026-10-24T09:00:00.000Z",
          "updated_at": "2026-10-24T09:00:00.000Z",
          "challenge_format": "solana_sign_message",
          "challenge_extra": {
            "encoding": "utf8",
            "envelopes": [
              "raw",
              "solana_offchain_v0"
            ]
          }
        }
      },
      "PayerWalletRegisteredSolanaSandbox": {
        "summary": "201 - sandbox (fp_test_) Solana wallet registered",
        "value": {
          "id": "pyw_00000000000000000000000000000002",
          "object": "payroll_payer_wallet",
          "network": "solana",
          "chain_ref": "mock:solana",
          "address": "EXAMPLESandboxWa11et111111111111111111111111",
          "address_kind": null,
          "proof_timing": "challenge_first",
          "status": "pending_verification",
          "verification_method": null,
          "verified_at": null,
          "revoked_at": null,
          "reverification_reason": null,
          "label": null,
          "challenge": "FastPay payroll payer wallet registration\nTenant: 00000000-0000-4000-8000-000000000000\nNetwork: solana (chain mock:solana)\nAddress: EXAMPLESandboxWa11et111111111111111111111111\nNonce: 00000000000000000000000000000000\nExpires: 2026-10-24T09:15:00.000Z\nSigning proves control of this wallet. It authorizes no payment.",
          "challenge_expires_at": "2026-10-24T09:15:00.000Z",
          "created_by_member_id": "mem_00000000000000000000000000000001",
          "verified_by_member_id": null,
          "is_test": true,
          "created_at": "2026-10-24T09:00:00.000Z",
          "updated_at": "2026-10-24T09:00:00.000Z",
          "challenge_format": "solana_sign_message",
          "challenge_extra": {}
        }
      },
      "UnsupportedNetworkError": {
        "summary": "400 - unknown network",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "unsupported_network",
            "message": "network must be one of the payroll networks: ethereum, base, polygon, arbitrum, bnb, avalanche, robinhood, linea, optimism, monad, megaeth, hyperevm, tron, solana, aptos, sui, ton, stacks, bitcoin.",
            "details": {
              "networks": [
                "ethereum",
                "base",
                "polygon",
                "arbitrum",
                "bnb",
                "avalanche",
                "robinhood",
                "linea",
                "optimism",
                "monad",
                "megaeth",
                "hyperevm",
                "tron",
                "solana",
                "aptos",
                "sui",
                "ton",
                "stacks",
                "bitcoin"
              ]
            }
          },
          "error_type": "invalid_request_error",
          "error_slug": "unsupported_network",
          "error_message": "network must be one of the payroll networks: ethereum, base, polygon, arbitrum, bnb, avalanche, robinhood, linea, optimism, monad, megaeth, hyperevm, tron, solana, aptos, sui, ton, stacks, bitcoin.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ApiKeyNetworkNotAllowedError": {
        "summary": "403 - a Solana-only key on Tron",
        "value": {
          "error": {
            "type": "permission_error",
            "code": "api_key_network_not_allowed",
            "message": "This API key may act on payroll for solana only, not tron.",
            "details": {
              "network": "tron",
              "key_networks": [
                "solana"
              ]
            }
          },
          "error_type": "permission_error",
          "error_slug": "api_key_network_not_allowed",
          "error_message": "This API key may act on payroll for solana only, not tron.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "PayrollNetworkNotEnabledTenantError": {
        "summary": "503 - the tenant's lane is not enabled (or outside the ceiling)",
        "value": {
          "error": {
            "type": "api_error",
            "code": "payroll_network_not_enabled",
            "message": "Client-signed payroll on solana is not open for this account's live keys.",
            "details": {
              "network": "solana"
            }
          },
          "error_type": "api_error",
          "error_slug": "payroll_network_not_enabled",
          "error_message": "Client-signed payroll on solana is not open for this account's live keys.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "PayerWalletVerifiedSolanaLive": {
        "summary": "A verified live Solana wallet",
        "value": {
          "id": "pyw_00000000000000000000000000000001",
          "object": "payroll_payer_wallet",
          "network": "solana",
          "chain_ref": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
          "address": "EXAMPLEPayerWa11et1111111111111111111111111",
          "address_kind": "solana_ed25519",
          "proof_timing": "challenge_first",
          "status": "verified",
          "verification_method": "solana_sign_message",
          "verified_at": "2026-10-24T09:05:00.000Z",
          "revoked_at": null,
          "reverification_reason": null,
          "label": "Treasury payer",
          "challenge": null,
          "challenge_expires_at": null,
          "created_by_member_id": "mem_00000000000000000000000000000001",
          "verified_by_member_id": "mem_00000000000000000000000000000001",
          "is_test": false,
          "created_at": "2026-10-24T09:00:00.000Z",
          "updated_at": "2026-10-24T09:05:00.000Z"
        }
      },
      "PayerWalletRevokedSolanaLive": {
        "summary": "200 - revoked, one authorization still executable until its expiry",
        "value": {
          "id": "pyw_00000000000000000000000000000001",
          "object": "payroll_payer_wallet",
          "network": "solana",
          "chain_ref": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
          "address": "EXAMPLEPayerWa11et1111111111111111111111111",
          "address_kind": "solana_ed25519",
          "proof_timing": "challenge_first",
          "status": "revoked",
          "verification_method": "solana_sign_message",
          "verified_at": "2026-10-24T09:05:00.000Z",
          "revoked_at": "2026-11-02T14:00:00.000Z",
          "reverification_reason": null,
          "label": "Treasury payer",
          "challenge": null,
          "challenge_expires_at": null,
          "created_by_member_id": "mem_00000000000000000000000000000001",
          "verified_by_member_id": "mem_00000000000000000000000000000001",
          "is_test": false,
          "created_at": "2026-10-24T09:00:00.000Z",
          "updated_at": "2026-11-02T14:00:00.000Z",
          "live_authorizations": 1
        }
      },
      "PayrollBatchOpenExample": {
        "summary": "A batch created open (policy never)",
        "value": {
          "id": "pyb_0123456789abcdef0123456789abcdef",
          "object": "payroll_batch",
          "status": "open",
          "network": "solana",
          "chain_ref": "solana:EXAMPLEGENESIS",
          "currency": "USDC",
          "token_address": "Examp1eMint1111111111111111111111111111111",
          "payer_wallet_id": "pyw_0123456789abcdef0123456789abcdef",
          "payer_address": "Examp1ePayerWa11et111111111111111111111111",
          "item_count": 2,
          "total_amount": "2480.5",
          "approvals_required": 0,
          "approvals": [],
          "expires_at": "2026-10-31T09:00:00.000Z",
          "cancel_requested_at": null,
          "closed_at": null,
          "counts": {
            "pending": 2,
            "authorized": 0,
            "completed": 0,
            "failed": 0,
            "cancelled": 0
          },
          "reference": "example-payroll-2026-10",
          "callback_url": null,
          "is_test": false,
          "created_at": "2026-10-24T09:00:00.000Z",
          "updated_at": "2026-10-24T09:00:00.000Z"
        }
      },
      "PayrollBatchAwaitingExample": {
        "summary": "A batch waiting for one approval (policy always/1)",
        "value": {
          "id": "pyb_0123456789abcdef0123456789abcdef",
          "object": "payroll_batch",
          "status": "awaiting_approval",
          "network": "solana",
          "chain_ref": "solana:EXAMPLEGENESIS",
          "currency": "USDC",
          "token_address": "Examp1eMint1111111111111111111111111111111",
          "payer_wallet_id": "pyw_0123456789abcdef0123456789abcdef",
          "payer_address": "Examp1ePayerWa11et111111111111111111111111",
          "item_count": 2,
          "total_amount": "2480.5",
          "approvals_required": 1,
          "approvals": [],
          "expires_at": "2026-10-31T09:00:00.000Z",
          "cancel_requested_at": null,
          "closed_at": null,
          "counts": {
            "pending": 2,
            "authorized": 0,
            "completed": 0,
            "failed": 0,
            "cancelled": 0
          },
          "reference": "example-payroll-2026-10",
          "callback_url": null,
          "is_test": true,
          "created_at": "2026-10-24T09:00:00.000Z",
          "updated_at": "2026-10-24T09:00:00.000Z"
        }
      },
      "PayrollBatchApprovedExample": {
        "summary": "The approve that reached the quorum",
        "value": {
          "id": "pyb_0123456789abcdef0123456789abcdef",
          "object": "payroll_batch",
          "status": "open",
          "network": "solana",
          "chain_ref": "solana:EXAMPLEGENESIS",
          "currency": "USDC",
          "token_address": "Examp1eMint1111111111111111111111111111111",
          "payer_wallet_id": "pyw_0123456789abcdef0123456789abcdef",
          "payer_address": "Examp1ePayerWa11et111111111111111111111111",
          "item_count": 2,
          "total_amount": "2480.5",
          "approvals_required": 1,
          "approvals": [
            {
              "member_id": "mem_example_approver",
              "member_name": "Example Approver",
              "decision": "approve",
              "reason": null,
              "created_at": "2026-10-24T09:05:00.000Z"
            }
          ],
          "expires_at": "2026-10-31T09:00:00.000Z",
          "cancel_requested_at": null,
          "closed_at": null,
          "counts": {
            "pending": 2,
            "authorized": 0,
            "completed": 0,
            "failed": 0,
            "cancelled": 0
          },
          "reference": "example-payroll-2026-10",
          "callback_url": null,
          "is_test": true,
          "created_at": "2026-10-24T09:00:00.000Z",
          "updated_at": "2026-10-24T09:05:00.000Z"
        }
      },
      "PayrollBatchClosingExample": {
        "summary": "A cancel with an authorization still out",
        "value": {
          "id": "pyb_0123456789abcdef0123456789abcdef",
          "object": "payroll_batch",
          "status": "closing",
          "network": "solana",
          "chain_ref": "solana:EXAMPLEGENESIS",
          "currency": "USDC",
          "token_address": "Examp1eMint1111111111111111111111111111111",
          "payer_wallet_id": "pyw_0123456789abcdef0123456789abcdef",
          "payer_address": "Examp1ePayerWa11et111111111111111111111111",
          "item_count": 2,
          "total_amount": "2480.5",
          "approvals_required": 0,
          "approvals": [],
          "expires_at": "2026-10-31T09:00:00.000Z",
          "cancel_requested_at": "2026-10-24T09:20:00.000Z",
          "closed_at": null,
          "counts": {
            "pending": 0,
            "authorized": 2,
            "completed": 0,
            "failed": 0,
            "cancelled": 0
          },
          "reference": "example-payroll-2026-10",
          "callback_url": null,
          "is_test": false,
          "created_at": "2026-10-24T09:00:00.000Z",
          "updated_at": "2026-10-24T09:20:00.000Z",
          "closes_at": "2026-10-24T10:10:00.000Z",
          "note": "FastPay co-signs nothing new for this batch. Authorizations already issued cannot be revoked: they stay executable by the payer wallet until they expire (closes_at), and anything they pay before then is recorded as completed. The remaining items are then cancelled or failed."
        }
      },
      "PayrollIntentSolanaExample": {
        "summary": "A Solana single-mode intent of two items (fake values)",
        "value": {
          "id": "pyx_0123456789abcdef0123456789abcdef",
          "object": "payroll_intent",
          "batch_id": "pyb_0123456789abcdef0123456789abcdef",
          "network": "solana",
          "chain_ref": "solana:EXAMPLEGENESIS",
          "currency": "USDC",
          "fee_bps": 50,
          "expires_at": "2026-10-24T10:10:00.000Z",
          "payer": "Examp1ePayerWa11et111111111111111111111111",
          "token": "Examp1eMint1111111111111111111111111111111",
          "deployment": {
            "key": "solana:EXAMPLEPROGRAM",
            "disburser": "Examp1eDisburserProgram111111111111111111"
          },
          "pre_steps": [],
          "transactions": [
            {
              "authorization_id": "pya_0123456789abcdef0123456789abcdef",
              "chunk_index": 0,
              "status": "live",
              "auth_scheme": "per_entry",
              "entry_match": "independent",
              "expires_at": "2026-10-24T10:10:00.000Z",
              "groups": [
                {
                  "group_index": 0,
                  "item_ids": [
                    "pyi_00000000000000000000000000000001"
                  ],
                  "digest": null,
                  "entry_digests": null
                },
                {
                  "group_index": 1,
                  "item_ids": [
                    "pyi_00000000000000000000000000000002"
                  ],
                  "digest": null,
                  "entry_digests": null
                }
              ],
              "items": [
                {
                  "id": "pyi_00000000000000000000000000000001",
                  "position": 0,
                  "reference": "example-2026-10-sol-emp-001",
                  "recipient": "Examp1eRecipientA11111111111111111111111111",
                  "amount": "1500",
                  "fee": "7.5",
                  "status": "authorized"
                },
                {
                  "id": "pyi_00000000000000000000000000000002",
                  "position": 1,
                  "reference": "example-2026-10-sol-emp-002",
                  "recipient": "Examp1eRecipientB11111111111111111111111111",
                  "amount": "980.5",
                  "fee": "4.9025",
                  "status": "authorized"
                }
              ],
              "totals": {
                "amount": "2480.5",
                "fee": "12.4025",
                "total": "2492.9025"
              },
              "payloads": [
                {
                  "group_index": 0,
                  "item_ids": [
                    "pyi_00000000000000000000000000000001"
                  ],
                  "format": "solana_tx",
                  "wallet_chain": "solana:mainnet",
                  "envelope_expires_at": "2026-10-24T09:11:00.000Z",
                  "transaction": "AQAAAEXAMPLEBASE64==",
                  "blockhash": "Examp1eB1ockhash111111111111111111111111111",
                  "last_valid_block_height": 1000,
                  "size_bytes": 900,
                  "envelope_valid_for_ms": 60000,
                  "warnings": []
                },
                {
                  "group_index": 1,
                  "item_ids": [
                    "pyi_00000000000000000000000000000002"
                  ],
                  "format": "solana_tx",
                  "wallet_chain": "solana:mainnet",
                  "envelope_expires_at": "2026-10-24T09:11:00.000Z",
                  "transaction": "AQAAAEXAMPLEBASE64==",
                  "blockhash": "Examp1eB1ockhash111111111111111111111111111",
                  "last_valid_block_height": 1000,
                  "size_bytes": 900,
                  "envelope_valid_for_ms": 60000,
                  "warnings": []
                }
              ],
              "envelope_expires_at": "2026-10-24T09:11:00.000Z"
            }
          ],
          "totals": {
            "amount": "2480.5",
            "fee": "12.4025",
            "total": "2492.9025"
          },
          "warnings": [],
          "note": "FastPay co-signed these authorizations with its fund-free payment authorizer and holds no key over the payer wallet. Each payload can be executed only by the payer wallet, only until expires_at, and FastPay cannot revoke a signature it handed out. Review every recipient and amount before signing.",
          "is_test": false,
          "created_at": "2026-10-24T09:10:00.000Z",
          "balances": [],
          "payer_balance": "5000000000"
        }
      },
      "PayrollConfirmationAppliedExample": {
        "value": {
          "object": "payroll_confirmation",
          "batch_id": "pyb_0123456789abcdef0123456789abcdef",
          "report_kind": "tx_hash",
          "tx_hash": "5Examp1eSo1anaSignature1111111111111111111111111111111111111111111111111111111111111111",
          "status": "applied",
          "confirmations": null,
          "required_confirmations": null,
          "authorization_ids": [
            "pya_0123456789abcdef0123456789abcdef"
          ],
          "transactions": [
            {
              "tx_ref": "5Examp1eSo1anaSignature1111111111111111111111111111111111111111111111111111111111111111",
              "status": "applied"
            }
          ],
          "items": [
            {
              "id": "pyi_00000000000000000000000000000001",
              "status": "completed"
            },
            {
              "id": "pyi_00000000000000000000000000000002",
              "status": "authorized"
            }
          ],
          "is_test": false
        }
      },
      "WebhookDeliveryListExample": {
        "value": {
          "object": "list",
          "data": [
            {
              "id": "4182",
              "object": "webhook_delivery",
              "event_id": "00000000-0000-4000-8000-000000000000",
              "allocation_id": "pyi_00000000000000000000000000000001",
              "job_id": "pyb_00000000000000000000000000000001",
              "action": "callback",
              "status": "completed",
              "callback_url": "https://hooks.example.com/fastpay/webhooks",
              "attempts": 0,
              "last_error": null,
              "response_status": 200,
              "response_body": "{\"ok\":true}",
              "webhook_endpoint_id": "we_00000000000000000000000000000001",
              "is_test": true,
              "delivered": true,
              "delivered_at": "2026-10-28T09:05:01.000Z",
              "created_at": "2026-10-28T09:05:00.000Z",
              "updated_at": "2026-10-28T09:05:01.000Z"
            }
          ],
          "has_more": false
        }
      },
      "ErrInternalExample": {
        "value": {
          "error": {
            "type": "api_error",
            "code": "internal_error",
            "message": "Internal server error.",
            "param": null,
            "request_id": "req_00000000-0000-4000-8000-000000000000"
          },
          "error_type": "api_error",
          "error_slug": "internal_error",
          "error_message": "Internal server error.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "WebhookDeliveryWithPayloadExample": {
        "value": {
          "id": "4182",
          "object": "webhook_delivery",
          "event_id": "00000000-0000-4000-8000-000000000000",
          "allocation_id": "pyi_00000000000000000000000000000001",
          "job_id": "pyb_00000000000000000000000000000001",
          "action": "callback",
          "status": "completed",
          "callback_url": "https://hooks.example.com/fastpay/webhooks",
          "attempts": 0,
          "last_error": null,
          "response_status": 200,
          "response_body": "{\"ok\":true}",
          "webhook_endpoint_id": "we_00000000000000000000000000000001",
          "is_test": true,
          "delivered": true,
          "delivered_at": "2026-10-28T09:05:01.000Z",
          "created_at": "2026-10-28T09:05:00.000Z",
          "updated_at": "2026-10-28T09:05:01.000Z",
          "payload": {
            "id": "evt_00000000000000000000000000000001",
            "type": "payroll_item.completed",
            "action": "callback",
            "status": "completed",
            "item_id": "pyi_00000000000000000000000000000001",
            "batch_id": "pyb_00000000000000000000000000000001",
            "reference": "EXAMPLE-0001",
            "network": "solana",
            "currency": "USDC",
            "recipient": "ExampleRecipientAddress1111111111111111111111",
            "amount": "1250.00",
            "fee": "0.50",
            "payer": "ExamplePayerAddress11111111111111111111111111",
            "allocation_id": "00000000000000000000000000000001",
            "tx_hash": "ExampleTxSignature",
            "chain_position": 123456789,
            "attempt_count": 1,
            "failure_reason": null,
            "is_test": true,
            "event_id": "00000000-0000-4000-8000-000000000000"
          }
        }
      },
      "WebhookDeliveryMissingExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "resource_missing",
            "message": "No such webhook delivery."
          },
          "error_type": "invalid_request_error",
          "error_slug": "resource_missing",
          "error_message": "No such webhook delivery.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "WebhookRedeliveryInProgressExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "webhook_delivery_redelivery_in_progress",
            "message": "A delivery of this webhook is already in flight (its first attempt, a redelivery, or the periodic sweep); retry shortly."
          },
          "error_type": "invalid_request_error",
          "error_slug": "webhook_delivery_redelivery_in_progress",
          "error_message": "A delivery of this webhook is already in flight (its first attempt, a redelivery, or the periodic sweep); retry shortly.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "WebhookEgressIpsUnsetExample": {
        "value": {
          "object": "list",
          "data": [],
          "has_more": false,
          "configured": false
        }
      },
      "WebhookEgressIpsSetExample": {
        "value": {
          "object": "list",
          "data": [
            "192.0.2.10",
            "2001:db8::10"
          ],
          "has_more": false,
          "configured": true
        }
      },
      "WebhookSecretRotationExample": {
        "value": {
          "object": "webhook_secret",
          "secret": "whsec_222222222222222222222222222222222222222222222222",
          "previous_secret_expires_at": "2026-10-29T09:00:00.000Z"
        }
      },
      "WebhookInvalidExpiresInHoursExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_expires_in_hours",
            "message": "expires_in_hours must be a number between 0 and 72 (0 invalidates the previous secret immediately)."
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_expires_in_hours",
          "error_message": "expires_in_hours must be a number between 0 and 72 (0 invalidates the previous secret immediately).",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "WebhookLiveKeyRequiredExample": {
        "value": {
          "error": {
            "type": "permission_error",
            "code": "live_key_required",
            "message": "This action changes live data: use a live (fp_live_) API key."
          },
          "error_type": "permission_error",
          "error_slug": "live_key_required",
          "error_message": "This action changes live data: use a live (fp_live_) API key.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "WebhooksInsufficientScopeExample": {
        "value": {
          "error_type": "permission_error",
          "error_slug": "insufficient_scope",
          "error": "This API key lacks the webhooks:write scope required for POST /v1/webhook-endpoints.",
          "error_message": "This API key lacks the webhooks:write scope required for POST /v1/webhook-endpoints.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "WebhookEndpointWithSecretExample": {
        "summary": "Created, secret shown once",
        "value": {
          "id": "we_00000000000000000000000000000001",
          "object": "webhook_endpoint",
          "url": "https://hooks.example.com/fastpay/webhooks",
          "description": "Payroll",
          "enabled_events": [
            "payroll_batch.closed",
            "payroll_item.completed",
            "payroll_item.released"
          ],
          "status": "enabled",
          "is_test": true,
          "previous_secret_expires_at": null,
          "created_at": "2026-10-28T09:00:00.000Z",
          "updated_at": "2026-10-28T09:00:00.000Z",
          "secret": "whsec_000000000000000000000000000000000000000000000000"
        }
      },
      "WebhookInvalidUrlHttpsExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_url",
            "message": "url must be an https URL."
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_url",
          "error_message": "url must be an https URL.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "WebhookInvalidUrlExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_url",
            "message": "callbackUrl resolves to a non-public address (203.0.113.5); refusing to accept."
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_url",
          "error_message": "callbackUrl resolves to a non-public address (203.0.113.5); refusing to accept.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "WebhookEndpointLimitExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "webhook_endpoint_limit_reached",
            "message": "At most 16 webhook endpoints per account."
          },
          "error_type": "invalid_request_error",
          "error_slug": "webhook_endpoint_limit_reached",
          "error_message": "At most 16 webhook endpoints per account.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "WebhookEndpointListExample": {
        "value": {
          "object": "list",
          "data": [
            {
              "id": "we_00000000000000000000000000000002",
              "object": "webhook_endpoint",
              "url": "https://hooks.example.com/fastpay/webhooks",
              "description": "Payroll",
              "enabled_events": [
                "*"
              ],
              "status": "enabled",
              "is_test": false,
              "previous_secret_expires_at": null,
              "created_at": "2026-10-28T09:00:00.000Z",
              "updated_at": "2026-10-28T09:00:00.000Z"
            }
          ],
          "has_more": false
        }
      },
      "WebhookEndpointExample": {
        "summary": "An endpoint (no secret)",
        "value": {
          "id": "we_00000000000000000000000000000001",
          "object": "webhook_endpoint",
          "url": "https://hooks.example.com/fastpay/webhooks",
          "description": "Payroll",
          "enabled_events": [
            "payroll_batch.closed",
            "payroll_item.completed",
            "payroll_item.released"
          ],
          "status": "enabled",
          "is_test": true,
          "previous_secret_expires_at": null,
          "created_at": "2026-10-28T09:00:00.000Z",
          "updated_at": "2026-10-28T09:00:00.000Z"
        }
      },
      "WebhookEndpointMissingExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "resource_missing",
            "message": "No such webhook endpoint."
          },
          "error_type": "invalid_request_error",
          "error_slug": "resource_missing",
          "error_message": "No such webhook endpoint.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "WebhookEndpointDeletedExample": {
        "value": {
          "id": "we_00000000000000000000000000000001",
          "object": "webhook_endpoint",
          "deleted": true
        }
      },
      "WebhookEndpointRotatedExample": {
        "summary": "Rotated with a 24 h overlap",
        "value": {
          "id": "we_00000000000000000000000000000001",
          "object": "webhook_endpoint",
          "url": "https://hooks.example.com/fastpay/webhooks",
          "description": "Payroll",
          "enabled_events": [
            "*"
          ],
          "status": "enabled",
          "is_test": false,
          "previous_secret_expires_at": "2026-10-29T09:00:00.000Z",
          "created_at": "2026-10-28T08:00:00.000Z",
          "updated_at": "2026-10-28T09:00:00.000Z",
          "secret": "whsec_111111111111111111111111111111111111111111111111"
        }
      },
      "TonConnectManifestExample": {
        "value": {
          "url": "https://relay.example.com/checkout/index.html",
          "name": "FastPay Checkout",
          "iconUrl": "https://relay.example.com/checkout/fastpay-icon.png"
        }
      },
      "ExchangeRateExample": {
        "value": {
          "object": "rate",
          "from": "EUR",
          "to": "USDC",
          "rate": "1.0812345678",
          "source": "live",
          "fetched_at": "2026-10-24T09:00:00.000Z"
        }
      },
      "PaymentRequestCreateExample": {
        "value": {
          "reference": "order-0001",
          "network": "base",
          "currency": "usdc",
          "amount": "25.00",
          "recipient": "0x1111111111111111111111111111111111111111",
          "expiresAt": "2026-10-25T09:00:00Z",
          "metadata": {
            "order_id": "0001",
            "redirect_url": "https://shop.example.com/thanks"
          },
          "callbackUrl": "https://hooks.example.com/fastpay"
        }
      },
      "PaymentRequestTenantViewExample": {
        "value": {
          "id": "pr_00000000000000000000000000000000",
          "tenantId": "00000000-0000-4000-8000-000000000000",
          "reference": "order-0001",
          "network": "base",
          "currency": "USDC",
          "amount": "25.00",
          "recipient": "0x1111111111111111111111111111111111111111",
          "status": "pending",
          "expiresAt": "2026-10-25T09:00:00.000Z",
          "metadata": {
            "order_id": "0001",
            "redirect_url": "https://shop.example.com/thanks"
          },
          "idempotencyKey": "00000000-0000-4000-8000-000000000001",
          "callbackUrl": "https://hooks.example.com/fastpay",
          "payer": null,
          "settledPayer": null,
          "txHash": null,
          "isTest": false,
          "paymentLinkId": null,
          "createdAt": "2026-10-24T09:00:00.000Z",
          "updatedAt": "2026-10-24T09:00:00.000Z",
          "checkoutUrl": "https://relay.example.com/checkout/index.html?id=pr_00000000000000000000000000000000",
          "qr": {
            "checkout": {
              "payload": "https://relay.example.com/checkout/index.html?id=pr_00000000000000000000000000000000",
              "svgUrl": "https://relay.example.com/v1/payment-requests/pr_00000000000000000000000000000000/qr?type=checkout"
            }
          }
        }
      },
      "InvalidPaymentRequestErrorExample": {
        "value": {
          "error_type": "request_error",
          "error_slug": "invalid_payment_request",
          "error": "expiresAt must be in the future.",
          "error_message": "expiresAt must be in the future.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "PaymentRequestPublicViewExample": {
        "value": {
          "id": "pr_00000000000000000000000000000000",
          "reference": "order-0001",
          "network": "base",
          "currency": "USDC",
          "amount": "25.00",
          "recipient": "0x1111111111111111111111111111111111111111",
          "status": "pending",
          "expiresAt": "2026-10-25T09:00:00.000Z",
          "txHash": null,
          "isTest": false,
          "chainId": 8453,
          "redirectUrl": "https://shop.example.com/thanks",
          "checkoutUrl": "https://relay.example.com/checkout/index.html?id=pr_00000000000000000000000000000000",
          "qr": {
            "checkout": {
              "payload": "https://relay.example.com/checkout/index.html?id=pr_00000000000000000000000000000000",
              "svgUrl": "https://relay.example.com/v1/payment-requests/pr_00000000000000000000000000000000/qr?type=checkout"
            }
          }
        }
      },
      "CheckoutIntentEvmExample": {
        "value": {
          "paymentRequestId": "pr_00000000000000000000000000000000",
          "network": "base",
          "chainId": 8453,
          "to": "0x4444444444444444444444444444444444444444",
          "data": "0x00000000",
          "value": null,
          "approval": {
            "to": "0x5555555555555555555555555555555555555555",
            "data": "0x095ea7b3"
          },
          "currency": "USDC",
          "amount": "25.00",
          "fee": "0.125000",
          "feeCurrency": "USDC",
          "total": "25.125000",
          "expiresAt": "2026-10-24T09:15:00.000Z"
        }
      },
      "CheckoutIntentSandboxExample": {
        "value": {
          "paymentRequestId": "pr_00000000000000000000000000000000",
          "simulated": true,
          "network": "base",
          "chainId": 0,
          "to": "0x0000000000000000000000000000000000000001",
          "data": "0x",
          "value": null,
          "approval": null,
          "currency": "USDC",
          "amount": "25.00",
          "fee": "0.000000",
          "feeCurrency": "USDC",
          "total": "25.000000",
          "expiresAt": "2026-10-25T09:00:00.000Z",
          "sandboxTxHashes": {
            "failed": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
            "pending": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
          }
        }
      },
      "CheckoutSessionCreateExample": {
        "value": {
          "amount": "49.99",
          "currency": "EUR",
          "success_url": "https://shop.example.com/paid",
          "cancel_url": "https://shop.example.com/cart",
          "network": "solana",
          "settlement_currency": "usdc",
          "merchant_reference": "order-0002",
          "callback_url": "https://hooks.example.com/fastpay"
        }
      },
      "CheckoutSessionCreatedExample": {
        "value": {
          "id": "cs_00000000000000000000000000000000",
          "object": "checkout_session",
          "status": "open",
          "payment_status": "created",
          "amount": "49.99",
          "currency": "EUR",
          "network": "solana",
          "payment_amount": "54.051234",
          "payment_currency": "USDC",
          "exchange_rate": "1.0812345678",
          "success_url": "https://shop.example.com/paid",
          "cancel_url": "https://shop.example.com/cart",
          "payment_url": "https://relay.example.com/checkout/index.html?id=cs_00000000000000000000000000000000",
          "expires_at": "2026-10-24T09:30:00.000Z",
          "is_test": true,
          "created_at": "2026-10-24T09:00:00.000Z",
          "payments": [
            {
              "id": "pay_00000000000000000000000000000000",
              "status": "created",
              "merchant_reference": "order-0002",
              "expires_at": "2026-10-24T09:30:00.000Z"
            }
          ],
          "rate_source": "static_fallback",
          "rate_fetched_at": null
        }
      },
      "CheckoutRefundCreatedExample": {
        "value": {
          "id": "re_00000000000000000000000000000000",
          "object": "refund",
          "payment_id": "pay_00000000000000000000000000000000",
          "status": "pending",
          "amount": "5.000000",
          "currency": "USDC",
          "destination": "0x2222222222222222222222222222222222222222",
          "reason": "customer request",
          "is_test": false,
          "tx_hash": null,
          "failure_reason": null,
          "created_at": "2026-10-24T10:00:00.000Z",
          "transaction": {
            "chainId": 8453,
            "from": "0x1111111111111111111111111111111111111111",
            "to": "0x5555555555555555555555555555555555555555",
            "data": "0xa9059cbb",
            "value": null
          }
        }
      },
      "PaymentLinkCreateExample": {
        "value": {
          "network": "solana",
          "currency": "USDC",
          "amount": "10.00",
          "reference_prefix": "donation",
          "max_uses": 100,
          "session_ttl_seconds": 1800
        }
      },
      "PaymentLinkTenantViewExample": {
        "value": {
          "id": "plink_00000000000000000000000000000000",
          "object": "payment_link",
          "network": "solana",
          "currency": "USDC",
          "amount": "10.00",
          "min_amount": null,
          "max_amount": null,
          "recipient": "ExampLeRecipient11111111111111111111111111",
          "status": "active",
          "available": true,
          "unavailable_reason": null,
          "expires_at": null,
          "is_test": true,
          "url": "https://relay.example.com/checkout/link.html?id=plink_00000000000000000000000000000000",
          "qr": {
            "checkout": {
              "payload": "https://relay.example.com/checkout/link.html?id=plink_00000000000000000000000000000000",
              "svgUrl": "https://relay.example.com/v1/payment-links/plink_00000000000000000000000000000000/qr?type=checkout"
            }
          },
          "reference_prefix": "donation",
          "session_ttl_seconds": 1800,
          "max_uses": 100,
          "uses": 0,
          "metadata": {},
          "callback_url": null,
          "created_at": "2026-10-24T09:00:00.000Z",
          "updated_at": "2026-10-24T09:00:00.000Z"
        }
      },
      "X402PaymentRequiredExample": {
        "value": {
          "x402Version": 2,
          "error": "PAYMENT-SIGNATURE header is required",
          "resource": {
            "url": "https://relay.example.com/v1/x402/pay/pr_00000000000000000000000000000000",
            "description": "order-0001, mimeType: application/json"
          },
          "accepts": [
            {
              "scheme": "exact",
              "network": "eip155:8453",
              "amount": "25000000",
              "asset": "0x5555555555555555555555555555555555555555",
              "payTo": "0x1111111111111111111111111111111111111111",
              "maxTimeoutSeconds": 300,
              "extra": {
                "name": "USD Coin",
                "version": "2",
                "assetTransferMethod": "eip3009",
                "paymentFlow": "upfront"
              }
            }
          ],
          "extensions": {},
          "error_type": "payment_required",
          "error_slug": "payment_required",
          "error_message": "PAYMENT-SIGNATURE header is required",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "CatalogNetworkListExample": {
        "summary": "First entries (all 17 have live true)",
        "value": {
          "object": "list",
          "data": [
            {
              "object": "network",
              "network": "ethereum",
              "live": true
            },
            {
              "object": "network",
              "network": "base",
              "live": true
            },
            {
              "object": "network",
              "network": "solana",
              "live": true
            },
            {
              "object": "network",
              "network": "tron",
              "live": true
            }
          ],
          "has_more": false
        }
      },
      "CatalogCurrencyListExample": {
        "summary": "A few rows",
        "value": {
          "object": "list",
          "data": [
            {
              "object": "currency",
              "currency": "ETH",
              "network": "ethereum",
              "decimals": 18,
              "type": "native",
              "live": true
            },
            {
              "object": "currency",
              "currency": "USDC",
              "network": "ethereum",
              "decimals": 6,
              "type": "token",
              "live": true
            },
            {
              "object": "currency",
              "currency": "USDC",
              "network": "bnb",
              "decimals": 18,
              "type": "token",
              "live": true
            },
            {
              "object": "currency",
              "currency": "SOL",
              "network": "solana",
              "decimals": 9,
              "type": "native",
              "live": true
            }
          ],
          "has_more": false
        }
      },
      "ReconciliationReportExample": {
        "value": {
          "object": "reconciliation_report",
          "tenant_id": "00000000-0000-4000-8000-000000000000",
          "period": {
            "from": "2026-10-01T00:00:00.000Z",
            "to": "2026-10-23T23:59:59.999Z"
          },
          "summary": {
            "payments_paid": 2,
            "total_settlement_amount_by_currency": {
              "USDC": "99.30"
            },
            "payments_with_discrepancy": 1
          },
          "discrepancies": [
            {
              "payment_id": "pay_00000000000000000000000000000c02",
              "type": "chain_unconfirmed",
              "detail": "Payment is marked paid but has no payment attempt with a recorded tx_hash."
            }
          ]
        }
      },
      "ReconciliationWindowTooLargeExample": {
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "window_too_large",
            "message": "A report covers at most 92 days; ask for a longer period in parts."
          },
          "error_type": "invalid_request_error",
          "error_slug": "window_too_large",
          "error_message": "A report covers at most 92 days; ask for a longer period in parts.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "CustomerExample": {
        "value": {
          "id": "cus_0000000000000000000000000000b001",
          "object": "customer",
          "email": "payer@example.com",
          "name": "Example Payer Ltd",
          "country": "GB",
          "is_test": true,
          "created_at": "2026-10-24T08:00:00.000Z"
        }
      },
      "AccountingConnectStartExample": {
        "value": {
          "provider": "xero",
          "authorizationUrl": "https://login.xero.com/identity/connect/authorize?response_type=code&client_id=EXAMPLE_CLIENT_ID&redirect_uri=https%3A%2F%2Fportal.example.com%2Fintegrations%2Fxero%2Freturn&scope=accounting.transactions+offline_access&state=0123456789abcdef0123456789abcdef&code_challenge=EXAMPLE_CHALLENGE&code_challenge_method=S256",
          "state": "0123456789abcdef0123456789abcdef"
        }
      },
      "AccountingNotConfiguredExample": {
        "value": {
          "error_type": "configuration_error",
          "error_slug": "provider_not_configured",
          "error": "xero is not configured on this deployment (missing XERO_CLIENT_ID).",
          "error_message": "xero is not configured on this deployment (missing XERO_CLIENT_ID).",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "AccountingMappingsViewExample": {
        "value": {
          "provider": "xero",
          "connected": true,
          "expiresAt": "2026-10-24T11:00:00.000Z",
          "mapping": {
            "xeroTenantId": "EXAMPLE-XERO-TENANT",
            "gateway": {
              "USDC": {
                "currencyCode": "USD",
                "revenueAccountCode": "200"
              }
            }
          }
        }
      },
      "AccountingMappingsRequestExample": {
        "value": {
          "mapping": {
            "xeroTenantId": "EXAMPLE-XERO-TENANT",
            "defaults": {
              "bankAccountCode": "090",
              "taxCode": "NONE"
            },
            "gateway": {
              "base:USDC": {
                "bankAccountCode": "091"
              },
              "USDC": {
                "currencyCode": "USD",
                "revenueAccountCode": "200"
              }
            },
            "disbursement": {
              "USDC": {
                "currencyCode": "USD",
                "expenseAccountCode": "477"
              }
            }
          }
        }
      },
      "AccountingSyncResultExample": {
        "value": {
          "success": true,
          "receipt": {
            "success": true,
            "paymentRequestId": "cs_0000000000000000000000000000d001",
            "externalInvoiceId": "EXAMPLE-INV-1",
            "externalPaymentId": "EXAMPLE-PAY-1",
            "syncedAt": "2026-10-24T09:00:00.000Z"
          }
        }
      },
      "EcommerceOrderCheckoutSessionExample": {
        "value": {
          "id": "cs_0000000000000000000000000000d001",
          "object": "checkout_session",
          "status": "open",
          "payment_status": "created",
          "amount": "49.90",
          "currency": "EUR",
          "network": "base",
          "payment_amount": "53.910000",
          "payment_currency": "USDC",
          "exchange_rate": "1.08036",
          "success_url": "https://shop.example.com/thanks",
          "cancel_url": "https://shop.example.com/cart",
          "payment_url": "https://relay.example.com/checkout/index.html?id=cs_0000000000000000000000000000d001",
          "expires_at": "2026-10-24T09:30:00.000Z",
          "is_test": true,
          "created_at": "2026-10-24T09:00:00.000Z",
          "payments": [
            {
              "id": "pay_00000000000000000000000000000c03",
              "status": "created",
              "merchant_reference": "WOO-ORDER-1042",
              "expires_at": "2026-10-24T09:30:00.000Z"
            }
          ],
          "rate_source": "static_fallback",
          "rate_fetched_at": null
        }
      },
      "ErrInvalidApiKeyExample": {
        "value": {
          "error_type": "authentication_error",
          "error_slug": "invalid_api_key",
          "error": "API key is invalid or has been revoked.",
          "error_message": "API key is invalid or has been revoked.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ErrApiKeyExpiredExample": {
        "value": {
          "error_type": "authentication_error",
          "error_slug": "api_key_expired",
          "error": "API key has expired - use the key it was rotated to, or mint a new one.",
          "error_message": "API key has expired - use the key it was rotated to, or mint a new one.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ErrMissingApiKeyExample": {
        "value": {
          "error_type": "authentication_error",
          "error_slug": "missing_api_key",
          "error": "Missing or malformed Authorization header - expected \"Bearer <api key>\".",
          "error_message": "Missing or malformed Authorization header - expected \"Bearer <api key>\".",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "ApiKeysErrUnsupportedCharsetExample": {
        "summary": "The body parser's 415 (its own message)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_request",
            "message": "unsupported charset \"LATIN1\"",
            "param": null,
            "request_id": null
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_request",
          "error_message": "unsupported charset \"LATIN1\"",
          "request_id": null
        }
      },
      "PayrollInvalidItemsErrorExample": {
        "summary": "400 invalid_items from the validator (every problem at once)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "invalid_items",
            "message": "2 item problems; nothing was created.",
            "details": {
              "problems": [
                {
                  "index": 0,
                  "field": "amount",
                  "problem": "a positive decimal string"
                },
                {
                  "index": 3,
                  "field": "reference",
                  "problem": "printable ASCII, 1 to 256 characters"
                }
              ]
            }
          },
          "error_type": "invalid_request_error",
          "error_slug": "invalid_items",
          "error_message": "2 item problems; nothing was created.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "PayrollKeyNetworkErrorExample": {
        "summary": "403 api_key_network_not_allowed",
        "value": {
          "error": {
            "type": "permission_error",
            "code": "api_key_network_not_allowed",
            "message": "This API key may act on payroll for tron only, not solana.",
            "details": {
              "network": "solana",
              "key_networks": [
                "tron"
              ]
            }
          },
          "error_type": "permission_error",
          "error_slug": "api_key_network_not_allowed",
          "error_message": "This API key may act on payroll for tron only, not solana.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "PayrollIntentAlreadyLiveErrorExample": {
        "summary": "409 payroll_intent_already_live (I3)",
        "value": {
          "error": {
            "type": "invalid_request_error",
            "code": "payroll_intent_already_live",
            "message": "Another payroll intent for this payer wallet and token is still live until 2026-10-24T10:10:00.000Z. Wait for it to settle or expire, or resend it.",
            "details": {
              "intent_id": "pyx_0123456789abcdef0123456789abcdef",
              "expires_at": "2026-10-24T10:10:00.000Z",
              "flagged": false
            }
          },
          "error_type": "invalid_request_error",
          "error_slug": "payroll_intent_already_live",
          "error_message": "Another payroll intent for this payer wallet and token is still live until 2026-10-24T10:10:00.000Z. Wait for it to settle or expire, or resend it.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      },
      "PayrollNetworkNotEnabledErrorExample": {
        "summary": "503 payroll_network_not_enabled (live key, lane not enabled)",
        "value": {
          "error": {
            "type": "api_error",
            "code": "payroll_network_not_enabled",
            "message": "Client-signed payroll on solana is not open for this account's live keys.",
            "details": {
              "network": "solana"
            }
          },
          "error_type": "api_error",
          "error_slug": "payroll_network_not_enabled",
          "error_message": "Client-signed payroll on solana is not open for this account's live keys.",
          "request_id": "req_00000000-0000-4000-8000-000000000000"
        }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 200
        },
        "description": "Optional on mutating calls (any method but GET/HEAD/OPTIONS). The first request with a key runs; a repeat with the same key in the same scope (below) and the same request hash (SHA-256 of JSON.stringify({body, params: {}, query})) gets the stored status and body back with `X-Cache-Lookup: HIT` for 24 hours (default 24 h, production keeps the default) and runs nothing, so a replay costs no rate-limit slot (the per-tenant limiter runs after requireApiKey, e.g.). One replay is refreshed: a replayed 2xx of POST /v1/payroll-batches answers the batch as it is now, not the stored snapshot. Same key, other request: 409 idempotency_parameter_mismatch; first call still running: 409 idempotency_key_in_progress (Shape B, request_id inside error). Every answer is stored, 4xx and 5xx included, except a 429, 409 tenant_not_approved, 403 approval_requires_second_operator and 409 payroll_lane_not_open (both layers), and on tenant routes the 5xx of POST /v1/payroll-batches,.../intents.../fee-quotes: a retry under the same key runs again. Tenant layer (/v1 and /v2): runs last in requireApiKey, after the key, host, IP and scope checks; scoped by tenant, key mode and the path as sent without its query string, not by method, so a POST and a DELETE to one path under one key share a scope; bound to the API key that claimed it (another key: 409 idempotency_key_in_use); since its stored answers are sealed with a key derived from the calling raw API key, which the relay never stores, and an answer stored unsealed by an earlier release is never replayed nor run again: 409 idempotency_key_in_use with its own message until it expires. A replayed 2xx of POST /v1/members/{id}/api-keys or POST /v1/api-keys/rotate writes an api_key.replayed audit row first. Skipped when the path as sent starts with /v2/x402/ in lower case: a capitalised spelling of the x402 facilitator routes goes through the layer; the key's length is not checked except on POST /v1/payment-requests, which refuses an empty or over-200-character key with Shape A 400 invalid_idempotency_key and also keeps the key on the payment request for good (409 idempotency_key_conflict). an empty or over-200-character key is Shape B 400 invalid_idempotency_key; A replayed answer that carries a secret writes an audit row first. A credential rotation makes the old answers unreachable."
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        },
        "description": "Page size: an integer from 1 to the maximum page size (100 unless configured), default (20 unless configured); production configures neither, so 100 and 20 hold there; anything else is Shape B 400 invalid_pagination_parameter."
      },
      "StartingAfter": {
        "name": "starting_after",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Cursor: the id of the previous page's last item; answers the items after it. Lists are newest first by insertion order (rowid), not by a date field. Not together with ending_before (400 invalid_pagination_parameter). An unknown, deleted, foreign or mistyped id answers the FIRST page again, not an error. GET /v1/webhook-deliveries takes the delivery's integer id and pages from that position whether or not the delivery still exists, and ignores a cursor that is not an integer (first page)."
      },
      "EndingBefore": {
        "name": "ending_before",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Cursor: the id of the previous page's first item; answers the newer items before it, still newest first. Not together with starting_after. Same unknown-cursor behaviour as starting_after."
      },
      "CreatedGte": {
        "name": "created[gte]",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Only items created at or after this instant: ISO-8601 (a date-only value is midnight UTC) or 9-11 digit unix seconds; the flat alias created_gte is accepted. Unparseable, or later than created[lte]: Shape B 400 invalid_filter_parameter. Only on the lists whose handler reads it."
      },
      "CreatedLte": {
        "name": "created[lte]",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Only items created at or before this instant; same forms as created[gte] (alias created_lte)."
      },
      "ApiKeyListLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Page size, 1 to the maximum page size (100; no production override). The API-key lists default to the largest page (100), not to the shared Limit's 20; anything else is 400 invalid_pagination_parameter.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 100
        }
      },
      "LiveGateCreatedGteAlias": {
        "name": "created_gte",
        "in": "query",
        "required": false,
        "description": "Flat alias of created[gte]; the bracket form wins when both are sent.",
        "schema": {
          "type": "string"
        }
      },
      "LiveGateCreatedLteAlias": {
        "name": "created_lte",
        "in": "query",
        "required": false,
        "description": "Flat alias of created[lte].",
        "schema": {
          "type": "string"
        }
      },
      "PayerWalletStatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "description": "Only wallets in this status; anything else (or a repeated parameter) is 400 invalid_filter_parameter.",
        "schema": {
          "type": "string",
          "enum": [
            "pending_verification",
            "verified",
            "revoked"
          ]
        }
      },
      "PayrollNetworkQueryFilter": {
        "name": "network",
        "in": "query",
        "required": false,
        "description": "Only rows on this payroll network; anything else (or a repeated parameter) is 400 invalid_filter_parameter.",
        "schema": {
          "$ref": "#/components/schemas/PayrollNetworkName"
        }
      },
      "PayrollBatchStatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "description": "One batch status. Anything else, or the parameter repeated, is Shape B 400 invalid_filter_parameter.",
        "schema": {
          "type": "string",
          "enum": [
            "awaiting_approval",
            "open",
            "closing",
            "completed",
            "closed",
            "cancelled"
          ]
        }
      },
      "PayrollNetworkFilter": {
        "name": "network",
        "in": "query",
        "required": false,
        "description": "One payroll network. Anything else is 400 invalid_filter_parameter.",
        "schema": {
          "$ref": "#/components/schemas/PayrollNetwork"
        }
      },
      "PayrollBatchIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "A payroll batch id (`pyb_` + 32 hex). Unknown, foreign or other-mode ids answer 404 resource_missing.",
        "schema": {
          "type": "string",
          "example": "pyb_0123456789abcdef0123456789abcdef"
        }
      },
      "PayrollItemStatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "description": "One item status. Anything else is 400 invalid_filter_parameter.",
        "schema": {
          "type": "string",
          "enum": [
            "pending",
            "authorized",
            "completed",
            "failed",
            "cancelled"
          ]
        }
      },
      "PayrollIntentIdPath": {
        "name": "intentId",
        "in": "path",
        "required": true,
        "description": "A payroll intent id (`pyx_` + 32 hex). An intent of another batch answers 404.",
        "schema": {
          "type": "string",
          "example": "pyx_0123456789abcdef0123456789abcdef"
        }
      },
      "PayrollFeeQuoteIdPath": {
        "name": "feeQuoteId",
        "in": "path",
        "required": true,
        "description": "A fee quote id; anything but `pfs_` + 32 lower-case hex is 404 before any lookup.",
        "schema": {
          "type": "string",
          "pattern": "^pfs_[0-9a-f]{32}$",
          "example": "pfs_0123456789abcdef0123456789abcdef"
        }
      },
      "WebhookDeliveryAllocationIdQuery": {
        "name": "allocation_id",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 256
        },
        "description": "Exact match on the outbox key: the mass-payout ALLOCATION_ID (payment.*), the job id (payout_job.*), the payment request or checkout session id, the refund id, the deposit id, the payroll batch id (payroll_batch.*), item id (payroll_item.*), authorization id (payroll_authorization.*), fee quote id (payroll_fee_settlement.paid) or payer wallet id. Empty = no filter; repeated, bracketed or over 256 characters - 400 invalid_filter_parameter."
      },
      "WebhookDeliveryIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "examples": [
            "4182"
          ]
        },
        "description": "The delivery's INTEGER outbox id, as text. A non-integer value is the same 404 as an unknown id, not a 400."
      },
      "WebhookEndpointStatusQuery": {
        "name": "status",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "enabled",
            "disabled"
          ]
        },
        "description": "Only endpoints of this status; anything else - 400 invalid_filter_parameter."
      },
      "WebhookEndpointIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "examples": [
            "we_00000000000000000000000000000001"
          ]
        },
        "description": "The endpoint id, we_ + 32 lower-case hex."
      },
      "PaymentRequestStatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "description": "anything else 400 invalid_filter_parameter.",
        "schema": {
          "type": "string",
          "enum": [
            "pending",
            "paid",
            "paid_after_expiry"
          ]
        }
      },
      "CheckoutNetworkFilter": {
        "name": "network",
        "in": "query",
        "required": false,
        "description": "A checkout network key, exact lower case.",
        "schema": {
          "$ref": "#/components/schemas/CheckoutNetwork"
        }
      },
      "PaymentLinkIdFilter": {
        "name": "payment_link_id",
        "in": "query",
        "required": false,
        "description": "Only requests opened from this link. Any string of at most 200 characters.",
        "schema": {
          "type": "string",
          "maxLength": 200
        }
      },
      "PaymentRequestIdPath": {
        "name": "requestId",
        "in": "path",
        "required": true,
        "description": "A payment request id (pr_ + 32 hex) or a checkout session id (cs_ + 32 hex: a session settles as a payment request with the same id). Not validated; an unknown id is a 404.",
        "schema": {
          "type": "string",
          "examples": [
            "pr_00000000000000000000000000000000"
          ]
        }
      },
      "CheckoutQrType": {
        "name": "type",
        "in": "query",
        "required": false,
        "description": "repeated or another value is 400 invalid_qr_type.",
        "schema": {
          "type": "string",
          "enum": [
            "checkout",
            "solana-pay"
          ],
          "default": "checkout"
        }
      },
      "CheckoutQrSize": {
        "name": "size",
        "in": "query",
        "required": false,
        "description": "Pixels. 1 to 5 digits, clamped to 128-1024 (not refused); empty or absent = 256; anything else 400 invalid_qr_size.",
        "schema": {
          "type": "string",
          "pattern": "^\\d{1,5}$",
          "default": "256"
        }
      },
      "CheckoutSessionStatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "open",
            "completed",
            "expired"
          ]
        }
      },
      "CheckoutCustomerIdFilter": {
        "name": "customer_id",
        "in": "query",
        "required": false,
        "description": "Equality on the session's stored customer id (at most 200 characters).",
        "schema": {
          "type": "string",
          "maxLength": 200
        }
      },
      "CheckoutSessionIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "A checkout session id (cs_ + 32 hex).",
        "schema": {
          "type": "string",
          "examples": [
            "cs_00000000000000000000000000000000"
          ]
        }
      },
      "CheckoutPaymentStatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "created",
            "paid",
            "expired"
          ]
        }
      },
      "CheckoutSessionIdFilter": {
        "name": "checkout_session_id",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 200
        }
      },
      "CheckoutPaymentIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "A checkout payment id (pay_ + 32 hex).",
        "schema": {
          "type": "string",
          "examples": [
            "pay_00000000000000000000000000000000"
          ]
        }
      },
      "CheckoutRefundStatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "pending",
            "confirmed",
            "failed",
            "expired"
          ]
        }
      },
      "CheckoutPaymentIdFilter": {
        "name": "payment_id",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 200
        }
      },
      "CheckoutRefundIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "A refund id (re_ + 32 hex).",
        "schema": {
          "type": "string",
          "examples": [
            "re_00000000000000000000000000000000"
          ]
        }
      },
      "PaymentLinkStatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "description": "The stored status.",
        "schema": {
          "type": "string",
          "enum": [
            "active",
            "inactive"
          ]
        }
      },
      "PaymentLinkIdPath": {
        "name": "linkId",
        "in": "path",
        "required": true,
        "description": "A payment link id (plink_ + 32 hex).",
        "schema": {
          "type": "string",
          "examples": [
            "plink_00000000000000000000000000000000"
          ]
        }
      },
      "X402PaymentSignatureHeader": {
        "name": "PAYMENT-SIGNATURE",
        "in": "header",
        "required": false,
        "description": "Base64 of the JSON x402 v2 PaymentPayload. Absent: the 402 with PAYMENT-REQUIRED.",
        "schema": {
          "type": "string",
          "contentEncoding": "base64"
        }
      },
      "EcommercePlatformPath": {
        "name": "platform",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "enum": [
            "whmcs",
            "shopify",
            "woocommerce",
            "magento",
            "prestashop",
            "wix",
            "squarespace",
            "bigcommerce",
            "opencart"
          ]
        },
        "description": "E-commerce platform, case-insensitive (lower-cased)."
      },
      "EcommerceWebhookTenantIdPath": {
        "name": "tenantId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "example": "00000000-0000-4000-8000-000000000000"
        },
        "description": "The tenant id (a UUID v4) as the settings route's webhook_url gave it; any value is accepted and looked up."
      },
      "ReconciliationFrom": {
        "name": "from",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "example": "2026-10-01"
        },
        "description": "Window start: ISO-8601 (a date-only value is midnight UTC) or 9-11 digit unix seconds. Default: `to` minus 30 days (the default window). Empty, repeated or bracketed: 400 invalid_date."
      },
      "ReconciliationTo": {
        "name": "to",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "example": "2026-10-23"
        },
        "description": "Window end, same forms; a date-only value runs to 23:59:59.999 UTC of that day. Default: now. At most 92 days after `from` (the maximum window)."
      },
      "CustomerIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "example": "cus_0000000000000000000000000000b001"
        },
        "description": "Customer id (cus_ + 32 hex)."
      },
      "AccountingProviderPath": {
        "name": "provider",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "enum": [
            "xero",
            "quickbooks",
            "zoho",
            "sage"
          ]
        },
        "description": "Accounting provider, case-insensitive (lower-cased)."
      },
      "AccountingRedirectUriQuery": {
        "name": "redirect_uri",
        "in": "query",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uri",
          "maxLength": 2048,
          "pattern": "^[Hh][Tt][Tt][Pp][Ss]://",
          "example": "https://portal.example.com/integrations/xero/return"
        },
        "description": "Required: the integrator's own https page that the provider sends the person's browser to with code and state; that page POSTs both to POST /v1/accounting/{provider}/callback with the API key (the relay serves no GET for the callback). Absent or empty: Shape A 400 parameter_missing; not one https URL with a host, in printable ASCII without backslash, user info or fragment, at most 2048 characters (a repeated parameter included): Shape A 400 invalid_url. Stored with the state and sent to the provider exactly as given, then used again at the token exchange: register the same URL with the provider."
      }
    },
    "responses": {
      "ApiKeysTenantUnauthorized": {
        "description": "The tenant key was refused at authentication (all envelope A, error_type authentication_error): `missing_api_key` (no `Authorization: Bearer <key>`; the word Bearer is case-sensitive), `invalid_api_key` (unknown, revoked, or the key's tenant is suspended), `api_key_expired`, `member_suspended`. Checked in the order revoked, expired, member suspended, tenant inactive.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "invalidKey": {
                "$ref": "#/components/examples/ErrInvalidApiKeyExample"
              },
              "expired": {
                "$ref": "#/components/examples/ErrApiKeyExpiredExample"
              },
              "missing": {
                "$ref": "#/components/examples/ErrMissingApiKeyExample"
              }
            }
          }
        }
      },
      "ApiKeysBodyTooLarge": {
        "description": "`request_body_too_large` (envelope B, request_id null): raised by the body parser before the request id exists.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ApiKeysUnsupportedBody": {
        "description": "`invalid_request` (envelope B, request_id null, no X-Request-ID or X-API-Version header): the body parser refused a JSON content-type whose charset is not a UTF one, or a Content-Encoding other than identity, gzip or deflate, with its own message, before every other check.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "charset": {
                "$ref": "#/components/examples/ApiKeysErrUnsupportedCharsetExample"
              }
            }
          }
        }
      },
      "ApiKeysInternalError": {
        "description": "`internal_error` (envelope B, error_type api_error), never a node's text.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "internal": {
                "$ref": "#/components/examples/ErrInternalExample"
              }
            }
          }
        }
      },
      "LiveGateBodyTooLarge413": {
        "description": "request_body_too_large (Shape B), from the body parser before routing: request_id null and no X-Request-ID header.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "tooLarge": {
                "value": {
                  "error": {
                    "type": "invalid_request_error",
                    "code": "request_body_too_large",
                    "message": "Request body exceeds the maximum allowed size.",
                    "param": null,
                    "request_id": null
                  },
                  "error_type": "invalid_request_error",
                  "error_slug": "request_body_too_large",
                  "error_message": "Request body exceeds the maximum allowed size.",
                  "request_id": null
                }
              }
            }
          }
        }
      },
      "LiveGateBodyEncodingUnsupported415": {
        "description": "invalid_request (Shape B) from the body parser before routing: a JSON content type with an unsupported charset, or a Content-Encoding other than identity, gzip or deflate; the message is the parser's own, request_id null.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "charset": {
                "value": {
                  "error": {
                    "type": "invalid_request_error",
                    "code": "invalid_request",
                    "message": "unsupported charset \"LATIN1\"",
                    "param": null,
                    "request_id": null
                  },
                  "error_type": "invalid_request_error",
                  "error_slug": "invalid_request",
                  "error_message": "unsupported charset \"LATIN1\"",
                  "request_id": null
                }
              }
            }
          }
        }
      },
      "LiveGateTenantUnauthorized401": {
        "description": "missing_api_key, invalid_api_key (unknown or revoked key, or a suspended tenant), api_key_expired or member_suspended (Shape A, error_type authentication_error).",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "invalidKey": {
                "value": {
                  "error_type": "authentication_error",
                  "error_slug": "invalid_api_key",
                  "error": "API key is invalid or has been revoked.",
                  "error_message": "API key is invalid or has been revoked.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "LiveGateTenantRateLimited429": {
        "description": "too_many_requests (Shape A, error_type rate_limit_error), per tenant per minute. Never stored by the Idempotency-Key layer: a retry under the same key runs.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "limited": {
                "value": {
                  "error_type": "rate_limit_error",
                  "error_slug": "too_many_requests",
                  "error": "Too many requests - slow down.",
                  "error_message": "Too many requests - slow down.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "LiveGateInternalError500": {
        "description": "internal_error (Shape B): an untyped error, its text in the operator log only. Never read a 500 on a read as a state; show \"unknown\".",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "internal": {
                "value": {
                  "error": {
                    "type": "api_error",
                    "code": "internal_error",
                    "message": "Internal server error.",
                    "param": null,
                    "request_id": "req_00000000-0000-4000-8000-000000000000"
                  },
                  "error_type": "api_error",
                  "error_slug": "internal_error",
                  "error_message": "Internal server error.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "LiveGateTenantReadForbidden403": {
        "description": "sandbox_host_requires_test_key, ip_not_allowed or insufficient_scope (Shape A, permission_error).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "scope": {
                "value": {
                  "error_type": "permission_error",
                  "error_slug": "insufficient_scope",
                  "error": "This API key lacks the payroll:read scope required for GET /v1/payroll-payer-wallets.",
                  "error_message": "This API key lacks the payroll:read scope required for GET /v1/payroll-payer-wallets.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "PayerWalletNotFound404": {
        "description": "resource_missing (Shape B, \"No such payer wallet.\").",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PayrollShapeBErrorBody"
            },
            "examples": {
              "missing": {
                "value": {
                  "error": {
                    "type": "invalid_request_error",
                    "code": "resource_missing",
                    "message": "No such payer wallet."
                  },
                  "error_type": "invalid_request_error",
                  "error_slug": "resource_missing",
                  "error_message": "No such payer wallet.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "PayrollBadRequest": {
        "description": "400 - the request was refused before anything changed. Branch on error_slug.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "invalidItems": {
                "$ref": "#/components/examples/PayrollInvalidItemsErrorExample"
              }
            }
          }
        }
      },
      "PayrollUnauthorized": {
        "description": "401, Shape A, error_type authentication_error: missing_api_key, invalid_api_key (unknown, revoked, or the tenant suspended), api_key_expired, member_suspended.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PayrollForbidden": {
        "description": "403. Shape A from authentication (error_type permission_error) or Shape B from payroll; see the operation.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "networkNotAllowed": {
                "$ref": "#/components/examples/PayrollKeyNetworkErrorExample"
              }
            }
          }
        }
      },
      "PayrollNotFound": {
        "description": "404, Shape B (error.type invalid_request_error).",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PayrollConflict": {
        "description": "409, Shape B. Nothing was changed by this request.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "intentAlreadyLive": {
                "$ref": "#/components/examples/PayrollIntentAlreadyLiveErrorExample"
              }
            }
          }
        }
      },
      "PayrollPayloadTooLarge": {
        "description": "413 request_body_too_large from the body parser, before authentication; request_id null in the body.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PayrollUnsupportedMediaType": {
        "description": "415 invalid_request, Shape B, from the body parser: a JSON content-type whose charset does not start with utf-, or an unsupported Content-Encoding. It answers before authentication and before the request id is set: request_id null in the body, no X-Request-ID or X-API-Version header.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PayrollRateLimited": {
        "description": "429 too_many_requests, Shape A, error_type rate_limit_error: the tenant's per-minute budget, one for every key of the tenant in both modes (60 per minute by default 60 unless the tenant has its own limit). Never stored for replay.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PayrollInternalError": {
        "description": "500 internal_error, Shape B: an untyped error (a bug or an escaped provider error); the payroll routes answer the global handler's generic body and log the cause.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PayrollUnavailable": {
        "description": "503, Shape B, error.type api_error. Retry-After is set when the error carries a retry hint. details.network names the request's own network only; details.retryable false means the same request is refused again until FastPay acts.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "laneClosed": {
                "$ref": "#/components/examples/PayrollNetworkNotEnabledErrorExample"
              }
            }
          }
        }
      },
      "PayrollPaymentRequired": {
        "description": "402, Shape B.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "WebhooksTenantKeyUnauthorized": {
        "description": "Shape A, error_type authentication_error: missing_api_key, invalid_api_key (unknown, revoked, or the tenant suspended), api_key_expired, member_suspended. See the operation's x-fastpay-error-codes.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing": {
                "$ref": "#/components/examples/ErrMissingApiKeyExample"
              }
            }
          }
        }
      },
      "WebhooksTenantKeyForbidden": {
        "description": "Shape A, permission_error: sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope (a key without webhooks:read / webhooks:write, or *; a member role narrowing it; a path in capital letters is classified in lower case).",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "scope": {
                "$ref": "#/components/examples/WebhooksInsufficientScopeExample"
              }
            }
          }
        }
      },
      "WebhooksBodyTooLarge": {
        "description": "Shape B request_body_too_large (over the request body limit). The body parser answers before every other step: request_id null, no X-Request-ID or X-API-Version header.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "WebhooksUnsupportedMediaType": {
        "description": "Shape B invalid_request with status 415: a JSON content-type whose charset is not UTF-* (for example application/json; charset=latin1), or a Content-Encoding other than identity, gzip or deflate (body-parser 1.20.6). The body parser answers before every other step: request_id null, no X-Request-ID or X-API-Version header.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CheckoutBodyRefused": {
        "description": "invalid_json (a body sent as JSON that does not parse) or invalid_request (body-parser refused the body: aborted, or an invalid Content-Length) - Shape B from the global handler, request_id null (express.json at runs before every route and before the request id).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CheckoutBodyTooLarge": {
        "description": "request_body_too_large - Shape B from the global handler; the default ceiling is 46,384 bytes (the request body limit = 50 x 600 + 16,384); request_id null (body-parser runs before the request id).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CheckoutBodyEncodingUnsupported": {
        "description": "invalid_request at 415 - body-parser refused the body's charset or Content-Encoding (Shape B from the global handler, request_id null).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CheckoutInternalErrorShapeB": {
        "description": "internal_error \"Internal server error.\" - the global error handler, Shape B with error.param null and error.request_id.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CheckoutRateLimited": {
        "description": "too_many_requests - Shape A, error_type rate_limit_error (the handler). Never stored by the Idempotency-Key layers.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "limited": {
                "value": {
                  "error_type": "rate_limit_error",
                  "error_slug": "too_many_requests",
                  "error": "Too many requests - slow down.",
                  "error_message": "Too many requests - slow down.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "CheckoutTenantUnauthorized": {
        "description": "missing_api_key, invalid_api_key, api_key_expired, member_suspended - Shape A, error_type authentication_error.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing": {
                "value": {
                  "error_type": "authentication_error",
                  "error_slug": "missing_api_key",
                  "error": "Missing or malformed Authorization header - expected \"Bearer <api key>\".",
                  "error_message": "Missing or malformed Authorization header - expected \"Bearer <api key>\".",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "CheckoutTenantForbidden": {
        "description": "sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope - Shape A, error_type permission_error.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CheckoutInternalErrorShapeA": {
        "description": "internal_error \"Internal error.\" - the route's own catch, Shape A; the cause goes to the operator log only.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CheckoutListParameterError": {
        "description": "invalid_pagination_parameter or invalid_filter_parameter (Shape B); also invalid_json and body-parser's invalid_request when a body is sent.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CheckoutPathNotDecodable": {
        "description": "invalid_request - a path parameter that is not valid percent-encoding, or a body body-parser refused (aborted, or an invalid Content-Length); invalid_json - a body sent as JSON that does not parse (Shape B).",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CheckoutPaymentRequestNotFound": {
        "description": "payment_request_not_found - Shape A.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "notFound": {
                "value": {
                  "error_type": "request_error",
                  "error_slug": "payment_request_not_found",
                  "error": "No such payment request.",
                  "error_message": "No such payment request.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "CheckoutIdempotencyConflict": {
        "description": "idempotency_key_in_use (another API key of the tenant used it, or its stored answer does not open under this key: a row stored unsealed before the 2026-10-28 deploy, until it expires), idempotency_parameter_mismatch, idempotency_key_in_progress - Shape B, request_id also inside error.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CheckoutResourceMissing": {
        "description": "resource_missing - Shape B.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CheckoutPaymentLinkNotFound": {
        "description": "payment_link_not_found - Shape A.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "LedgerPayoutsBodyTooLarge": {
        "description": "Shape B 413 request_body_too_large: the body is over the request body limit (default 46,384 bytes). Raised before the request-id middleware: request_id null, no X-Request-ID.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "LedgerPayoutsUnsupportedMediaType": {
        "description": "Shape B invalid_request with body-parser's own 4xx status, e.g. 415 for an unsupported charset or Content-Encoding. request_id null.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "LedgerPayoutsTenantUnauthorized": {
        "description": "Shape A, error_type authentication_error: missing_api_key, invalid_api_key (unknown, revoked, or suspended tenant), api_key_expired, member_suspended.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing": {
                "value": {
                  "error_type": "authentication_error",
                  "error_slug": "missing_api_key",
                  "error": "Missing or malformed Authorization header - expected \"Bearer <api key>\".",
                  "error_message": "Missing or malformed Authorization header - expected \"Bearer <api key>\".",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "LedgerPayoutsTenantForbidden": {
        "description": "Shape A, error_type permission_error: sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {}
          }
        }
      },
      "LedgerPayoutsInternalError": {
        "description": "Shape B 500 internal_error \"Internal server error.\" for any untyped failure; routes with their own catch-all say so on the operation.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "internal": {
                "value": {
                  "error": {
                    "type": "api_error",
                    "code": "internal_error",
                    "message": "Internal server error."
                  },
                  "error_type": "api_error",
                  "error_slug": "internal_error",
                  "error_message": "Internal server error.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "LedgerPayoutsIdempotencyConflict": {
        "description": "Shape B 409 with error.request_id (error.type invalid_request_error): idempotency_parameter_mismatch (same key, other request), idempotency_key_in_progress (first call still running), idempotency_key_in_use (key claimed by another API key of the tenant, a stored answer that does not open with the calling key: the tenant layer seals what it stores with a key derived from the calling API key, and a row an earlier release stored unsealed is refused until it expires, never replayed or run again). Route-specific 409s are listed on the operation.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "mismatch": {
                "value": {
                  "error": {
                    "type": "invalid_request_error",
                    "code": "idempotency_parameter_mismatch",
                    "message": "Idempotency key parameter mismatch.",
                    "request_id": "req_00000000-0000-4000-8000-000000000000"
                  },
                  "error_type": "invalid_request_error",
                  "error_slug": "idempotency_parameter_mismatch",
                  "error_message": "Idempotency key parameter mismatch.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "LedgerPayoutsTenantForbiddenLiveKey": {
        "description": "Shape A, error_type permission_error: the key's own refusals sandbox_host_requires_test_key, ip_not_allowed, insufficient_scope, and live_key_required for a sandbox (fp_test_) key on a route whose data has one copy for both modes. The integration routes (accounting and e-commerce settings) only.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "liveKey": {
                "value": {
                  "error_type": "permission_error",
                  "error_slug": "live_key_required",
                  "error": "This action changes live data: use a live (fp_live_) API key.",
                  "error_message": "This action changes live data: use a live (fp_live_) API key.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              },
              "scope": {
                "value": {
                  "error_type": "permission_error",
                  "error_slug": "insufficient_scope",
                  "error": "This API key lacks the integrations:write scope required for POST /v1/accounting/xero/mappings.",
                  "error_message": "This API key lacks the integrations:write scope required for POST /v1/accounting/xero/mappings.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      },
      "LedgerPayoutsRateLimited": {
        "description": "Shape A 429 too_many_requests, error_type rate_limit_error, with Retry-After and the draft-6 RateLimit headers. Never stored by the Idempotency-Key layer.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "limited": {
                "value": {
                  "error_type": "rate_limit_error",
                  "error_slug": "too_many_requests",
                  "error": "Too many requests - slow down.",
                  "error_message": "Too many requests - slow down.",
                  "request_id": "req_00000000-0000-4000-8000-000000000000"
                }
              }
            }
          }
        }
      }
    }
  }
}
