{
  "openapi": "3.0.4",
  "info": {
    "title": "Nalpay API",
    "description": "The Nalpay API moves money for Saudi merchants: card payments, payment links, saved cards and\nrecurring subscriptions. It is small on purpose — a handful of flat resources, one error shape, one\nway to paginate — so that a developer, or the agent working for them, can integrate without reading\na manual first.\n\n## Authentication\n\nSend a secret key as a bearer token: `Authorization: Bearer sk_test_…`. The key's prefix decides\nwhich universe you are in. An `sk_test_` key can only ever see and create test objects, and no\nheader can change that, so a staging server holding a test key cannot charge a real card. Live keys\nrequire an approved business verification. A publishable (`pk_`) key authenticates nothing.\n\nStart with `GET /v1/account`: it answers who you are, which mode you are in, and what is unlocked.\n\n## Money\n\nAmounts are integers in the currency's minor units — 10000 is 100.00 SAR. There are no decimals\nanywhere in this API.\n\nEvery amount is between `1` and `100000000` halalas — one halala to one million riyals — and anything\noutside that is refused with `parameter_invalid` naming the field. `currency` must be `SAR`: Nalpay\nsettles in Saudi riyals only today, so an amount in another currency is one we could not collect or\npay out, and it is refused rather than quietly charged in riyals.\n\n## Object ids\n\nEvery id carries a prefix that says what it refers to: `pay_`, `cus_`, `card_`, `sub_`, `cyc_`,\n`plink_`, `ref_`, `acct_`. Ids are opaque; pass them back exactly as you received them. An id created\nby a test key does not exist for a live key, and the reverse.\n\n## Idempotency\n\nEvery POST must carry an `Idempotency-Key` header — any unique string, a UUID is ideal. Repeat a\nrequest with the same key and the same body and you get the stored response back rather than a\nsecond charge; repeat it with the same key and a different body and it is refused with\n`idempotency_key_reuse`. Keys are remembered for 24 hours. This is not optional, because a client\nthat retries freely and an endpoint that charges money are otherwise a bad combination.\n\n## Errors\n\nEvery non-2xx response has the same body:\n\n```json\n{ \"error\": { \"type\": \"invalid_request_error\", \"code\": \"parameter_invalid\",\n             \"message\": \"…\", \"param\": \"amount\",\n             \"doc_url\": \"https://paywithnal.com/docs/errors/parameter_invalid\" } }\n```\n\nBranch on `type` (`invalid_request_error`, `authentication_error`, `permission_error`,\n`not_found_error`, `rate_limit_error`, `api_error`) and report `code`.\n\n## Pagination\n\nLists take `limit` (1–100, default 20) and `starting_after` — the id of the last object on the\nprevious page. They return `{ \"data\": [...], \"has_more\": true }`. There are no offsets and no page\nnumbers, so a list you are walking stays consistent while it is being written to.\n\n## Versioning\n\nThe version lives in the path. Send an optional dated `Nalpay-Version` header (e.g. `2026-09-04`) to\npin behaviour; it is echoed back on every response so you can always see what served you.\n\n## Rate limits\n\n100 requests per minute per key, with a ceiling of 20 per second. Over the limit you get `429` with\na `Retry-After` header.\n\n## Testing recurring billing\n\nRenewals are a month away, which makes them untestable in a sitting. With a test key,\n`POST /v1/test/subscriptions/{id}/advance` runs the next cycle immediately using the same billing\ncode the nightly job runs.\n\nEvery response carries a `Nalpay-Request-Id`. Quote it to support and we can find your exact request.",
    "contact": {
      "name": "Nalpay support",
      "url": "https://paywithnal.com/docs"
    },
    "version": "v1"
  },
  "paths": {
    "/v1/account": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Who this key belongs to, which mode it is bound to, and what is unlocked.",
        "description": "Make this your first call. The mode comes from the key's own prefix — an `sk_test_` key can never\nbe talked into live mode by a header — and `capabilities` tells you whether the account can charge\nyet, so an integration can fail loudly on setup rather than on the first payment.\n            \n```curl https://paywithnal.com/v1/account -H \"Authorization: Bearer sk_test_…\"```",
        "operationId": "GetAccount",
        "parameters": [
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Account"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/cards/{id}": {
      "delete": {
        "tags": [
          "Customers"
        ],
        "summary": "Forget a stored card.",
        "description": "The token is deleted at the gateway and the card stops being chargeable. Payments already made with it\nkeep their history. Returns the card as it was at the moment it was removed.",
        "operationId": "DeleteCard",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Card"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/customers": {
      "post": {
        "tags": [
          "Customers"
        ],
        "summary": "Create a customer, or return the one that already matches.",
        "description": "Customers are matched on phone first, then email, so calling this twice with the same details gives you\nthe same customer rather than a duplicate. Give at least one of `name`, `email`, `phone`.\n            \n```\ncurl https://paywithnal.com/v1/customers \\\n  -H \"Authorization: Bearer sk_test_…\" -H \"Idempotency-Key: 8c1d…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"Sara\",\"email\":\"sara@example.com\",\"phone\":\"0512345678\"}'\n```",
        "operationId": "CreateCustomer",
        "parameters": [
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateCustomerRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateCustomerRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateCustomerRequest"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Customer"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      },
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "Customers, newest first.",
        "operationId": "ListCustomers",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "1–100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "A `cus_` id from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1List_V1Customer"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/customers/{id}": {
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "One customer, by its `cus_` id.",
        "operationId": "GetCustomer",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Customer"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/customers/{id}/cards": {
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "The cards this customer has stored, newest first.",
        "description": "A card is a token held at the gateway. Nalpay stores the brand, the last four digits and the expiry —\nnever a card number.",
        "operationId": "ListCustomerCards",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The customer's `cus_` id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1–100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "A `card_` id from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1List_V1Card"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/events/{id}": {
      "get": {
        "tags": [
          "Events"
        ],
        "summary": "One event, by its `evt_` id.",
        "description": "The `data` you get back is read from the stored payload rather than re-rendered, so it matches\nthe body that was posted to your endpoint byte for byte, even if the object has changed since. It is\nthe object as of when the event was recorded — a moment after the change, not at it.",
        "operationId": "GetEvent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Event"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/events": {
      "get": {
        "tags": [
          "Events"
        ],
        "summary": "Events in this mode, newest first.",
        "description": "Useful as a catch-up: after an outage, page from the last event id you processed rather than asking us\nto resend each one.",
        "operationId": "ListEvents",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "1–100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "An `evt_` id from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "One event type, for example `payment_paid`. Omit for all of them.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "created_from",
            "in": "query",
            "description": "Inclusive lower bound on `created`, UTC ISO 8601.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "created_to",
            "in": "query",
            "description": "Exclusive upper bound on `created`, UTC ISO 8601.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1List_V1Event"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/events/{id}/resend": {
      "post": {
        "tags": [
          "Events"
        ],
        "summary": "Send an event again.",
        "description": "The same stored bytes are posted, so the body is identical to the original delivery; only the\ntimestamp in `Nalpay-Signature` and therefore the signature itself differ, which is what stops a\nreplay being usable forever. `Nalpay-Delivery` carries the same `evt_` id, so a receiver that\ndedupes on it will recognise the repeat.\n            \nName an endpoint in `webhook_endpoint` to send it to one; omit it to send to every enabled\nendpoint subscribed to this event's type. Returns the deliveries that were queued.\n            \n```\ncurl https://paywithnal.com/v1/events/evt_…/resend \\\n  -H \"Authorization: Bearer sk_test_…\" -H \"Idempotency-Key: 4f7a…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"webhook_endpoint\":\"we_…\"}'\n```",
        "operationId": "ResendEvent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1ResendEventRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/V1ResendEventRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/V1ResendEventRequest"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1List_V1WebhookDelivery"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/payment_links": {
      "post": {
        "tags": [
          "PaymentLinks"
        ],
        "summary": "Create a payment link.",
        "description": "Returns a `url` on Nalpay's hosted page. Send it to the customer by any channel you like; the card\ndetails are collected there, inside the gateway's own fields, and never pass through your site or ours.\n            \n```\ncurl https://paywithnal.com/v1/payment_links \\\n  -H \"Authorization: Bearer sk_test_…\" -H \"Idempotency-Key: 4d9e…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"amount\":25000,\"currency\":\"SAR\",\"description\":\"Invoice 2026-114\"}'\n```",
        "operationId": "CreatePaymentLink",
        "parameters": [
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreatePaymentLinkRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreatePaymentLinkRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreatePaymentLinkRequest"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1PaymentLink"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      },
      "get": {
        "tags": [
          "PaymentLinks"
        ],
        "summary": "Payment links, newest first.",
        "operationId": "ListPaymentLinks",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "1–100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "A `plink_` id from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customer",
            "in": "query",
            "description": "Optional `cus_` id to list only that customer's links.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1List_V1PaymentLink"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/payment_links/{id}": {
      "get": {
        "tags": [
          "PaymentLinks"
        ],
        "summary": "One payment link, by its `plink_` id.",
        "operationId": "GetPaymentLink",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1PaymentLink"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/payments": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Charge a token your page created in the customer's browser.",
        "description": "A card number never reaches this endpoint and there is no field that would accept one: tokenise the\ncard in the customer's browser against the gateway's own hosted fields, then send the token here.\nA value shaped like a card number is refused rather than forwarded.\n            \nIf the gateway asks for a 3-D Secure step, `transaction_url` comes back on the payment and you\nredirect the customer there.\n            \n```\ncurl https://paywithnal.com/v1/payments \\\n  -H \"Authorization: Bearer sk_test_…\" \\\n  -H \"Idempotency-Key: 1f0a…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"amount\":10000,\"currency\":\"SAR\",\"description\":\"Order 1024\",\n       \"source\":{\"type\":\"token\",\"token\":\"token_9fbc4d2e6a1b\"}}'\n````{\"type\":\"card\"}` — a card the customer saved earlier — is refused with\n`stored_card_not_chargeable`, and so is a token we already hold on file, which is the same card\nunder another name. A stored card is charged by Nalpay on the schedule of a subscription the customer\nagreed to, and by nothing a merchant can reach. To take money from a customer who is not at a keyboard,\nsend them a payment link.",
        "operationId": "CreatePayment",
        "parameters": [
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreatePaymentRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreatePaymentRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreatePaymentRequest"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Payment"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      },
      "get": {
        "tags": [
          "Payments"
        ],
        "summary": "Payments, newest first.",
        "description": "Cursor paging: read `has_more`, then pass the last object's id as `starting_after`. There are\nno page numbers and no offsets, so a list that is being written to while you walk it stays consistent.",
        "operationId": "ListPayments",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "1–100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "A `pay_` id from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customer",
            "in": "query",
            "description": "Optional `cus_` id to list only that customer's payments.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "payment_link",
            "in": "query",
            "description": "Optional `plink_` id to list only the payments made against that link.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1List_V1Payment"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/payments/{id}": {
      "get": {
        "tags": [
          "Payments"
        ],
        "summary": "One payment, by its `pay_` id.",
        "description": "An id from the other mode does not exist here: a test key cannot read a live payment.",
        "operationId": "GetPayment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Payment"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/payments/{id}/refunds": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Refund a payment, in full or in part.",
        "description": "Omit `amount` to refund everything still refundable. Partial refunds may be repeated until the\npayment is fully refunded.\n            \n```\ncurl https://paywithnal.com/v1/payments/pay_…/refunds \\\n  -H \"Authorization: Bearer sk_test_…\" -H \"Idempotency-Key: 2b7c…\" \\\n  -H \"Content-Type: application/json\" -d '{\"amount\":2500}'\n```",
        "operationId": "RefundPayment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateRefundRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateRefundRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateRefundRequest"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Refund"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/refunds/{id}": {
      "get": {
        "tags": [
          "Payments"
        ],
        "summary": "One refund, by its `ref_` id.",
        "operationId": "GetRefund",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Refund"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/subscriptions": {
      "post": {
        "tags": [
          "Subscriptions"
        ],
        "summary": "Create a subscription and get the link that starts it.",
        "description": "A subscription begins life as a link. Send the customer to `subscribe_url`; they pay the first\ncycle with 3-D Secure and agree to the recurring charge there, and that payment is what activates the\nsubscription and saves the card. Every renewal after it is charged by Nalpay.\n            \n```\ncurl https://paywithnal.com/v1/subscriptions \\\n  -H \"Authorization: Bearer sk_test_…\" -H \"Idempotency-Key: 7a3b…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"description\":\"Studio membership\",\"amount\":49900,\"currency\":\"SAR\",\"billing_period_days\":30}'\n```\n            \nName the customer with `customer` when you know them: a customer who has paid this merchant\nbefore is offered their saved card on that page, and starting takes one tap.\n            \n`source` and `consent` are refused, with `stored_card_not_chargeable`. A subscription\ncannot be started from a card the customer saved earlier: a card on file is charged by Nalpay on the\nschedule of an agreement made on our own subscribe page, and by nothing a merchant can reach. Starting\none on a merchant's assertion that the cardholder had agreed somewhere else is a charge nobody\npresent consented to, which is exactly what that rule is about.\n            \nA period is a number of days — `billing_period_days` is 30, 90, 180 or 365 — and each one starts\nwhere the last ended. So a monthly subscription renews every 30 days rather than on the same date each\nmonth, and its renewal date walks backwards through the calendar. There is no billing-day parameter.",
        "operationId": "CreateSubscription",
        "parameters": [
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateSubscriptionRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateSubscriptionRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateSubscriptionRequest"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Subscription"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      },
      "get": {
        "tags": [
          "Subscriptions"
        ],
        "summary": "Subscriptions, newest first.",
        "operationId": "ListSubscriptions",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "1–100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "A `sub_` id from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customer",
            "in": "query",
            "description": "Optional `cus_` id to list only that customer's subscriptions.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1List_V1Subscription"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/subscriptions/{id}": {
      "get": {
        "tags": [
          "Subscriptions"
        ],
        "summary": "One subscription, by its `sub_` id.",
        "operationId": "GetSubscription",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Subscription"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/subscriptions/{id}/cancel": {
      "post": {
        "tags": [
          "Subscriptions"
        ],
        "summary": "Stop a subscription, now or at the end of the paid period.",
        "description": "`at_period_end: true` lets the customer keep what they have already paid for and stops the next\n            renewal. `false` ends it immediately. A cancellation scheduled for the period end can be undone\n            with `resume`.",
        "operationId": "CancelSubscription",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CancelSubscriptionRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CancelSubscriptionRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/V1CancelSubscriptionRequest"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Subscription"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/subscriptions/{id}/resume": {
      "post": {
        "tags": [
          "Subscriptions"
        ],
        "summary": "Undo a cancellation that was scheduled for the end of the period.",
        "operationId": "ResumeSubscription",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Subscription"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/subscriptions/{id}/cycles": {
      "get": {
        "tags": [
          "Subscriptions"
        ],
        "summary": "Every charge attempt on this subscription, newest first.",
        "description": "One cycle per attempt, including retries, so a failed renewal and the retries that followed it are all\nvisible with their reasons.",
        "operationId": "ListSubscriptionCycles",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The subscription's `sub_` id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1–100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "A `cyc_` id from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1List_V1Cycle"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/test/subscriptions/{id}/advance": {
      "post": {
        "tags": [
          "Test mode"
        ],
        "summary": "Run this subscription's next cycle immediately. Test keys only.",
        "description": "Brings the next charge forward to now and runs the real billing runner for this one subscription — the\nsame code the nightly job executes. Without it, recurring billing could only be tested by waiting a\nmonth, which means it would not be tested.\n            \nA live key is refused with `test_mode_only`. The subscription must have had its first payment.\n            \n```\ncurl -X POST https://paywithnal.com/v1/test/subscriptions/sub_…/advance \\\n  -H \"Authorization: Bearer sk_test_…\" -H \"Idempotency-Key: 91cd…\"\n```",
        "operationId": "AdvanceSubscription",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1TestAdvance"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/test/payment_links/{id}/pay": {
      "post": {
        "tags": [
          "Test mode"
        ],
        "summary": "Pay this link, in test mode, without a browser. Test keys only.",
        "description": "Cards are tokenised in the browser against the payment provider's own fields, which means an\nintegration built entirely from a server could be finished but never demonstrated: the last step, a\npayment landing and a webhook arriving, needed a person with a card form. This is that step, for test\nmode.\n            \nIt does not call the payment provider. It records a paid payment through the same ledger a real one\ngoes through, so everything downstream is genuinely exercised rather than mimicked: the link settles\nunder its own exact-amount rule, the fee is priced by the real schedule, the payment appears in\n`GET /v1/payments` and in the dashboard, `payment_paid` is delivered to your registered test\nendpoints signed with their real secret, and the payment refunds like any other.\n            \n**The payment is marked as simulated and stays marked.** `simulated` is `true` on the\npayment object and in the webhook body, `source.message` says so, `source.last4` is\n`0000`, and the row itself carries the flag. Nothing here is meant to be mistaken for money.\n            \nThere is no `amount` field: the amount and currency are the link's own, so a simulated payment\nsettles a link on exactly the same terms a real one does. A link that is already paid, expired or\ncancelled is refused. A live key is refused with `test_mode_only` — there is no route by which\nNalpay records a live payment that did not happen.\n            \n```\ncurl -X POST https://paywithnal.com/v1/test/payment_links/plink_…/pay \\\n  -H \"Authorization: Bearer sk_test_…\" -H \"Idempotency-Key: 6b21…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"outcome\":\"paid\",\"card_brand\":\"visa\"}'\n```\n            \nSend `{\"outcome\":\"failed\"}` to get exactly what a real decline produces — a recorded failed\npayment, a `payment_failed` webhook, and a link that is still open — so you can build the failure\npath deliberately instead of hoping.",
        "operationId": "PayPaymentLink",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The link's `plink_` id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "requestBody": {
          "description": "Optional. Defaults to a `paid` outcome on a `visa` label.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1TestPayRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/V1TestPayRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/V1TestPayRequest"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1TestPayment"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/webhook_endpoints": {
      "post": {
        "tags": [
          "WebhookEndpoints"
        ],
        "summary": "Register a place to receive events.",
        "description": "The response is the only place, besides `rotate_secret`, where the signing secret appears. Store\nit now: it is not recoverable afterwards, and a lost secret means a rotation and a redeploy.\n            \nVerify a delivery by taking `t` and `v1` out of the `Nalpay-Signature` header, computing\n`HMAC_SHA256(secret, \"{t}.{raw body}\")` over the <b>raw</b> body — before any JSON parsing or\nre-serialisation — comparing it to `v1` in constant time, and rejecting anything whose `t` is\nmore than five minutes from your own clock. Dedupe on the `Nalpay-Delivery` header, which carries\nthe same `evt_` id on every retry.\n            \nThe endpoint belongs to the mode of the key that created it, permanently. A test key's endpoint never\nreceives a live event, and there is no way to move one between modes.\n            \n```\ncurl https://paywithnal.com/v1/webhook_endpoints \\\n  -H \"Authorization: Bearer sk_test_…\" -H \"Idempotency-Key: 8c1d…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\":\"https://example.com/webhooks/nalpay\",\"enabled_events\":[\"payment_paid\",\"payment_failed\"]}'\n```",
        "operationId": "CreateWebhookEndpoint",
        "parameters": [
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateWebhookEndpointRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateWebhookEndpointRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/V1CreateWebhookEndpointRequest"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1WebhookEndpoint"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      },
      "get": {
        "tags": [
          "WebhookEndpoints"
        ],
        "summary": "Your endpoints in this mode, newest first.",
        "operationId": "ListWebhookEndpoints",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "1–100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "A `we_` id from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1List_V1WebhookEndpoint"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/webhook_endpoints/{id}": {
      "get": {
        "tags": [
          "WebhookEndpoints"
        ],
        "summary": "One endpoint, by its `we_` id. Never includes the secret.",
        "operationId": "GetWebhookEndpoint",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1WebhookEndpoint"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      },
      "patch": {
        "tags": [
          "WebhookEndpoints"
        ],
        "summary": "Change an endpoint's URL, description, event selection, or whether it is enabled.",
        "description": "Only the fields you send are changed. Disabling an endpoint also cancels the retries already queued for\nit; enabling one clears an automatic disable and its failure count.",
        "operationId": "UpdateWebhookEndpoint",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1UpdateWebhookEndpointRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/V1UpdateWebhookEndpointRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/V1UpdateWebhookEndpointRequest"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1WebhookEndpoint"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      },
      "delete": {
        "tags": [
          "WebhookEndpoints"
        ],
        "summary": "Remove an endpoint.",
        "description": "Deliveries still waiting to be retried for it are dropped. Past deliveries stay in the log. Returns the\nendpoint as it was at the moment it was removed.",
        "operationId": "DeleteWebhookEndpoint",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1WebhookEndpoint"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    },
    "/v1/webhook_endpoints/{id}/rotate_secret": {
      "post": {
        "tags": [
          "WebhookEndpoints"
        ],
        "summary": "Issue a new signing secret for this endpoint.",
        "description": "The response carries the new secret in full — the last time it is ever shown. The old secret stops\nverifying immediately, so deploy the new one before rotating, or accept a gap in which your receiver\nrejects deliveries.",
        "operationId": "RotateWebhookEndpointSecret",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Nalpay-Version",
            "in": "header",
            "description": "Optional dated version to pin, e.g. 2026-09-04. Echoed back on the response.",
            "schema": {
              "type": "string",
              "example": "2026-09-04"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Any unique string, a UUID is ideal. The same key with the same body replays the stored response instead of doing the work twice; with a different body it is refused.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ErrorEnvelope"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1WebhookEndpoint"
                }
              }
            }
          }
        },
        "security": [
          {
            "SecretKey": [ ]
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "V1Account": {
        "required": [
          "api_version",
          "business_name",
          "capabilities",
          "id",
          "kyc_status",
          "livemode",
          "mode"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `account`.",
            "nullable": true,
            "example": "account"
          },
          "id": {
            "type": "string",
            "description": "The merchant this key belongs to.",
            "nullable": true,
            "example": "acct_3a23798db89f6de771366f9cc2760331"
          },
          "business_name": {
            "type": "string",
            "description": "The merchant's business name, as it appears to their customers.",
            "nullable": true,
            "example": "Nasaak Salon"
          },
          "business_name_ar": {
            "type": "string",
            "description": "The same name in Arabic, when the merchant has given one.",
            "nullable": true,
            "example": "صالون نسّاك"
          },
          "mode": {
            "$ref": "#/components/schemas/V1Mode"
          },
          "livemode": {
            "type": "boolean",
            "description": "Convenience mirror of `mode`: true only for a live key.",
            "example": false
          },
          "kyc_status": {
            "$ref": "#/components/schemas/V1KycStatus"
          },
          "capabilities": {
            "$ref": "#/components/schemas/V1Capabilities"
          },
          "api_version": {
            "type": "string",
            "description": "The version this response was served under (see the `Nalpay-Version` header).",
            "nullable": true,
            "example": "2026-09-04"
          }
        },
        "additionalProperties": false,
        "description": "Who the key belongs to and what it is allowed to do."
      },
      "V1CancelSubscriptionRequest": {
        "type": "object",
        "properties": {
          "at_period_end": {
            "type": "boolean",
            "description": "True to keep the subscription running until the period the customer already paid for ends, then stop.\nFalse (the default) cancels immediately.",
            "example": true
          }
        },
        "additionalProperties": false
      },
      "V1Capabilities": {
        "required": [
          "can_charge",
          "can_create_live_keys",
          "subscriptions_enabled"
        ],
        "type": "object",
        "properties": {
          "can_charge": {
            "type": "boolean",
            "description": "Whether payments, refunds, paymentLinks and subscriptions may be created at all.",
            "example": true
          },
          "can_create_live_keys": {
            "type": "boolean",
            "description": "Whether the merchant's verification is far enough along to issue live keys.",
            "example": true
          },
          "subscriptions_enabled": {
            "type": "boolean",
            "description": "Whether recurring billing is turned on for this merchant.",
            "example": false
          }
        },
        "additionalProperties": false,
        "description": "What this account can do right now. Check it before building a flow around something it cannot."
      },
      "V1Card": {
        "required": [
          "created_at",
          "customer",
          "id",
          "status"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `card`.",
            "nullable": true,
            "example": "card"
          },
          "id": {
            "type": "string",
            "description": "The card id, which is what you charge.",
            "nullable": true,
            "example": "card_3a237a6b496470e726bb2a412990960d"
          },
          "customer": {
            "type": "string",
            "description": "The customer this card belongs to.",
            "nullable": true,
            "example": "cus_3a237a5653eb9b2fe4a04c2b1db8089b"
          },
          "brand": {
            "type": "string",
            "description": "The card network.",
            "nullable": true,
            "example": "mada"
          },
          "last4": {
            "type": "string",
            "description": "The last four digits.",
            "nullable": true,
            "example": "1111"
          },
          "exp_month": {
            "type": "integer",
            "description": "1–12.",
            "format": "int32",
            "nullable": true,
            "example": 12
          },
          "exp_year": {
            "type": "integer",
            "description": "Four digits.",
            "format": "int32",
            "nullable": true,
            "example": 2029
          },
          "name": {
            "type": "string",
            "description": "The cardholder name.",
            "nullable": true,
            "example": "Sara Ali"
          },
          "status": {
            "$ref": "#/components/schemas/V1CardStatus"
          },
          "created_at": {
            "type": "string",
            "description": "When the card was stored. UTC, ISO 8601.",
            "format": "date-time",
            "example": "2026-09-03T16:45:44Z"
          }
        },
        "additionalProperties": false,
        "description": "A card stored at the gateway. Nalpay holds a token; the number never reaches our servers."
      },
      "V1CardStatus": {
        "enum": [
          "initiated",
          "active",
          "inactive",
          "expired"
        ],
        "type": "string",
        "description": "Whether a stored card can still be charged.",
        "x-enumNames": [
          "initiated",
          "active",
          "inactive",
          "expired"
        ]
      },
      "V1ConsentRequest": {
        "type": "object",
        "properties": {
          "accepted_at": {
            "type": "string",
            "description": "When the cardholder agreed, UTC, ISO 8601. It must be in the past: it is a record of something that\nhappened, not a promise about something that will.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-09-04T09:12:00Z"
          },
          "reference": {
            "type": "string",
            "description": "Your own pointer to the agreement: a terms version, the id of a signed form, an order number —\nwhatever you would produce if the charge were disputed. Required, at most 200 characters. It is stored\nas you send it and returned on the subscription.",
            "nullable": true,
            "example": "terms-v4:order-1024"
          }
        },
        "additionalProperties": false,
        "description": "What you were telling us about an agreement you had collected yourself, so that a card on file could be\ncharged on it. <b>Not accepted any more.</b>\n            \nThe card schemes require the cardholder to have agreed to a recurring charge before the first one made on\ntheir behalf, and require whoever took that agreement to be able to produce it in a dispute. Nalpay takes\nthat agreement on its own subscribe page and holds the evidence. An assertion in a request body is not\nevidence — it is a timestamp the caller chose and a reference we can neither read nor check — and it is\nnot what stands between a customer's saved card and a charge."
      },
      "V1ConsentSource": {
        "enum": [
          "nalpay_hosted_page",
          "merchant"
        ],
        "type": "string",
        "description": "Who took the cardholder's agreement to a recurring charge, and therefore who can produce it.",
        "x-enumNames": [
          "nalpay_hosted_page",
          "merchant"
        ]
      },
      "V1CreateCustomerRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The customer's name.",
            "nullable": true,
            "example": "Sara Ali"
          },
          "email": {
            "type": "string",
            "description": "Stored lower-cased. Give at least one of name, email and phone.",
            "nullable": true,
            "example": "sara@example.com"
          },
          "phone": {
            "type": "string",
            "description": "Saudi mobile number in any common format; stored as international digits (966…), no \"+\".",
            "nullable": true,
            "example": "0512345678"
          }
        },
        "additionalProperties": false
      },
      "V1CreatePaymentLinkRequest": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "integer",
            "description": "Minor units, 1000 to 100000000 — ten riyals to one million riyals.",
            "format": "int64",
            "example": 25000
          },
          "currency": {
            "type": "string",
            "description": "Must be `SAR`, which is also the default: Nalpay settles in Saudi riyals only.",
            "nullable": true,
            "example": "SAR"
          },
          "description": {
            "type": "string",
            "description": "What the link is for. Required, 10 to 200 characters. Shown on the hosted page.",
            "nullable": true,
            "example": "Invoice 2026-114"
          },
          "customer": {
            "type": "string",
            "description": "Pre-fill the page for a known customer.",
            "nullable": true,
            "example": "cus_3a237a5653eb9b2fe4a04c2b1db8089b"
          },
          "expires_at": {
            "type": "string",
            "description": "UTC. After this instant the link stops being payable. Must be in the future.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-10-01T00:00:00Z"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Your own key/value pairs. Keys may not begin with `nalpay_`.",
            "nullable": true,
            "example": {
              "paymentLink_no": "2026-114"
            }
          },
          "multiple_payers": {
            "type": "boolean",
            "description": "Set to `true` for a link anyone holding it may pay, each of them paying `amount` in full and\ngiving their name on the hosted page. Such a link stays open until `expires_at` or until you\ncancel it, and cannot also name a `customer`. Defaults to `false`: one link, one payer,\nclosed by their payment.",
            "nullable": true,
            "example": false
          }
        },
        "additionalProperties": false
      },
      "V1CreatePaymentRequest": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "integer",
            "description": "Minor units, 1 to 100000000. 10000 is 100.00 SAR; the ceiling is one million riyals.",
            "format": "int64",
            "example": 1500
          },
          "currency": {
            "type": "string",
            "description": "Must be `SAR`, which is also the default: Nalpay settles in Saudi riyals only.",
            "nullable": true,
            "example": "SAR"
          },
          "description": {
            "type": "string",
            "description": "What the customer is paying for. Required, at most 200 characters.",
            "nullable": true,
            "example": "Order 1024"
          },
          "source": {
            "$ref": "#/components/schemas/V1PaymentSourceRequest"
          },
          "customer": {
            "type": "string",
            "description": "Who is paying, as `cus_…`. Optional, and the payment is attributed to them.",
            "nullable": true,
            "example": "cus_3a237a5653eb9b2fe4a04c2b1db8089b"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Your own key/value pairs, echoed back on the payment. Keys may not begin with `nalpay_`.",
            "nullable": true,
            "example": {
              "order_id": "1024"
            }
          },
          "callback_url": {
            "type": "string",
            "description": "Where the gateway sends the cardholder if it asks for a 3-D Secure step. Only used when the gateway\nreturns a `transaction_url`.",
            "nullable": true,
            "example": "https://merchant.example.com/orders/1024/done"
          }
        },
        "additionalProperties": false
      },
      "V1CreateRefundRequest": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "integer",
            "description": "Minor units, 1 to 100000000. Omit to refund everything that is still refundable.",
            "format": "int64",
            "nullable": true,
            "example": 500
          },
          "reason": {
            "type": "string",
            "description": "Why, in your own words, up to 200 characters. Kept on the refund and echoed back; nothing reads it,\nand nothing is inferred from it. Optional.",
            "nullable": true,
            "example": "Customer cancelled the order"
          }
        },
        "additionalProperties": false
      },
      "V1CreateSubscriptionRequest": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "description": "What the customer is subscribing to. Required, one line, at most 200 characters.",
            "nullable": true,
            "example": "Studio membership"
          },
          "amount": {
            "type": "integer",
            "description": "Charged every period, in minor units, 1 to 100000000.",
            "format": "int64",
            "example": 49900
          },
          "currency": {
            "type": "string",
            "description": "Must be `SAR`, which is also the default: Nalpay settles in Saudi riyals only.",
            "nullable": true,
            "example": "SAR"
          },
          "billing_period_days": {
            "type": "integer",
            "description": "How many days one period lasts: 30 (monthly), 90 (quarterly), 180 (twice a year) or 365 (yearly).\nDefaults to 30. A period is a count of days, so the renewal date is 30 days after the last one rather\nthan the same date each month.",
            "format": "int32",
            "nullable": true,
            "example": 30
          },
          "customer": {
            "type": "string",
            "description": "Optional; otherwise the customer identifies themselves on the subscribe page.",
            "nullable": true,
            "example": "cus_3a237a5653eb9b2fe4a04c2b1db8089b"
          },
          "source": {
            "$ref": "#/components/schemas/V1SubscriptionSourceRequest"
          },
          "consent": {
            "$ref": "#/components/schemas/V1ConsentRequest"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Your own key/value pairs. Keys may not begin with `nalpay_`.",
            "nullable": true,
            "example": {
              "plan": "studio"
            }
          }
        },
        "additionalProperties": false
      },
      "V1CreateWebhookEndpointRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Where to post. Required. Live mode requires `https` and a publicly routable host.",
            "nullable": true,
            "example": "https://merchant.example.com/webhooks/nalpay"
          },
          "description": {
            "type": "string",
            "description": "Your own label, at most 200 characters.",
            "nullable": true,
            "example": "Order service"
          },
          "enabled_events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Which event types to send. Required and non-empty; an unknown type is refused.",
            "nullable": true,
            "example": [
              "payment_paid",
              "payment_failed"
            ]
          }
        },
        "additionalProperties": false
      },
      "V1Customer": {
        "required": [
          "created_at",
          "id"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `customer`.",
            "nullable": true,
            "example": "customer"
          },
          "id": {
            "type": "string",
            "description": "The customer id.",
            "nullable": true,
            "example": "cus_3a237a5653eb9b2fe4a04c2b1db8089b"
          },
          "name": {
            "type": "string",
            "description": "The customer's name, if you gave one.",
            "nullable": true,
            "example": "Sara Ali"
          },
          "email": {
            "type": "string",
            "description": "Lower-cased.",
            "nullable": true,
            "example": "sara@example.com"
          },
          "phone": {
            "type": "string",
            "description": "International digits, country code first, no \"+\".",
            "nullable": true,
            "example": "966512345678"
          },
          "created_at": {
            "type": "string",
            "description": "When the customer was first seen. UTC, ISO 8601.",
            "format": "date-time",
            "example": "2026-09-04T08:09:05Z"
          }
        },
        "additionalProperties": false,
        "description": "One of the merchant's end customers: the person who pays."
      },
      "V1Cycle": {
        "required": [
          "attempt",
          "created_at",
          "id",
          "kind",
          "period_end",
          "period_start",
          "scheduled_at",
          "status",
          "subscription"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `cycle`.",
            "nullable": true,
            "example": "cycle"
          },
          "id": {
            "type": "string",
            "description": "The cycle id.",
            "nullable": true,
            "example": "cyc_3a237e839d9781a5000e9a68aa1b88e1"
          },
          "subscription": {
            "type": "string",
            "description": "The subscription this attempt belongs to.",
            "nullable": true,
            "example": "sub_3a237e31f93b606afe649985ba8b9cb1"
          },
          "status": {
            "$ref": "#/components/schemas/V1CycleStatus"
          },
          "kind": {
            "$ref": "#/components/schemas/V1CycleKind"
          },
          "attempt": {
            "type": "integer",
            "description": "0 for a customer-initiated payment, 1 for the scheduled charge, then one per retry.",
            "format": "int32",
            "example": 1
          },
          "period_start": {
            "type": "string",
            "description": "Start of the period this attempt buys. UTC.",
            "format": "date-time",
            "example": "2026-11-04T06:00:00Z"
          },
          "period_end": {
            "type": "string",
            "description": "End of the period this attempt buys. UTC.",
            "format": "date-time",
            "example": "2026-12-04T06:00:00Z"
          },
          "scheduled_at": {
            "type": "string",
            "description": "When the attempt was due. UTC.",
            "format": "date-time",
            "example": "2026-11-04T06:00:00Z"
          },
          "charged_at": {
            "type": "string",
            "description": "When the attempt concluded, or null while it has not. UTC.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-11-04T06:00:03Z"
          },
          "payment": {
            "type": "string",
            "description": "The payment this attempt produced, or null when it never reached the gateway.",
            "nullable": true,
            "example": "pay_3912753d1fb3580d8678ae33d858968c"
          },
          "failure_code": {
            "type": "string",
            "description": "Short reason the attempt failed or was skipped.",
            "nullable": true,
            "example": "declined"
          },
          "failure_message": {
            "type": "string",
            "description": "The longer form of the same, when the gateway gave one.",
            "nullable": true,
            "example": "INSUFFICIENT FUNDS"
          },
          "created_at": {
            "type": "string",
            "description": "When the cycle row was created. UTC, ISO 8601.",
            "format": "date-time",
            "example": "2026-11-04T06:00:01Z"
          }
        },
        "additionalProperties": false,
        "description": "One charge attempt on a subscription: what was tried, when, and what came of it."
      },
      "V1CycleKind": {
        "enum": [
          "first_payment",
          "renewal",
          "retry",
          "recovery",
          "payoff"
        ],
        "type": "string",
        "description": "Why a charge attempt happened.",
        "x-enumNames": [
          "first_payment",
          "renewal",
          "retry",
          "recovery",
          "payoff"
        ]
      },
      "V1CycleStatus": {
        "enum": [
          "scheduled",
          "charging",
          "paid",
          "failed",
          "skipped"
        ],
        "type": "string",
        "description": "Where one charge attempt got to.",
        "x-enumNames": [
          "scheduled",
          "charging",
          "paid",
          "failed",
          "skipped"
        ]
      },
      "V1ErrorBody": {
        "required": [
          "code",
          "doc_url",
          "message",
          "type"
        ],
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Branch on this. One of `invalid_request_error`, `authentication_error`,\n`permission_error`, `not_found_error`, `rate_limit_error` and `api_error`.",
            "nullable": true,
            "example": "invalid_request_error"
          },
          "code": {
            "type": "string",
            "description": "A stable, machine-readable reason. Quote this to support.",
            "nullable": true,
            "example": "parameter_invalid"
          },
          "message": {
            "type": "string",
            "description": "A sentence a developer can read. Never a stack trace, never an internal message.",
            "nullable": true,
            "example": "amount must be a positive integer in the currency's minor units (halalas for SAR), and at most 100000000 — one million riyals."
          },
          "param": {
            "type": "string",
            "description": "The request field at fault, in the same snake_case the request used. Null when not field-specific.",
            "nullable": true,
            "example": "amount"
          },
          "doc_url": {
            "type": "string",
            "description": "Where this code is documented.",
            "nullable": true,
            "example": "https://paywithnal.com/docs/errors/parameter_invalid"
          }
        },
        "additionalProperties": false,
        "description": "What went wrong, in the one shape every Nalpay error uses."
      },
      "V1ErrorEnvelope": {
        "type": "object",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/V1ErrorBody"
          }
        },
        "additionalProperties": false,
        "description": "The body of every non-2xx `/v1` response. One shape, always, so a client parses errors once."
      },
      "V1Event": {
        "required": [
          "created",
          "data",
          "id",
          "livemode",
          "type"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `event`.",
            "nullable": true,
            "example": "event"
          },
          "id": {
            "type": "string",
            "description": "The event id, repeated in the `Nalpay-Delivery` header of every attempt.",
            "nullable": true,
            "example": "evt_3a237e835a0d5214f7c94b4b58739157"
          },
          "type": {
            "type": "string",
            "description": "What happened, for example `payment_paid`.",
            "nullable": true,
            "example": "payment_paid"
          },
          "created": {
            "type": "string",
            "description": "When it happened. UTC, ISO 8601.",
            "format": "date-time",
            "example": "2026-09-04T12:13:24Z"
          },
          "livemode": {
            "type": "boolean",
            "description": "True on a live event. Only endpoints of the same mode ever receive it.",
            "example": false
          },
          "data": {
            "description": "The object the event is about, exactly as `/v1` publishes it."
          }
        },
        "additionalProperties": false,
        "description": "Something that happened in your account. The `data` of an event is the same object the corresponding\n`GET` would return, read when the event was recorded — a moment after the change, not at it, because\nthe payload is built on a worker rather than on the request thread. It is then stored rather than\nre-rendered, so a resend posts the same bytes as the original delivery.\n            \nTwo consequences worth designing around. A change that is immediately followed by another can be\ndescribed by the later state: a payment refunded within the second can arrive as\n`type: payment_paid` with `data.status: refunded`, so branch on `type` and treat\n`data` as context. And because several of our own paths can observe the same change at once, the\nsame transition can occasionally produce two events with different ids — dedupe on\n`data.id` plus `type` as well as on `Nalpay-Delivery`."
      },
      "V1KycStatus": {
        "enum": [
          "unverified",
          "in_review",
          "pending_documents",
          "approved",
          "rejected"
        ],
        "type": "string",
        "description": "Where a merchant stands with business verification. Live money needs `approved`.",
        "x-enumNames": [
          "unverified",
          "in_review",
          "pending_documents",
          "approved",
          "rejected"
        ]
      },
      "V1List_V1Card": {
        "required": [
          "data",
          "has_more"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "nullable": true,
            "example": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1Card"
            },
            "description": "The objects on this page, newest first.",
            "nullable": true
          },
          "has_more": {
            "type": "boolean",
            "description": "True when more objects exist after the last one in `data`. Pass that object's id as\n`starting_after` to get the next page.",
            "example": true
          }
        },
        "additionalProperties": false,
        "description": "A page of a list. Every list endpoint returns this shape, so paging is written once in a client and reused."
      },
      "V1List_V1Customer": {
        "required": [
          "data",
          "has_more"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "nullable": true,
            "example": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1Customer"
            },
            "description": "The objects on this page, newest first.",
            "nullable": true
          },
          "has_more": {
            "type": "boolean",
            "description": "True when more objects exist after the last one in `data`. Pass that object's id as\n`starting_after` to get the next page.",
            "example": true
          }
        },
        "additionalProperties": false,
        "description": "A page of a list. Every list endpoint returns this shape, so paging is written once in a client and reused."
      },
      "V1List_V1Cycle": {
        "required": [
          "data",
          "has_more"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "nullable": true,
            "example": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1Cycle"
            },
            "description": "The objects on this page, newest first.",
            "nullable": true
          },
          "has_more": {
            "type": "boolean",
            "description": "True when more objects exist after the last one in `data`. Pass that object's id as\n`starting_after` to get the next page.",
            "example": true
          }
        },
        "additionalProperties": false,
        "description": "A page of a list. Every list endpoint returns this shape, so paging is written once in a client and reused."
      },
      "V1List_V1Event": {
        "required": [
          "data",
          "has_more"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "nullable": true,
            "example": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1Event"
            },
            "description": "The objects on this page, newest first.",
            "nullable": true
          },
          "has_more": {
            "type": "boolean",
            "description": "True when more objects exist after the last one in `data`. Pass that object's id as\n`starting_after` to get the next page.",
            "example": true
          }
        },
        "additionalProperties": false,
        "description": "A page of a list. Every list endpoint returns this shape, so paging is written once in a client and reused."
      },
      "V1List_V1Payment": {
        "required": [
          "data",
          "has_more"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "nullable": true,
            "example": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1Payment"
            },
            "description": "The objects on this page, newest first.",
            "nullable": true
          },
          "has_more": {
            "type": "boolean",
            "description": "True when more objects exist after the last one in `data`. Pass that object's id as\n`starting_after` to get the next page.",
            "example": true
          }
        },
        "additionalProperties": false,
        "description": "A page of a list. Every list endpoint returns this shape, so paging is written once in a client and reused."
      },
      "V1List_V1PaymentLink": {
        "required": [
          "data",
          "has_more"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "nullable": true,
            "example": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1PaymentLink"
            },
            "description": "The objects on this page, newest first.",
            "nullable": true
          },
          "has_more": {
            "type": "boolean",
            "description": "True when more objects exist after the last one in `data`. Pass that object's id as\n`starting_after` to get the next page.",
            "example": true
          }
        },
        "additionalProperties": false,
        "description": "A page of a list. Every list endpoint returns this shape, so paging is written once in a client and reused."
      },
      "V1List_V1Subscription": {
        "required": [
          "data",
          "has_more"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "nullable": true,
            "example": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1Subscription"
            },
            "description": "The objects on this page, newest first.",
            "nullable": true
          },
          "has_more": {
            "type": "boolean",
            "description": "True when more objects exist after the last one in `data`. Pass that object's id as\n`starting_after` to get the next page.",
            "example": true
          }
        },
        "additionalProperties": false,
        "description": "A page of a list. Every list endpoint returns this shape, so paging is written once in a client and reused."
      },
      "V1List_V1WebhookDelivery": {
        "required": [
          "data",
          "has_more"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "nullable": true,
            "example": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1WebhookDelivery"
            },
            "description": "The objects on this page, newest first.",
            "nullable": true
          },
          "has_more": {
            "type": "boolean",
            "description": "True when more objects exist after the last one in `data`. Pass that object's id as\n`starting_after` to get the next page.",
            "example": true
          }
        },
        "additionalProperties": false,
        "description": "A page of a list. Every list endpoint returns this shape, so paging is written once in a client and reused."
      },
      "V1List_V1WebhookEndpoint": {
        "required": [
          "data",
          "has_more"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `list`.",
            "nullable": true,
            "example": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1WebhookEndpoint"
            },
            "description": "The objects on this page, newest first.",
            "nullable": true
          },
          "has_more": {
            "type": "boolean",
            "description": "True when more objects exist after the last one in `data`. Pass that object's id as\n`starting_after` to get the next page.",
            "example": true
          }
        },
        "additionalProperties": false,
        "description": "A page of a list. Every list endpoint returns this shape, so paging is written once in a client and reused."
      },
      "V1Mode": {
        "enum": [
          "live",
          "test"
        ],
        "type": "string",
        "description": "The status vocabulary `/v1` publishes. These are deliberately their own enums rather than the domain's:\nthe wire value of every member is part of the promise, so a rename inside the domain cannot reach a caller.\nEvery member serializes as its name in lower snake_case, pinned by a test.",
        "x-enumNames": [
          "live",
          "test"
        ]
      },
      "V1Payment": {
        "required": [
          "amount",
          "amount_refunded",
          "created_at",
          "currency",
          "fee",
          "id",
          "metadata",
          "net",
          "simulated",
          "source",
          "status"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `payment`.",
            "nullable": true,
            "example": "payment"
          },
          "id": {
            "type": "string",
            "description": "The payment id.",
            "nullable": true,
            "example": "pay_3a237e835a0d5214f7c94b4b58739157"
          },
          "amount": {
            "type": "integer",
            "description": "Minor units. 10000 is 100.00 SAR.",
            "format": "int64",
            "example": 1500
          },
          "currency": {
            "type": "string",
            "description": "Three-letter ISO code, upper case. `SAR` today.",
            "nullable": true,
            "example": "SAR"
          },
          "status": {
            "$ref": "#/components/schemas/V1PaymentStatus"
          },
          "simulated": {
            "type": "boolean",
            "description": "True when no money moved and none was ever going to: no card was charged and nothing reached the\npayment provider. Two things produce it, both test-mode only — a payment recorded by\n`POST /v1/test/payment_links/:id/pay`, and the synthetic payment the MCP `simulate_event` tool\nputs inside an event when you do not name a real one. That second one exists only in the event and is\nnot readable at `GET /v1/payments/:id`.\n            \nAlways false on a live payment — there is no route that can set it in live mode — and false on a test\npayment that really was made through the hosted page. Look at `source.message` too: a simulation\nsays so there as well.",
            "example": false
          },
          "description": {
            "type": "string",
            "description": "What the customer paid for.",
            "nullable": true,
            "example": "Order 1024"
          },
          "source": {
            "$ref": "#/components/schemas/V1PaymentSourceInfo"
          },
          "amount_refunded": {
            "type": "integer",
            "description": "Total refunded so far, in minor units. Zero on a payment that has not been refunded.",
            "format": "int64",
            "example": 0
          },
          "fee": {
            "type": "integer",
            "description": "What Nalpay charges the merchant for this payment, VAT included, in minor units.",
            "format": "int64",
            "example": 166
          },
          "net": {
            "type": "integer",
            "description": "What the merchant is due: amount minus refunds minus fee. Minor units.",
            "format": "int64",
            "example": 1334
          },
          "customer": {
            "type": "string",
            "description": "The customer this payment is attributed to, or null.",
            "nullable": true,
            "example": "cus_3a237a5653eb9b2fe4a04c2b1db8089b"
          },
          "payment_link": {
            "type": "string",
            "description": "The payment link this settled, or null.",
            "nullable": true,
            "example": "plink_3a237e83027c48e3b23b32c94ac80b06"
          },
          "subscription": {
            "type": "string",
            "description": "The subscription this cycle belongs to, or null.",
            "nullable": true,
            "example": "sub_3a237e31f93b606afe649985ba8b9cb1"
          },
          "transaction_url": {
            "type": "string",
            "description": "Where to send the cardholder when the gateway asks for a 3-D Secure step. Usually null.",
            "nullable": true,
            "example": "https://api.gateway.example/v1/transaction_auths/9f3c"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Whatever you sent on the request. Nalpay's own `nalpay_*` keys are stripped out.",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "description": "When the payment was created. UTC, ISO 8601.",
            "format": "date-time",
            "example": "2026-09-04T12:13:24Z"
          }
        },
        "additionalProperties": false,
        "description": "A charge. Created by `POST /v1/payments`, by a payment link, or by a subscription renewal."
      },
      "V1PaymentLink": {
        "required": [
          "amount",
          "amount_paid",
          "created_at",
          "currency",
          "description",
          "id",
          "metadata",
          "multiple_payers",
          "status",
          "url"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `payment_link`.",
            "nullable": true,
            "example": "payment_link"
          },
          "id": {
            "type": "string",
            "description": "The payment link id.",
            "nullable": true,
            "example": "plink_3a237e83027c48e3b23b32c94ac80b06"
          },
          "status": {
            "$ref": "#/components/schemas/V1PaymentLinkStatus"
          },
          "amount": {
            "type": "integer",
            "description": "Minor units.",
            "format": "int64",
            "example": 25000
          },
          "currency": {
            "type": "string",
            "description": "Three-letter ISO code, upper case.",
            "nullable": true,
            "example": "SAR"
          },
          "description": {
            "type": "string",
            "description": "What the link is for. Shown on the hosted page.",
            "nullable": true,
            "example": "Invoice 2026-114"
          },
          "url": {
            "type": "string",
            "description": "The hosted page to send the customer to. Safe to share as-is.",
            "nullable": true,
            "example": "https://paywithnal.com/pay/d2f493c1ea5d"
          },
          "customer": {
            "type": "string",
            "description": "The customer the link is for, or null.",
            "nullable": true,
            "example": "cus_3a237a5653eb9b2fe4a04c2b1db8089b"
          },
          "expires_at": {
            "type": "string",
            "description": "After this instant the link stops being payable. Null means it never expires.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-10-01T00:00:00Z"
          },
          "amount_paid": {
            "type": "integer",
            "description": "What has been received against this link so far, in minor units, net of refunds. It reaches\n`amount` when the link is paid. `GET /v1/payments?payment_link=…` lists the payments behind\nit.",
            "format": "int64",
            "example": 25000
          },
          "multiple_payers": {
            "type": "boolean",
            "description": "True when anyone holding the link may pay it, each paying `amount` in full. Such a link stays\n`open` until it expires or is cancelled, however much has been paid against it.",
            "example": false
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Your own key/value pairs, echoed back.",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "description": "When the link was created. UTC, ISO 8601.",
            "format": "date-time",
            "example": "2026-09-04T12:13:01Z"
          }
        },
        "additionalProperties": false,
        "description": "A payment link: a hosted page that collects one payment."
      },
      "V1PaymentLinkStatus": {
        "enum": [
          "open",
          "paid",
          "failed",
          "canceled",
          "expired",
          "refunded"
        ],
        "type": "string",
        "description": "Where a payment link stands.",
        "x-enumNames": [
          "open",
          "paid",
          "failed",
          "canceled",
          "expired",
          "refunded"
        ]
      },
      "V1PaymentSource": {
        "enum": [
          "card",
          "token",
          "apple_pay",
          "samsung_pay",
          "stc_pay"
        ],
        "type": "string",
        "description": "How a payment was funded.",
        "x-enumNames": [
          "card",
          "token",
          "apple_pay",
          "samsung_pay",
          "stc_pay"
        ]
      },
      "V1PaymentSourceInfo": {
        "type": "object",
        "properties": {
          "type": {
            "$ref": "#/components/schemas/V1PaymentSource"
          },
          "brand": {
            "type": "string",
            "description": "The card network.",
            "nullable": true,
            "example": "visa"
          },
          "last4": {
            "type": "string",
            "description": "The last four digits of the card.",
            "nullable": true,
            "example": "1111"
          },
          "name": {
            "type": "string",
            "description": "The cardholder name the gateway recorded.",
            "nullable": true,
            "example": "Sara Ali"
          },
          "message": {
            "type": "string",
            "description": "The gateway's own words about the outcome. Useful on a decline.",
            "nullable": true,
            "example": "APPROVED"
          },
          "reference_number": {
            "type": "string",
            "description": "The gateway's reference number for a settled card transaction.",
            "nullable": true,
            "example": "992624724143"
          }
        },
        "additionalProperties": false,
        "description": "How a payment was funded. Never contains a card number: only the last four digits."
      },
      "V1PaymentSourceRequest": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Required, and `token`: a token your page just created in the browser. `card` is refused —\nsee the note on this object — and so is any other value.",
            "nullable": true,
            "example": "token"
          },
          "card": {
            "type": "string",
            "description": "A saved card's id. <b>Not accepted.</b> The field is still described so that sending one is answered\nwith `stored_card_not_chargeable` and a sentence saying where a stored card is charged, rather\nthan with \"unknown field\", which would tell you nothing.",
            "nullable": true,
            "example": "card_3a237a6b496470e726bb2a412990960d"
          },
          "token": {
            "type": "string",
            "description": "A single-use token created in the customer's browser by the payment fields on your page. Required.\nSingle-use is not a figure of speech: a token we have kept as a saved card is refused here. Never a\ncard number: send one and the request is refused.",
            "nullable": true,
            "example": "token_9fbc4d2e6a1b"
          }
        },
        "additionalProperties": false,
        "description": "How a charge is funded: `{\"type\":\"token\",\"token\":\"token_…\"}`, a token your own page created in the\ncustomer's browser from the card they are typing. That is the only kind this endpoint accepts.\n            \n`{\"type\":\"card\"}` — a card the customer saved earlier — is refused with\n`stored_card_not_chargeable`, and so is a token we hold on file, because that is the same card under\nanother name. Only Nalpay charges a card on file, on the schedule of a subscription the customer agreed\nto, and no merchant, key or dashboard can ask it to.\n            \nThere is no field for a card number and there never will be — a card number must not reach a Nalpay\nserver, so the card is turned into a token in the browser and only the token is sent here, and a value\nthat looks like a card number is refused rather than forwarded."
      },
      "V1PaymentStatus": {
        "enum": [
          "initiated",
          "paid",
          "authorized",
          "captured",
          "failed",
          "refunded",
          "voided",
          "verified"
        ],
        "type": "string",
        "description": "Where a charge got to.",
        "x-enumNames": [
          "initiated",
          "paid",
          "authorized",
          "captured",
          "failed",
          "refunded",
          "voided",
          "verified"
        ]
      },
      "V1Refund": {
        "required": [
          "amount",
          "created_at",
          "currency",
          "id",
          "payment",
          "status"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `refund`.",
            "nullable": true,
            "example": "refund"
          },
          "id": {
            "type": "string",
            "description": "The refund id.",
            "nullable": true,
            "example": "ref_3a237e837848debb518626436d551d45"
          },
          "payment": {
            "type": "string",
            "description": "The payment that was refunded.",
            "nullable": true,
            "example": "pay_3a237e835a0d5214f7c94b4b58739157"
          },
          "amount": {
            "type": "integer",
            "description": "Minor units.",
            "format": "int64",
            "example": 500
          },
          "currency": {
            "type": "string",
            "description": "Three-letter ISO code, upper case.",
            "nullable": true,
            "example": "SAR"
          },
          "status": {
            "$ref": "#/components/schemas/V1RefundStatus"
          },
          "reason": {
            "type": "string",
            "description": "Whatever you sent as `reason`, echoed back. Never anything you did not send.",
            "nullable": true,
            "example": "Customer cancelled the order"
          },
          "bank_reference": {
            "type": "string",
            "description": "The acquirer's reference for the movement. This is the number to give a customer whose bank says it\ncannot see the refund. Null until the refund succeeds, and null when the acquirer reported none.",
            "nullable": true,
            "example": "106152735102"
          },
          "failure_reason": {
            "type": "string",
            "description": "Why the gateway refused. Null unless `status` is `failed`.",
            "nullable": true
          },
          "succeeded_at": {
            "type": "string",
            "description": "When the gateway confirmed it. Null until it does. UTC, ISO 8601.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-09-04T12:13:31Z"
          },
          "failed_at": {
            "type": "string",
            "description": "When the gateway refused. Null unless it did. UTC, ISO 8601.",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "description": "When the refund was asked for. UTC, ISO 8601.",
            "format": "date-time",
            "example": "2026-09-04T12:13:31Z"
          }
        },
        "additionalProperties": false,
        "description": "Money returned to a customer. A payment may have several, up to its own amount."
      },
      "V1RefundStatus": {
        "enum": [
          "requested",
          "succeeded",
          "failed"
        ],
        "type": "string",
        "description": "Where a refund got to. There is no pending state: the gateway answers a refund synchronously.",
        "x-enumNames": [
          "requested",
          "succeeded",
          "failed"
        ]
      },
      "V1ResendEventRequest": {
        "type": "object",
        "properties": {
          "webhook_endpoint": {
            "type": "string",
            "description": "The endpoint to resend to, as `we_…`. Omit to resend to every enabled endpoint subscribed to\nthis event's type.",
            "nullable": true,
            "example": "we_3a237e835a0d5214f7c94b4b58739157"
          }
        },
        "additionalProperties": false
      },
      "V1Subscription": {
        "required": [
          "amount",
          "billing_period_days",
          "cancel_at_period_end",
          "created_at",
          "currency",
          "description",
          "failed_attempts",
          "id",
          "metadata",
          "recover_url",
          "status",
          "subscribe_url"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `subscription`.",
            "nullable": true,
            "example": "subscription"
          },
          "id": {
            "type": "string",
            "description": "The subscription id.",
            "nullable": true,
            "example": "sub_3a237e31f93b606afe649985ba8b9cb1"
          },
          "status": {
            "$ref": "#/components/schemas/V1SubscriptionStatus"
          },
          "description": {
            "type": "string",
            "description": "Shown to the customer on the subscribe page and sent to the gateway on every charge.",
            "nullable": true,
            "example": "Studio membership"
          },
          "amount": {
            "type": "integer",
            "description": "Charged every period, in minor units.",
            "format": "int64",
            "example": 49900
          },
          "currency": {
            "type": "string",
            "description": "Three-letter ISO code, upper case.",
            "nullable": true,
            "example": "SAR"
          },
          "billing_period_days": {
            "type": "integer",
            "description": "How many days one period lasts: 30, 90, 180 or 365. Each period starts where the last one ended and\nruns for exactly this many days, so a renewal falls 30 days after the previous one rather than on the\nsame date each month.",
            "format": "int32",
            "example": 30
          },
          "customer": {
            "type": "string",
            "description": "The subscriber, set when they identify themselves on the subscribe page.",
            "nullable": true,
            "example": "cus_3a237a5653eb9b2fe4a04c2b1db8089b"
          },
          "card": {
            "type": "string",
            "description": "The card renewals are charged against, set by the first payment.",
            "nullable": true,
            "example": "card_3a237a6b496470e726bb2a412990960d"
          },
          "current_period_start": {
            "type": "string",
            "description": "Start of the period the customer has paid for. UTC.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-11-04T06:00:00Z"
          },
          "current_period_end": {
            "type": "string",
            "description": "End of the period the customer has paid for. UTC.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-12-04T06:00:00Z"
          },
          "next_charge_at": {
            "type": "string",
            "description": "When the next charge is due, or null while nothing is scheduled.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-12-04T06:00:00Z"
          },
          "cancel_at_period_end": {
            "type": "boolean",
            "description": "True when the subscription will stop at the end of the current period instead of renewing.",
            "example": false
          },
          "canceled_at": {
            "type": "string",
            "description": "When it was cancelled, or null. UTC.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-12-01T09:15:00Z"
          },
          "cancel_reason": {
            "type": "string",
            "description": "Why it was cancelled: who or what stopped it. Null while it is running. This is the field that makes\na `subscription_canceled` webhook actionable — a merchant needs to tell \"the customer asked\" from\n\"every retry failed\".",
            "nullable": true,
            "example": "dunning_exhausted"
          },
          "failed_attempts": {
            "type": "integer",
            "description": "Failed charges inside the current period. Reset by every success.",
            "format": "int32",
            "example": 0
          },
          "consent": {
            "$ref": "#/components/schemas/V1SubscriptionConsent"
          },
          "subscribe_url": {
            "type": "string",
            "description": "Where to send the customer to pay the first cycle and consent to the recurring charge.",
            "nullable": true,
            "example": "https://paywithnal.com/subscribe/36ff9b064fab"
          },
          "recover_url": {
            "type": "string",
            "description": "Where to send the customer to settle an overdue cycle, possibly with a different card.",
            "nullable": true,
            "example": "https://paywithnal.com/subscribe/36ff9b064fab/recover"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Your own key/value pairs, echoed back.",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "description": "When the subscription was created. UTC, ISO 8601.",
            "format": "date-time",
            "example": "2026-09-04T10:44:30Z"
          }
        },
        "additionalProperties": false,
        "description": "A recurring agreement with one customer: what is charged, how often, and against which card."
      },
      "V1SubscriptionConsent": {
        "required": [
          "source"
        ],
        "type": "object",
        "properties": {
          "source": {
            "$ref": "#/components/schemas/V1ConsentSource"
          },
          "accepted_at": {
            "type": "string",
            "description": "When the cardholder agreed. UTC, ISO 8601.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-09-04T09:12:00Z"
          },
          "reference": {
            "type": "string",
            "description": "Your own pointer to the agreement, exactly as you sent it. Null unless you asserted it.",
            "nullable": true,
            "example": "terms-v4:order-1024"
          },
          "text_version": {
            "type": "string",
            "description": "The version of the consent wording Nalpay showed. Null unless we showed it.",
            "nullable": true,
            "example": "2026-09-v2"
          }
        },
        "additionalProperties": false,
        "description": "The cardholder's agreement to a recurring charge, and — the part that matters in a dispute — who took it.\n            \n`source` is `nalpay_hosted_page` when it was ticked on our subscribe page, and then\n`text_version` names the wording we showed and we hold the evidence. It is `merchant` when you\nstarted the subscription from a saved card and told us the agreement existed: then `reference` is\nwhat you sent, `text_version` is null because there is no Nalpay wording to name, and the evidence is\nyours to produce."
      },
      "V1SubscriptionSourceRequest": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Was always `card`. Whatever it says, the request is refused.",
            "nullable": true,
            "example": "card"
          },
          "card": {
            "type": "string",
            "description": "The saved card that was to be charged. It is not looked up: there is no answer that charges it.",
            "nullable": true,
            "example": "card_3a237a6b496470e726bb2a412990960d"
          }
        },
        "additionalProperties": false,
        "description": "Which saved card to start a subscription against. <b>Nothing accepts this any more</b>; it is still\npublished so that an integration built while it did is answered with `stored_card_not_chargeable`\nand told where a stored card is charged, rather than with a bare \"unknown field\"."
      },
      "V1SubscriptionStatus": {
        "enum": [
          "incomplete",
          "active",
          "past_due",
          "canceled",
          "ended"
        ],
        "type": "string",
        "description": "Where a recurring agreement stands.",
        "x-enumNames": [
          "incomplete",
          "active",
          "past_due",
          "canceled",
          "ended"
        ]
      },
      "V1TestAdvance": {
        "required": [
          "outcome",
          "subscription"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `test_advance`.",
            "nullable": true,
            "example": "test_advance"
          },
          "subscription": {
            "$ref": "#/components/schemas/V1Subscription"
          },
          "cycle": {
            "$ref": "#/components/schemas/V1Cycle"
          },
          "outcome": {
            "type": "string",
            "description": "What the billing runner did: `charged`, `failed`, `skipped`, `canceled` or `nothing_due`.",
            "nullable": true,
            "example": "charged"
          }
        },
        "additionalProperties": false,
        "description": "The result of running one subscription's next cycle from the test clock."
      },
      "V1TestPayRequest": {
        "type": "object",
        "properties": {
          "card_brand": {
            "type": "string",
            "description": "The card network to label the simulated payment with: `visa` (the default), `mastercard`,\n`mada` or `amex`. It decides which rate the fee is priced at, nothing else. It is a label,\nnot a card: no card number is accepted here or anywhere else on this API.",
            "nullable": true,
            "example": "visa"
          },
          "outcome": {
            "type": "string",
            "description": "`paid` (the default) settles the link. `failed` produces exactly what a real decline\n            produces — a recorded failed payment, a `payment_failed` webhook, and a link that is still open —\n            so the path most integrations never exercise can be exercised deliberately.",
            "nullable": true,
            "example": "paid"
          }
        },
        "additionalProperties": false,
        "description": "The body of `POST /v1/test/payment_links/:id/pay`. Both fields are optional; the defaults are the case an\nagent wants first.\n            \nThere is deliberately no `amount`, no `currency` and no field a card could travel in. The amount\nand currency are the link's own, so the exact-match rule that decides whether a payment settles a link is\nnot something a caller can aim at — and a simulated payment can no more mis-settle a link than a real one."
      },
      "V1TestPayment": {
        "required": [
          "message",
          "outcome",
          "payment",
          "payment_link"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `test_payment`. Not `payment`: this object is not one, and never sorts with one.",
            "nullable": true,
            "example": "test_payment"
          },
          "simulated": {
            "type": "boolean",
            "description": "Always true. Present so a client that reads only this object still cannot miss it.",
            "example": true
          },
          "outcome": {
            "type": "string",
            "description": "What was asked for and what happened: `paid` or `failed`.",
            "nullable": true,
            "example": "paid"
          },
          "payment": {
            "$ref": "#/components/schemas/V1Payment"
          },
          "payment_link": {
            "$ref": "#/components/schemas/V1PaymentLink"
          },
          "message": {
            "type": "string",
            "description": "Said in words, so it reaches a reader who only ever prints the response.",
            "nullable": true,
            "example": "Simulated in test mode. No card was charged, nothing reached the payment provider, and this payment exists only in Nalpay's test ledger."
          }
        },
        "additionalProperties": false,
        "description": "The result of paying a link in test mode without a browser. Its own object rather than a bare payment,\nbecause the two things a caller wants next are the payment and what became of the link, and one call\nshould answer both."
      },
      "V1UpdateWebhookEndpointRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Omit to leave the URL alone.",
            "nullable": true,
            "example": "https://merchant.example.com/webhooks/v2"
          },
          "description": {
            "type": "string",
            "description": "Omit to leave the description alone; send an empty string to clear it.",
            "nullable": true,
            "example": "Order service"
          },
          "enabled_events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Omit to leave the selection alone. When given it replaces the whole set.",
            "nullable": true,
            "example": [
              "payment_paid"
            ]
          },
          "enabled": {
            "type": "boolean",
            "description": "Omit to leave it as it is. Enabling clears an automatic disable and its failure count.",
            "nullable": true,
            "example": true
          }
        },
        "additionalProperties": false
      },
      "V1WebhookDelivery": {
        "required": [
          "attempts",
          "created_at",
          "event",
          "id",
          "is_replay",
          "status",
          "type",
          "url",
          "webhook_endpoint"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `webhook_delivery`.",
            "nullable": true,
            "example": "webhook_delivery"
          },
          "id": {
            "type": "string",
            "description": "The delivery id.",
            "nullable": true,
            "example": "whd_3a237e83027c48e3b23b32c94ac80b06"
          },
          "event": {
            "type": "string",
            "description": "The event being delivered.",
            "nullable": true,
            "example": "evt_3a237e835a0d5214f7c94b4b58739157"
          },
          "webhook_endpoint": {
            "type": "string",
            "description": "The endpoint it is going to.",
            "nullable": true,
            "example": "we_3a237e835a0d5214f7c94b4b58739157"
          },
          "type": {
            "type": "string",
            "description": "The event type, repeated so a delivery reads on its own.",
            "nullable": true,
            "example": "payment_paid"
          },
          "url": {
            "type": "string",
            "description": "Where it was posted.",
            "nullable": true,
            "example": "https://merchant.example.com/webhooks/nalpay"
          },
          "status": {
            "$ref": "#/components/schemas/V1WebhookDeliveryStatus"
          },
          "attempts": {
            "type": "integer",
            "description": "How many attempts have been made, out of eight.",
            "format": "int32",
            "example": 1
          },
          "last_response_status": {
            "type": "integer",
            "description": "The HTTP status your endpoint returned on the last attempt, or null if nothing answered.",
            "format": "int32",
            "nullable": true,
            "example": 200
          },
          "last_error": {
            "type": "string",
            "description": "The transport error on the last attempt, when there was no response at all.",
            "nullable": true,
            "example": "Connection refused"
          },
          "next_attempt_at": {
            "type": "string",
            "description": "When the next attempt is due, or null when none is scheduled.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-09-04T12:19:02Z"
          },
          "is_replay": {
            "type": "boolean",
            "description": "True when this delivery was started by a resend rather than by the event itself.",
            "example": false
          },
          "created_at": {
            "type": "string",
            "description": "When the delivery was queued. UTC, ISO 8601.",
            "format": "date-time",
            "example": "2026-09-04T12:13:24Z"
          }
        },
        "additionalProperties": false,
        "description": "One attempt at delivering one event to one endpoint."
      },
      "V1WebhookDeliveryStatus": {
        "enum": [
          "pending",
          "succeeded",
          "failed"
        ],
        "type": "string",
        "description": "Where one event's delivery to one endpoint got to.",
        "x-enumNames": [
          "pending",
          "succeeded",
          "failed"
        ]
      },
      "V1WebhookDisableReason": {
        "enum": [
          "manual",
          "delivery_failures"
        ],
        "type": "string",
        "description": "Why an endpoint is switched off.",
        "x-enumNames": [
          "manual",
          "delivery_failures"
        ]
      },
      "V1WebhookEndpoint": {
        "required": [
          "created_at",
          "enabled",
          "enabled_events",
          "id",
          "livemode",
          "mode",
          "secret_last4",
          "url"
        ],
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "description": "Always `webhook_endpoint`.",
            "nullable": true,
            "example": "webhook_endpoint"
          },
          "id": {
            "type": "string",
            "description": "The endpoint id.",
            "nullable": true,
            "example": "we_3a237e835a0d5214f7c94b4b58739157"
          },
          "url": {
            "type": "string",
            "description": "Where deliveries are posted. `https` only in live mode.",
            "nullable": true,
            "example": "https://merchant.example.com/webhooks/nalpay"
          },
          "description": {
            "type": "string",
            "description": "Your own label for this endpoint.",
            "nullable": true,
            "example": "Order service"
          },
          "enabled_events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The event types this endpoint receives. Never empty.",
            "nullable": true,
            "example": [
              "payment_paid",
              "payment_refunded"
            ]
          },
          "enabled": {
            "type": "boolean",
            "description": "False while the endpoint is switched off. Nothing is delivered to a disabled endpoint.",
            "example": true
          },
          "disabled_reason": {
            "$ref": "#/components/schemas/V1WebhookDisableReason"
          },
          "mode": {
            "$ref": "#/components/schemas/V1Mode"
          },
          "livemode": {
            "type": "boolean",
            "description": "Convenience mirror of `mode`.",
            "example": false
          },
          "secret_last4": {
            "type": "string",
            "description": "The last four characters of the signing secret, so you can tell which one is configured.",
            "nullable": true,
            "example": "k3Qz"
          },
          "secret": {
            "type": "string",
            "description": "The signing secret, in full. Present <b>only</b> on the response that created the endpoint and on the\nresponse to `rotate_secret`. Store it then; it is null on every subsequent read and cannot be\nrecovered.",
            "nullable": true,
            "example": "whsec_9tQ2mVx7bLpR4sZaE1nKcJdWfYhU0oGi"
          },
          "last_delivery_at": {
            "type": "string",
            "description": "When the most recent attempt to this endpoint happened, or null if there has not been one.",
            "format": "date-time",
            "nullable": true,
            "example": "2026-09-04T12:14:02Z"
          },
          "last_delivery_status": {
            "type": "integer",
            "description": "The HTTP status of that attempt, or null when nothing answered.",
            "format": "int32",
            "nullable": true,
            "example": 200
          },
          "created_at": {
            "type": "string",
            "description": "When the endpoint was created. UTC, ISO 8601.",
            "format": "date-time",
            "example": "2026-09-04T12:10:00Z"
          }
        },
        "additionalProperties": false,
        "description": "A place a merchant wants events delivered. The signing secret appears in exactly two responses — the one\nthat creates the endpoint and the one that rotates its secret — and in no other, ever. A read of an\nendpoint returns `secret_last4` and nothing more, because an endpoint listing is the sort of thing\nthat ends up in a log, a screenshot or an agent's context window."
      }
    },
    "securitySchemes": {
      "SecretKey": {
        "type": "http",
        "description": "Your secret API key, e.g. sk_test_… . Never a publishable (pk_) key.",
        "scheme": "bearer"
      }
    }
  },
  "tags": [
    {
      "name": "Account",
      "description": "Who this key belongs to, which mode it is bound to, and what the account may do today. The first call to make.",
      "x-displayName": "Account"
    },
    {
      "name": "Payments",
      "description": "Charges, and the refunds that send money back. A payment is the only object here that moves money on its own.",
      "x-displayName": "Payments"
    },
    {
      "name": "PaymentLinks",
      "description": "Payment links: a page we host that collects one payment for one amount, safe to send to a customer as it is.",
      "x-displayName": "PaymentLinks"
    },
    {
      "name": "Customers",
      "description": "People, and the cards they have agreed you may keep and charge again.",
      "x-displayName": "Customers"
    },
    {
      "name": "Subscriptions",
      "description": "Recurring agreements, every past and scheduled cycle, and cancelling or resuming one. There is no plan object to create first.",
      "x-displayName": "Subscriptions"
    },
    {
      "name": "WebhookEndpoints",
      "description": "Where we post what happens on your account, what each endpoint is subscribed to, and the secret it signs with.",
      "x-displayName": "Webhook endpoints"
    },
    {
      "name": "Events",
      "description": "Everything we have sent you, readable and replayable.",
      "x-displayName": "Events"
    },
    {
      "name": "Test mode",
      "description": "Endpoints only a test key may call. They exist so an integration can be finished and proved in one sitting: pay a link without a browser, and run a subscription's next cycle without waiting a month.",
      "x-displayName": "Test mode"
    },
    {
      "name": "TestPaymentLinks",
      "description": "The test-mode half of a payment link: the way to pay one without a browser.\n            \nIt is its own controller rather than another route on Nalpay.V1.Controllers.PaymentLinksController so that\neverything under `/v1/test/` reads as one thing on the published surface — endpoints only a test key\nmay call."
    }
  ]
}