# Nalpay
> Card payments, payment links, saved cards and recurring subscriptions for Saudi merchants. A small REST API over HTTPS, JSON in and JSON out, amounts as whole numbers of halalas.
This file is written for machines. It is the complete surface: authentication, modes, every
endpoint with the shape of its request and response, the one error envelope, idempotency,
webhooks and how to verify their signatures, and the test cards. Read it and you can
integrate without opening anything else.
## Where everything is
API base URL: https://paywithnal.com
OpenAPI 3 document: https://paywithnal.com/openapi.json
This file: https://paywithnal.com/llms.txt
Agent skills index: https://paywithnal.com/.well-known/agent-skills/index.json
MCP server: https://mcp.paywithnal.com/mcp (Streamable HTTP, POST JSON-RPC, bearer secret key)
Human documentation: https://paywithnal.com/docs
## Authentication
Send a secret key as a bearer token on every request:
Authorization: Bearer sk_test_...
Keys come in four kinds and the prefix carries the meaning:
sk_test_ server-side, test mode. Everything works; no real money exists.
sk_live_ server-side, live mode. Real money. Issued after business verification.
pk_test_ browser. Tokenises a card and nothing else. Refused by this API.
pk_live_ browser, live. Same.
A key is shown once when it is created and stored only as a hash, so it cannot be read
back. Keys are revoked, never deleted. Never put a secret key in a browser, a public
repository, or a chat transcript.
## Test and live are separate universes
The key's prefix decides the mode, and nothing else can: there is no mode parameter and
the x-nalpay-mode header is ignored for key-authenticated requests. An object created by
a test key does not exist for a live key — it is a 404, not a 403, because saying
'forbidden' would confirm the id is real. The same holds in the other direction, and for
webhooks: a test endpoint never receives a live event.
Build in test mode. A staging server holding sk_test_ cannot charge a real card.
## Money
Amounts are integers in the currency's minor units. 10000 is 100.00 SAR. There are no
decimals anywhere in this API. Every amount is between 1 and 100000000 halalas — one
halala to one million riyals — and anything outside that is refused with
parameter_invalid naming the field. Currency must be SAR: Nalpay settles in Saudi riyals
only today, and any other code is refused rather than quietly charged in riyals.
## Object ids
Every id is a prefix plus 32 hexadecimal characters, and the prefix says what it refers to:
acct_ merchant account pay_ payment ref_ refund
cus_ customer card_ saved card plink_ payment link
sub_ subscription cyc_ billing cycle evt_ event
we_ webhook endpoint whd_ webhook delivery
Ids are opaque: pass them back exactly as you received them. A bare UUID is never accepted.
## Idempotency
Every POST must carry an Idempotency-Key header. Any unique string; a UUID is ideal.
Idempotency-Key: 6f1a2c34-8b7d-4f52-9e60-2a5c0d3b7e11
The same key with the same body replays the stored response instead of doing the work
twice, and the response carries Idempotent-Replayed: true. The same key with a different
body is refused with idempotency_key_reuse. Keys are remembered for 24 hours. This is
required, not optional: it is what stands between a retry loop and a second charge.
## Errors
Every non-2xx response has the same body:
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid",
"message": "amount must be a positive integer in the currency's minor units.",
"param": "amount",
"doc_url": "https://paywithnal.com/docs/errors/parameter_invalid"
}
}
Branch on type; report code. The six types:
invalid_request_error the request was wrong. Retrying it unchanged will not help.
authentication_error no key, an unknown key, a revoked key, or a pk_ key.
permission_error the key is valid but the account may not do this yet.
not_found_error no such object for this merchant in this mode.
rate_limit_error slow down; see Retry-After.
api_error our fault. Safe to retry with the same Idempotency-Key.
## Pagination
Lists take limit (1-100, default 20) and starting_after (the id of the last object on the
previous page) and return { "object": "list", "data": [...], "has_more": true }. There are
no offsets and no page numbers, so a list stays consistent while it is being written to.
## Versioning and request ids
The version is in the path. Send an optional dated Nalpay-Version header (for example
2026-09-04) to pin behaviour; it is echoed back on every response. Every response also
carries Nalpay-Request-Id — quote it to support and we can find your exact request.
## Rate limits
100 requests per minute per key, with a ceiling of 20 per second. Over the limit you get
429 with a Retry-After header in seconds. It is one budget per key across the whole
surface: these endpoints and the MCP server's tool calls draw from the same allowance, so
give your server and your agent a key each if either gets busy.
## Endpoints
Generated from the OpenAPI document above, so this list cannot fall behind it.
GET /v1/account
Who this key belongs to, which mode it is bound to, and what is unlocked.
Make this your first call. The mode comes from the key's own prefix — an `sk_test_` key can never be talked into live mode by a header — and `capabilities` tells you whether the account can charge yet, so an integration…
response:
object string Always `account`.
id string required The merchant this key belongs to.
business_name string required The merchant's business name, as it appears to their customers.
business_name_ar string The same name in Arabic, when the merchant has given one.
mode live|test required
livemode boolean required Convenience mirror of `mode`: true only for a live key.
kyc_status unverified|in_review|pending_documents|approved|rejected required
capabilities object required What this account can do right now. Check it before building a flow around something it cannot.
capabilities.can_charge boolean required Whether payments, refunds, paymentLinks and subscriptions may be created at all.
capabilities.can_create_live_keys boolean required Whether the merchant's verification is far enough along to issue live keys.
capabilities.subscriptions_enabled boolean required Whether recurring billing is turned on for this merchant.
api_version string required The version this response was served under (see the `Nalpay-Version` header).
DELETE /v1/cards/{id}
Forget a stored card.
The token is deleted at the gateway and the card stops being chargeable. Payments already made with it keep their history. Returns the card as it was at the moment it was removed.
response:
object string Always `card`.
id string required The card id, which is what you charge.
customer string required The customer this card belongs to.
brand string The card network.
last4 string The last four digits.
exp_month integer 1–12.
exp_year integer Four digits.
name string The cardholder name.
status initiated|active|inactive|expired required
created_at string required When the card was stored. UTC, ISO 8601.
POST /v1/customers
Create a customer, or return the one that already matches.
Customers are matched on phone first, then email, so calling this twice with the same details gives you the same customer rather than a duplicate. Give at least one of `name`, `email`, `phone`. ``` curl https://paywithna…
request:
name string The customer's name.
email string Stored lower-cased. Give at least one of name, email and phone.
phone string Saudi mobile number in any common format; stored as international digits (966…), no "+".
response:
object string Always `customer`.
id string required The customer id.
name string The customer's name, if you gave one.
email string Lower-cased.
phone string International digits, country code first, no "+".
created_at string required When the customer was first seen. UTC, ISO 8601.
GET /v1/customers
Customers, newest first.
query:
limit integer 1–100. Defaults to 20.
starting_after string A `cus_` id from the previous page.
response:
object string Always `list`.
data array of V1Customer required The objects on this page, newest first.
data[].object string Always `customer`.
data[].id string required The customer id.
data[].name string The customer's name, if you gave one.
data[].email string Lower-cased.
data[].phone string International digits, country code first, no "+".
data[].created_at string required When the customer was first seen. UTC, ISO 8601.
has_more boolean required True when more objects exist after the last one in `data`. Pass that object's id as `starting_after` to get the next page.
GET /v1/customers/{id}
One customer, by its `cus_` id.
response:
object string Always `customer`.
id string required The customer id.
name string The customer's name, if you gave one.
email string Lower-cased.
phone string International digits, country code first, no "+".
created_at string required When the customer was first seen. UTC, ISO 8601.
GET /v1/customers/{id}/cards
The cards this customer has stored, newest first.
A card is a token held at the gateway. Nalpay stores the brand, the last four digits and the expiry — never a card number.
query:
limit integer 1–100. Defaults to 20.
starting_after string A `card_` id from the previous page.
response:
object string Always `list`.
data array of V1Card required The objects on this page, newest first.
data[].object string Always `card`.
data[].id string required The card id, which is what you charge.
data[].customer string required The customer this card belongs to.
data[].brand string The card network.
data[].last4 string The last four digits.
data[].exp_month integer 1–12.
data[].exp_year integer Four digits.
data[].name string The cardholder name.
data[].status initiated|active|inactive|expired required
data[].created_at string required When the card was stored. UTC, ISO 8601.
has_more boolean required True when more objects exist after the last one in `data`. Pass that object's id as `starting_after` to get the next page.
GET /v1/events
Events in this mode, newest first.
Useful as a catch-up: after an outage, page from the last event id you processed rather than asking us to resend each one.
query:
limit integer 1–100. Defaults to 20.
starting_after string An `evt_` id from the previous page.
type string One event type, for example `payment_paid`. Omit for all of them.
created_from string Inclusive lower bound on `created`, UTC ISO 8601.
created_to string Exclusive upper bound on `created`, UTC ISO 8601.
response:
object string Always `list`.
data array of V1Event required The objects on this page, newest first.
data[].object string Always `event`.
data[].id string required The event id, repeated in the `Nalpay-Delivery` header of every attempt.
data[].type string required What happened, for example `payment_paid`.
data[].created string required When it happened. UTC, ISO 8601.
data[].livemode boolean required True on a live event. Only endpoints of the same mode ever receive it.
data[].data object required The object the event is about, exactly as `/v1` publishes it.
has_more boolean required True when more objects exist after the last one in `data`. Pass that object's id as `starting_after` to get the next page.
GET /v1/events/{id}
One event, by its `evt_` id.
The `data` you get back is read from the stored payload rather than re-rendered, so it matches the body that was posted to your endpoint byte for byte, even if the object has changed since. It is the object as of when th…
response:
object string Always `event`.
id string required The event id, repeated in the `Nalpay-Delivery` header of every attempt.
type string required What happened, for example `payment_paid`.
created string required When it happened. UTC, ISO 8601.
livemode boolean required True on a live event. Only endpoints of the same mode ever receive it.
data object required The object the event is about, exactly as `/v1` publishes it.
POST /v1/events/{id}/resend
Send an event again.
The same stored bytes are posted, so the body is identical to the original delivery; only the timestamp in `Nalpay-Signature` and therefore the signature itself differ, which is what stops a replay being usable forever.…
request:
webhook_endpoint string The endpoint to resend to, as `we_…`. Omit to resend to every enabled endpoint subscribed to this event's type.
response:
object string Always `list`.
data array of V1WebhookDelivery required The objects on this page, newest first.
data[].object string Always `webhook_delivery`.
data[].id string required The delivery id.
data[].event string required The event being delivered.
data[].webhook_endpoint string required The endpoint it is going to.
data[].type string required The event type, repeated so a delivery reads on its own.
data[].url string required Where it was posted.
data[].status pending|succeeded|failed required
data[].attempts integer required How many attempts have been made, out of eight.
data[].last_response_status integer The HTTP status your endpoint returned on the last attempt, or null if nothing answered.
data[].last_error string The transport error on the last attempt, when there was no response at all.
data[].next_attempt_at string When the next attempt is due, or null when none is scheduled.
data[].is_replay boolean required True when this delivery was started by a resend rather than by the event itself.
data[].created_at string required When the delivery was queued. UTC, ISO 8601.
has_more boolean required True when more objects exist after the last one in `data`. Pass that object's id as `starting_after` to get the next page.
POST /v1/payment_links
Create a payment link.
Returns a `url` on Nalpay's hosted page. Send it to the customer by any channel you like; the card details are collected there, inside the gateway's own fields, and never pass through your site or ours. ``` curl https://…
request:
amount integer Minor units, 1000 to 100000000 — ten riyals to one million riyals.
currency string Must be `SAR`, which is also the default: Nalpay settles in Saudi riyals only.
description string What the link is for. Required, 10 to 200 characters. Shown on the hosted page.
customer string Pre-fill the page for a known customer.
expires_at string UTC. After this instant the link stops being payable. Must be in the future.
metadata object Your own key/value pairs. Keys may not begin with `nalpay_`.
multiple_payers boolean Set to `true` for a link anyone holding it may pay, each of them paying `amount` in full and giving their name on the hosted page. Such a link stays open until `expires_at` or until you cancel it, and cannot also name a…
response:
object string Always `payment_link`.
id string required The payment link id.
status open|paid|failed|canceled|expired|refunded required
amount integer required Minor units.
currency string required Three-letter ISO code, upper case.
description string required What the link is for. Shown on the hosted page.
url string required The hosted page to send the customer to. Safe to share as-is.
customer string The customer the link is for, or null.
expires_at string After this instant the link stops being payable. Null means it never expires.
amount_paid integer required What has been received against this link so far, in minor units, net of refunds. It reaches `amount` when the link is paid. `GET /v1/payments?payment_link=…` lists the payments behind it.
multiple_payers boolean required True when anyone holding the link may pay it, each paying `amount` in full. Such a link stays `open` until it expires or is cancelled, however much has been paid against it.
metadata object required Your own key/value pairs, echoed back.
created_at string required When the link was created. UTC, ISO 8601.
GET /v1/payment_links
Payment links, newest first.
query:
limit integer 1–100. Defaults to 20.
starting_after string A `plink_` id from the previous page.
customer string Optional `cus_` id to list only that customer's links.
response:
object string Always `list`.
data array of V1PaymentLink required The objects on this page, newest first.
data[].object string Always `payment_link`.
data[].id string required The payment link id.
data[].status open|paid|failed|canceled|expired|refunded required
data[].amount integer required Minor units.
data[].currency string required Three-letter ISO code, upper case.
data[].description string required What the link is for. Shown on the hosted page.
data[].url string required The hosted page to send the customer to. Safe to share as-is.
data[].customer string The customer the link is for, or null.
data[].expires_at string After this instant the link stops being payable. Null means it never expires.
data[].amount_paid integer required What has been received against this link so far, in minor units, net of refunds. It reaches `amount` when the link is paid. `GET /v1/payments?payment_link=…` lists the payments behind it.
data[].multiple_payers boolean required True when anyone holding the link may pay it, each paying `amount` in full. Such a link stays `open` until it expires or is cancelled, however much has been paid against it.
data[].metadata object required Your own key/value pairs, echoed back.
data[].created_at string required When the link was created. UTC, ISO 8601.
has_more boolean required True when more objects exist after the last one in `data`. Pass that object's id as `starting_after` to get the next page.
GET /v1/payment_links/{id}
One payment link, by its `plink_` id.
response:
object string Always `payment_link`.
id string required The payment link id.
status open|paid|failed|canceled|expired|refunded required
amount integer required Minor units.
currency string required Three-letter ISO code, upper case.
description string required What the link is for. Shown on the hosted page.
url string required The hosted page to send the customer to. Safe to share as-is.
customer string The customer the link is for, or null.
expires_at string After this instant the link stops being payable. Null means it never expires.
amount_paid integer required What has been received against this link so far, in minor units, net of refunds. It reaches `amount` when the link is paid. `GET /v1/payments?payment_link=…` lists the payments behind it.
multiple_payers boolean required True when anyone holding the link may pay it, each paying `amount` in full. Such a link stays `open` until it expires or is cancelled, however much has been paid against it.
metadata object required Your own key/value pairs, echoed back.
created_at string required When the link was created. UTC, ISO 8601.
POST /v1/payments
Charge a token your page created in the customer's browser.
A card number never reaches this endpoint and there is no field that would accept one: tokenise the card in the customer's browser against the gateway's own hosted fields, then send the token here. A value shaped like a…
request:
amount integer Minor units, 1 to 100000000. 10000 is 100.00 SAR; the ceiling is one million riyals.
currency string Must be `SAR`, which is also the default: Nalpay settles in Saudi riyals only.
description string What the customer is paying for. Required, at most 200 characters.
source object How a charge is funded: `{"type":"token","token":"token_…"}`, a token your own page created in the customer's browser from the card they are typing. That is the only kind this endpoint accepts. `{"type":"card"}` — a card…
source.type string Required, and `token`: a token your page just created in the browser. `card` is refused — see the note on this object — and so is any other value.
source.card string A saved card's id. Not accepted. The field is still described so that sending one is answered with `stored_card_not_chargeable` and a sentence saying where a stored card is charged, rather than with "unknown field…
source.token string A single-use token created in the customer's browser by the payment fields on your page. Required. Single-use is not a figure of speech: a token we have kept as a saved card is refused here. Never a card number: send one…
customer string Who is paying, as `cus_…`. Optional, and the payment is attributed to them.
metadata object Your own key/value pairs, echoed back on the payment. Keys may not begin with `nalpay_`.
callback_url string Where the gateway sends the cardholder if it asks for a 3-D Secure step. Only used when the gateway returns a `transaction_url`.
response:
object string Always `payment`.
id string required The payment id.
amount integer required Minor units. 10000 is 100.00 SAR.
currency string required Three-letter ISO code, upper case. `SAR` today.
status initiated|paid|authorized|captured|failed|refunded|voided|verified required
simulated boolean required True when no money moved and none was ever going to: no card was charged and nothing reached the payment provider. Two things produce it, both test-mode only — a payment recorded by `POST /v1/test/payment_links/:id/pay`,…
description string What the customer paid for.
source object required How a payment was funded. Never contains a card number: only the last four digits.
source.type card|token|apple_pay|samsung_pay|stc_pay
source.brand string The card network.
source.last4 string The last four digits of the card.
source.name string The cardholder name the gateway recorded.
source.message string The gateway's own words about the outcome. Useful on a decline.
source.reference_number string The gateway's reference number for a settled card transaction.
amount_refunded integer required Total refunded so far, in minor units. Zero on a payment that has not been refunded.
fee integer required What Nalpay charges the merchant for this payment, VAT included, in minor units.
net integer required What the merchant is due: amount minus refunds minus fee. Minor units.
customer string The customer this payment is attributed to, or null.
payment_link string The payment link this settled, or null.
subscription string The subscription this cycle belongs to, or null.
transaction_url string Where to send the cardholder when the gateway asks for a 3-D Secure step. Usually null.
metadata object required Whatever you sent on the request. Nalpay's own `nalpay_*` keys are stripped out.
created_at string required When the payment was created. UTC, ISO 8601.
GET /v1/payments
Payments, newest first.
Cursor paging: read `has_more`, then pass the last object's id as `starting_after`. There are no page numbers and no offsets, so a list that is being written to while you walk it stays consistent.
query:
limit integer 1–100. Defaults to 20.
starting_after string A `pay_` id from the previous page.
customer string Optional `cus_` id to list only that customer's payments.
payment_link string Optional `plink_` id to list only the payments made against that link.
response:
object string Always `list`.
data array of V1Payment required The objects on this page, newest first.
data[].object string Always `payment`.
data[].id string required The payment id.
data[].amount integer required Minor units. 10000 is 100.00 SAR.
data[].currency string required Three-letter ISO code, upper case. `SAR` today.
data[].status initiated|paid|authorized|captured|failed|refunded|voided|verified required
data[].simulated boolean required True when no money moved and none was ever going to: no card was charged and nothing reached the payment provider. Two things produce it, both test-mode only — a payment recorded by `POST /v1/test/payment_links/:id/pay`,…
data[].description string What the customer paid for.
data[].source object required How a payment was funded. Never contains a card number: only the last four digits.
data[].amount_refunded integer required Total refunded so far, in minor units. Zero on a payment that has not been refunded.
data[].fee integer required What Nalpay charges the merchant for this payment, VAT included, in minor units.
data[].net integer required What the merchant is due: amount minus refunds minus fee. Minor units.
data[].customer string The customer this payment is attributed to, or null.
data[].payment_link string The payment link this settled, or null.
data[].subscription string The subscription this cycle belongs to, or null.
data[].transaction_url string Where to send the cardholder when the gateway asks for a 3-D Secure step. Usually null.
data[].metadata object required Whatever you sent on the request. Nalpay's own `nalpay_*` keys are stripped out.
data[].created_at string required When the payment was created. UTC, ISO 8601.
has_more boolean required True when more objects exist after the last one in `data`. Pass that object's id as `starting_after` to get the next page.
GET /v1/payments/{id}
One payment, by its `pay_` id.
An id from the other mode does not exist here: a test key cannot read a live payment.
response:
object string Always `payment`.
id string required The payment id.
amount integer required Minor units. 10000 is 100.00 SAR.
currency string required Three-letter ISO code, upper case. `SAR` today.
status initiated|paid|authorized|captured|failed|refunded|voided|verified required
simulated boolean required True when no money moved and none was ever going to: no card was charged and nothing reached the payment provider. Two things produce it, both test-mode only — a payment recorded by `POST /v1/test/payment_links/:id/pay`,…
description string What the customer paid for.
source object required How a payment was funded. Never contains a card number: only the last four digits.
source.type card|token|apple_pay|samsung_pay|stc_pay
source.brand string The card network.
source.last4 string The last four digits of the card.
source.name string The cardholder name the gateway recorded.
source.message string The gateway's own words about the outcome. Useful on a decline.
source.reference_number string The gateway's reference number for a settled card transaction.
amount_refunded integer required Total refunded so far, in minor units. Zero on a payment that has not been refunded.
fee integer required What Nalpay charges the merchant for this payment, VAT included, in minor units.
net integer required What the merchant is due: amount minus refunds minus fee. Minor units.
customer string The customer this payment is attributed to, or null.
payment_link string The payment link this settled, or null.
subscription string The subscription this cycle belongs to, or null.
transaction_url string Where to send the cardholder when the gateway asks for a 3-D Secure step. Usually null.
metadata object required Whatever you sent on the request. Nalpay's own `nalpay_*` keys are stripped out.
created_at string required When the payment was created. UTC, ISO 8601.
POST /v1/payments/{id}/refunds
Refund a payment, in full or in part.
Omit `amount` to refund everything still refundable. Partial refunds may be repeated until the payment is fully refunded. ``` curl https://paywithnal.com/v1/payments/pay_…/refunds \ -H "Authorization: Bearer sk_test_…" -…
request:
amount integer Minor units, 1 to 100000000. Omit to refund everything that is still refundable.
reason string Why, in your own words, up to 200 characters. Kept on the refund and echoed back; nothing reads it, and nothing is inferred from it. Optional.
response:
object string Always `refund`.
id string required The refund id.
payment string required The payment that was refunded.
amount integer required Minor units.
currency string required Three-letter ISO code, upper case.
status requested|succeeded|failed required
reason string Whatever you sent as `reason`, echoed back. Never anything you did not send.
bank_reference string The acquirer's reference for the movement. This is the number to give a customer whose bank says it cannot see the refund. Null until the refund succeeds, and null when the acquirer reported none.
failure_reason string Why the gateway refused. Null unless `status` is `failed`.
succeeded_at string When the gateway confirmed it. Null until it does. UTC, ISO 8601.
failed_at string When the gateway refused. Null unless it did. UTC, ISO 8601.
created_at string required When the refund was asked for. UTC, ISO 8601.
GET /v1/refunds/{id}
One refund, by its `ref_` id.
response:
object string Always `refund`.
id string required The refund id.
payment string required The payment that was refunded.
amount integer required Minor units.
currency string required Three-letter ISO code, upper case.
status requested|succeeded|failed required
reason string Whatever you sent as `reason`, echoed back. Never anything you did not send.
bank_reference string The acquirer's reference for the movement. This is the number to give a customer whose bank says it cannot see the refund. Null until the refund succeeds, and null when the acquirer reported none.
failure_reason string Why the gateway refused. Null unless `status` is `failed`.
succeeded_at string When the gateway confirmed it. Null until it does. UTC, ISO 8601.
failed_at string When the gateway refused. Null unless it did. UTC, ISO 8601.
created_at string required When the refund was asked for. UTC, ISO 8601.
POST /v1/subscriptions
Create a subscription and get the link that starts it.
A subscription begins life as a link. Send the customer to `subscribe_url`; they pay the first cycle with 3-D Secure and agree to the recurring charge there, and that payment is what activates the subscription and saves…
request:
description string What the customer is subscribing to. Required, one line, at most 200 characters.
amount integer Charged every period, in minor units, 1 to 100000000.
currency string Must be `SAR`, which is also the default: Nalpay settles in Saudi riyals only.
billing_period_days integer How many days one period lasts: 30 (monthly), 90 (quarterly), 180 (twice a year) or 365 (yearly). Defaults to 30. A period is a count of days, so the renewal date is 30 days after the last one rather than the same date e…
customer string Optional; otherwise the customer identifies themselves on the subscribe page.
source object Which saved card to start a subscription against. Nothing accepts this any more; it is still published so that an integration built while it did is answered with `stored_card_not_chargeable` and told where a store…
source.type string Was always `card`. Whatever it says, the request is refused.
source.card string The saved card that was to be charged. It is not looked up: there is no answer that charges it.
consent object What you were telling us about an agreement you had collected yourself, so that a card on file could be charged on it. Not accepted any more. The card schemes require the cardholder to have agreed to a recurring c…
consent.accepted_at string When the cardholder agreed, UTC, ISO 8601. It must be in the past: it is a record of something that happened, not a promise about something that will.
consent.reference string Your own pointer to the agreement: a terms version, the id of a signed form, an order number — whatever you would produce if the charge were disputed. Required, at most 200 characters. It is stored as you send it and ret…
metadata object Your own key/value pairs. Keys may not begin with `nalpay_`.
response:
object string Always `subscription`.
id string required The subscription id.
status incomplete|active|past_due|canceled|ended required
description string required Shown to the customer on the subscribe page and sent to the gateway on every charge.
amount integer required Charged every period, in minor units.
currency string required Three-letter ISO code, upper case.
billing_period_days integer required How many days one period lasts: 30, 90, 180 or 365. Each period starts where the last one ended and runs for exactly this many days, so a renewal falls 30 days after the previous one rather than on the same date each mon…
customer string The subscriber, set when they identify themselves on the subscribe page.
card string The card renewals are charged against, set by the first payment.
current_period_start string Start of the period the customer has paid for. UTC.
current_period_end string End of the period the customer has paid for. UTC.
next_charge_at string When the next charge is due, or null while nothing is scheduled.
cancel_at_period_end boolean required True when the subscription will stop at the end of the current period instead of renewing.
canceled_at string When it was cancelled, or null. UTC.
cancel_reason string Why it was cancelled: who or what stopped it. Null while it is running. This is the field that makes a `subscription_canceled` webhook actionable — a merchant needs to tell "the customer asked" from "every retry failed".
failed_attempts integer required Failed charges inside the current period. Reset by every success.
consent object The cardholder's agreement to a recurring charge, and — the part that matters in a dispute — who took it. `source` is `nalpay_hosted_page` when it was ticked on our subscribe page, and then `text_version` names the wordi…
consent.source nalpay_hosted_page|merchant required
consent.accepted_at string When the cardholder agreed. UTC, ISO 8601.
consent.reference string Your own pointer to the agreement, exactly as you sent it. Null unless you asserted it.
consent.text_version string The version of the consent wording Nalpay showed. Null unless we showed it.
subscribe_url string required Where to send the customer to pay the first cycle and consent to the recurring charge.
recover_url string required Where to send the customer to settle an overdue cycle, possibly with a different card.
metadata object required Your own key/value pairs, echoed back.
created_at string required When the subscription was created. UTC, ISO 8601.
GET /v1/subscriptions
Subscriptions, newest first.
query:
limit integer 1–100. Defaults to 20.
starting_after string A `sub_` id from the previous page.
customer string Optional `cus_` id to list only that customer's subscriptions.
response:
object string Always `list`.
data array of V1Subscription required The objects on this page, newest first.
data[].object string Always `subscription`.
data[].id string required The subscription id.
data[].status incomplete|active|past_due|canceled|ended required
data[].description string required Shown to the customer on the subscribe page and sent to the gateway on every charge.
data[].amount integer required Charged every period, in minor units.
data[].currency string required Three-letter ISO code, upper case.
data[].billing_period_days integer required How many days one period lasts: 30, 90, 180 or 365. Each period starts where the last one ended and runs for exactly this many days, so a renewal falls 30 days after the previous one rather than on the same date each mon…
data[].customer string The subscriber, set when they identify themselves on the subscribe page.
data[].card string The card renewals are charged against, set by the first payment.
data[].current_period_start string Start of the period the customer has paid for. UTC.
data[].current_period_end string End of the period the customer has paid for. UTC.
data[].next_charge_at string When the next charge is due, or null while nothing is scheduled.
data[].cancel_at_period_end boolean required True when the subscription will stop at the end of the current period instead of renewing.
data[].canceled_at string When it was cancelled, or null. UTC.
data[].cancel_reason string Why it was cancelled: who or what stopped it. Null while it is running. This is the field that makes a `subscription_canceled` webhook actionable — a merchant needs to tell "the customer asked" from "every retry failed".
data[].failed_attempts integer required Failed charges inside the current period. Reset by every success.
data[].consent object The cardholder's agreement to a recurring charge, and — the part that matters in a dispute — who took it. `source` is `nalpay_hosted_page` when it was ticked on our subscribe page, and then `text_version` names the wordi…
data[].subscribe_url string required Where to send the customer to pay the first cycle and consent to the recurring charge.
data[].recover_url string required Where to send the customer to settle an overdue cycle, possibly with a different card.
data[].metadata object required Your own key/value pairs, echoed back.
data[].created_at string required When the subscription was created. UTC, ISO 8601.
has_more boolean required True when more objects exist after the last one in `data`. Pass that object's id as `starting_after` to get the next page.
GET /v1/subscriptions/{id}
One subscription, by its `sub_` id.
response:
object string Always `subscription`.
id string required The subscription id.
status incomplete|active|past_due|canceled|ended required
description string required Shown to the customer on the subscribe page and sent to the gateway on every charge.
amount integer required Charged every period, in minor units.
currency string required Three-letter ISO code, upper case.
billing_period_days integer required How many days one period lasts: 30, 90, 180 or 365. Each period starts where the last one ended and runs for exactly this many days, so a renewal falls 30 days after the previous one rather than on the same date each mon…
customer string The subscriber, set when they identify themselves on the subscribe page.
card string The card renewals are charged against, set by the first payment.
current_period_start string Start of the period the customer has paid for. UTC.
current_period_end string End of the period the customer has paid for. UTC.
next_charge_at string When the next charge is due, or null while nothing is scheduled.
cancel_at_period_end boolean required True when the subscription will stop at the end of the current period instead of renewing.
canceled_at string When it was cancelled, or null. UTC.
cancel_reason string Why it was cancelled: who or what stopped it. Null while it is running. This is the field that makes a `subscription_canceled` webhook actionable — a merchant needs to tell "the customer asked" from "every retry failed".
failed_attempts integer required Failed charges inside the current period. Reset by every success.
consent object The cardholder's agreement to a recurring charge, and — the part that matters in a dispute — who took it. `source` is `nalpay_hosted_page` when it was ticked on our subscribe page, and then `text_version` names the wordi…
consent.source nalpay_hosted_page|merchant required
consent.accepted_at string When the cardholder agreed. UTC, ISO 8601.
consent.reference string Your own pointer to the agreement, exactly as you sent it. Null unless you asserted it.
consent.text_version string The version of the consent wording Nalpay showed. Null unless we showed it.
subscribe_url string required Where to send the customer to pay the first cycle and consent to the recurring charge.
recover_url string required Where to send the customer to settle an overdue cycle, possibly with a different card.
metadata object required Your own key/value pairs, echoed back.
created_at string required When the subscription was created. UTC, ISO 8601.
POST /v1/subscriptions/{id}/cancel
Stop a subscription, now or at the end of the paid period.
`at_period_end: true` lets the customer keep what they have already paid for and stops the next renewal. `false` ends it immediately. A cancellation scheduled for the period end can be undone with `resume`.
request:
at_period_end boolean True to keep the subscription running until the period the customer already paid for ends, then stop. False (the default) cancels immediately.
response:
object string Always `subscription`.
id string required The subscription id.
status incomplete|active|past_due|canceled|ended required
description string required Shown to the customer on the subscribe page and sent to the gateway on every charge.
amount integer required Charged every period, in minor units.
currency string required Three-letter ISO code, upper case.
billing_period_days integer required How many days one period lasts: 30, 90, 180 or 365. Each period starts where the last one ended and runs for exactly this many days, so a renewal falls 30 days after the previous one rather than on the same date each mon…
customer string The subscriber, set when they identify themselves on the subscribe page.
card string The card renewals are charged against, set by the first payment.
current_period_start string Start of the period the customer has paid for. UTC.
current_period_end string End of the period the customer has paid for. UTC.
next_charge_at string When the next charge is due, or null while nothing is scheduled.
cancel_at_period_end boolean required True when the subscription will stop at the end of the current period instead of renewing.
canceled_at string When it was cancelled, or null. UTC.
cancel_reason string Why it was cancelled: who or what stopped it. Null while it is running. This is the field that makes a `subscription_canceled` webhook actionable — a merchant needs to tell "the customer asked" from "every retry failed".
failed_attempts integer required Failed charges inside the current period. Reset by every success.
consent object The cardholder's agreement to a recurring charge, and — the part that matters in a dispute — who took it. `source` is `nalpay_hosted_page` when it was ticked on our subscribe page, and then `text_version` names the wordi…
consent.source nalpay_hosted_page|merchant required
consent.accepted_at string When the cardholder agreed. UTC, ISO 8601.
consent.reference string Your own pointer to the agreement, exactly as you sent it. Null unless you asserted it.
consent.text_version string The version of the consent wording Nalpay showed. Null unless we showed it.
subscribe_url string required Where to send the customer to pay the first cycle and consent to the recurring charge.
recover_url string required Where to send the customer to settle an overdue cycle, possibly with a different card.
metadata object required Your own key/value pairs, echoed back.
created_at string required When the subscription was created. UTC, ISO 8601.
GET /v1/subscriptions/{id}/cycles
Every charge attempt on this subscription, newest first.
One cycle per attempt, including retries, so a failed renewal and the retries that followed it are all visible with their reasons.
query:
limit integer 1–100. Defaults to 20.
starting_after string A `cyc_` id from the previous page.
response:
object string Always `list`.
data array of V1Cycle required The objects on this page, newest first.
data[].object string Always `cycle`.
data[].id string required The cycle id.
data[].subscription string required The subscription this attempt belongs to.
data[].status scheduled|charging|paid|failed|skipped required
data[].kind first_payment|renewal|retry|recovery|payoff required
data[].attempt integer required 0 for a customer-initiated payment, 1 for the scheduled charge, then one per retry.
data[].period_start string required Start of the period this attempt buys. UTC.
data[].period_end string required End of the period this attempt buys. UTC.
data[].scheduled_at string required When the attempt was due. UTC.
data[].charged_at string When the attempt concluded, or null while it has not. UTC.
data[].payment string The payment this attempt produced, or null when it never reached the gateway.
data[].failure_code string Short reason the attempt failed or was skipped.
data[].failure_message string The longer form of the same, when the gateway gave one.
data[].created_at string required When the cycle row was created. UTC, ISO 8601.
has_more boolean required True when more objects exist after the last one in `data`. Pass that object's id as `starting_after` to get the next page.
POST /v1/subscriptions/{id}/resume
Undo a cancellation that was scheduled for the end of the period.
response:
object string Always `subscription`.
id string required The subscription id.
status incomplete|active|past_due|canceled|ended required
description string required Shown to the customer on the subscribe page and sent to the gateway on every charge.
amount integer required Charged every period, in minor units.
currency string required Three-letter ISO code, upper case.
billing_period_days integer required How many days one period lasts: 30, 90, 180 or 365. Each period starts where the last one ended and runs for exactly this many days, so a renewal falls 30 days after the previous one rather than on the same date each mon…
customer string The subscriber, set when they identify themselves on the subscribe page.
card string The card renewals are charged against, set by the first payment.
current_period_start string Start of the period the customer has paid for. UTC.
current_period_end string End of the period the customer has paid for. UTC.
next_charge_at string When the next charge is due, or null while nothing is scheduled.
cancel_at_period_end boolean required True when the subscription will stop at the end of the current period instead of renewing.
canceled_at string When it was cancelled, or null. UTC.
cancel_reason string Why it was cancelled: who or what stopped it. Null while it is running. This is the field that makes a `subscription_canceled` webhook actionable — a merchant needs to tell "the customer asked" from "every retry failed".
failed_attempts integer required Failed charges inside the current period. Reset by every success.
consent object The cardholder's agreement to a recurring charge, and — the part that matters in a dispute — who took it. `source` is `nalpay_hosted_page` when it was ticked on our subscribe page, and then `text_version` names the wordi…
consent.source nalpay_hosted_page|merchant required
consent.accepted_at string When the cardholder agreed. UTC, ISO 8601.
consent.reference string Your own pointer to the agreement, exactly as you sent it. Null unless you asserted it.
consent.text_version string The version of the consent wording Nalpay showed. Null unless we showed it.
subscribe_url string required Where to send the customer to pay the first cycle and consent to the recurring charge.
recover_url string required Where to send the customer to settle an overdue cycle, possibly with a different card.
metadata object required Your own key/value pairs, echoed back.
created_at string required When the subscription was created. UTC, ISO 8601.
POST /v1/test/payment_links/{id}/pay
Pay this link, in test mode, without a browser. Test keys only.
Cards are tokenised in the browser against the payment provider's own fields, which means an integration built entirely from a server could be finished but never demonstrated: the last step, a payment landing and a webho…
request:
card_brand string The card network to label the simulated payment with: `visa` (the default), `mastercard`, `mada` or `amex`. It decides which rate the fee is priced at, nothing else. It is a label, not a card: no card number is accepted…
outcome string `paid` (the default) settles the link. `failed` produces exactly what a real decline produces — a recorded failed payment, a `payment_failed` webhook, and a link that is still open — so the path most integrations never e…
response:
object string Always `test_payment`. Not `payment`: this object is not one, and never sorts with one.
simulated boolean Always true. Present so a client that reads only this object still cannot miss it.
outcome string required What was asked for and what happened: `paid` or `failed`.
payment object required A charge. Created by `POST /v1/payments`, by a payment link, or by a subscription renewal.
payment.object string Always `payment`.
payment.id string required The payment id.
payment.amount integer required Minor units. 10000 is 100.00 SAR.
payment.currency string required Three-letter ISO code, upper case. `SAR` today.
payment.status initiated|paid|authorized|captured|failed|refunded|voided|verified required
payment.simulated boolean required True when no money moved and none was ever going to: no card was charged and nothing reached the payment provider. Two things produce it, both test-mode only — a payment recorded by `POST /v1/test/payment_links/:id/pay`,…
payment.description string What the customer paid for.
payment.source object required How a payment was funded. Never contains a card number: only the last four digits.
payment.amount_refunded integer required Total refunded so far, in minor units. Zero on a payment that has not been refunded.
payment.fee integer required What Nalpay charges the merchant for this payment, VAT included, in minor units.
payment.net integer required What the merchant is due: amount minus refunds minus fee. Minor units.
payment.customer string The customer this payment is attributed to, or null.
payment.payment_link string The payment link this settled, or null.
payment.subscription string The subscription this cycle belongs to, or null.
payment.transaction_url string Where to send the cardholder when the gateway asks for a 3-D Secure step. Usually null.
payment.metadata object required Whatever you sent on the request. Nalpay's own `nalpay_*` keys are stripped out.
payment.created_at string required When the payment was created. UTC, ISO 8601.
payment_link object required A payment link: a hosted page that collects one payment.
payment_link.object string Always `payment_link`.
payment_link.id string required The payment link id.
payment_link.status open|paid|failed|canceled|expired|refunded required
payment_link.amount integer required Minor units.
payment_link.currency string required Three-letter ISO code, upper case.
payment_link.description string required What the link is for. Shown on the hosted page.
payment_link.url string required The hosted page to send the customer to. Safe to share as-is.
payment_link.customer string The customer the link is for, or null.
payment_link.expires_at string After this instant the link stops being payable. Null means it never expires.
payment_link.amount_paid integer required What has been received against this link so far, in minor units, net of refunds. It reaches `amount` when the link is paid. `GET /v1/payments?payment_link=…` lists the payments behind it.
payment_link.multiple_payers boolean required True when anyone holding the link may pay it, each paying `amount` in full. Such a link stays `open` until it expires or is cancelled, however much has been paid against it.
payment_link.metadata object required Your own key/value pairs, echoed back.
payment_link.created_at string required When the link was created. UTC, ISO 8601.
message string required Said in words, so it reaches a reader who only ever prints the response.
POST /v1/test/subscriptions/{id}/advance
Run this subscription's next cycle immediately. Test keys only.
Brings the next charge forward to now and runs the real billing runner for this one subscription — the same code the nightly job executes. Without it, recurring billing could only be tested by waiting a month, which mean…
response:
object string Always `test_advance`.
subscription object required A recurring agreement with one customer: what is charged, how often, and against which card.
subscription.object string Always `subscription`.
subscription.id string required The subscription id.
subscription.status incomplete|active|past_due|canceled|ended required
subscription.description string required Shown to the customer on the subscribe page and sent to the gateway on every charge.
subscription.amount integer required Charged every period, in minor units.
subscription.currency string required Three-letter ISO code, upper case.
subscription.billing_period_days integer required How many days one period lasts: 30, 90, 180 or 365. Each period starts where the last one ended and runs for exactly this many days, so a renewal falls 30 days after the previous one rather than on the same date each mon…
subscription.customer string The subscriber, set when they identify themselves on the subscribe page.
subscription.card string The card renewals are charged against, set by the first payment.
subscription.current_period_start string Start of the period the customer has paid for. UTC.
subscription.current_period_end string End of the period the customer has paid for. UTC.
subscription.next_charge_at string When the next charge is due, or null while nothing is scheduled.
subscription.cancel_at_period_end boolean required True when the subscription will stop at the end of the current period instead of renewing.
subscription.canceled_at string When it was cancelled, or null. UTC.
subscription.cancel_reason string Why it was cancelled: who or what stopped it. Null while it is running. This is the field that makes a `subscription_canceled` webhook actionable — a merchant needs to tell "the customer asked" from "every retry failed".
subscription.failed_attempts integer required Failed charges inside the current period. Reset by every success.
subscription.consent object The cardholder's agreement to a recurring charge, and — the part that matters in a dispute — who took it. `source` is `nalpay_hosted_page` when it was ticked on our subscribe page, and then `text_version` names the wordi…
subscription.subscribe_url string required Where to send the customer to pay the first cycle and consent to the recurring charge.
subscription.recover_url string required Where to send the customer to settle an overdue cycle, possibly with a different card.
subscription.metadata object required Your own key/value pairs, echoed back.
subscription.created_at string required When the subscription was created. UTC, ISO 8601.
cycle object One charge attempt on a subscription: what was tried, when, and what came of it.
cycle.object string Always `cycle`.
cycle.id string required The cycle id.
cycle.subscription string required The subscription this attempt belongs to.
cycle.status scheduled|charging|paid|failed|skipped required
cycle.kind first_payment|renewal|retry|recovery|payoff required
cycle.attempt integer required 0 for a customer-initiated payment, 1 for the scheduled charge, then one per retry.
cycle.period_start string required Start of the period this attempt buys. UTC.
cycle.period_end string required End of the period this attempt buys. UTC.
cycle.scheduled_at string required When the attempt was due. UTC.
cycle.charged_at string When the attempt concluded, or null while it has not. UTC.
cycle.payment string The payment this attempt produced, or null when it never reached the gateway.
cycle.failure_code string Short reason the attempt failed or was skipped.
cycle.failure_message string The longer form of the same, when the gateway gave one.
cycle.created_at string required When the cycle row was created. UTC, ISO 8601.
outcome string required What the billing runner did: `charged`, `failed`, `skipped`, `canceled` or `nothing_due`.
POST /v1/webhook_endpoints
Register a place to receive events.
The response is the only place, besides `rotate_secret`, where the signing secret appears. Store it now: it is not recoverable afterwards, and a lost secret means a rotation and a redeploy. Verify a delivery by taking `t…
request:
url string Where to post. Required. Live mode requires `https` and a publicly routable host.
description string Your own label, at most 200 characters.
enabled_events array of string Which event types to send. Required and non-empty; an unknown type is refused.
response:
object string Always `webhook_endpoint`.
id string required The endpoint id.
url string required Where deliveries are posted. `https` only in live mode.
description string Your own label for this endpoint.
enabled_events array of string required The event types this endpoint receives. Never empty.
enabled boolean required False while the endpoint is switched off. Nothing is delivered to a disabled endpoint.
disabled_reason manual|delivery_failures
mode live|test required
livemode boolean required Convenience mirror of `mode`.
secret_last4 string required The last four characters of the signing secret, so you can tell which one is configured.
secret string The signing secret, in full. Present only on the response that created the endpoint and on the response to `rotate_secret`. Store it then; it is null on every subsequent read and cannot be recovered.
last_delivery_at string When the most recent attempt to this endpoint happened, or null if there has not been one.
last_delivery_status integer The HTTP status of that attempt, or null when nothing answered.
created_at string required When the endpoint was created. UTC, ISO 8601.
GET /v1/webhook_endpoints
Your endpoints in this mode, newest first.
query:
limit integer 1–100. Defaults to 20.
starting_after string A `we_` id from the previous page.
response:
object string Always `list`.
data array of V1WebhookEndpoint required The objects on this page, newest first.
data[].object string Always `webhook_endpoint`.
data[].id string required The endpoint id.
data[].url string required Where deliveries are posted. `https` only in live mode.
data[].description string Your own label for this endpoint.
data[].enabled_events array of string required The event types this endpoint receives. Never empty.
data[].enabled boolean required False while the endpoint is switched off. Nothing is delivered to a disabled endpoint.
data[].disabled_reason manual|delivery_failures
data[].mode live|test required
data[].livemode boolean required Convenience mirror of `mode`.
data[].secret_last4 string required The last four characters of the signing secret, so you can tell which one is configured.
data[].secret string The signing secret, in full. Present only on the response that created the endpoint and on the response to `rotate_secret`. Store it then; it is null on every subsequent read and cannot be recovered.
data[].last_delivery_at string When the most recent attempt to this endpoint happened, or null if there has not been one.
data[].last_delivery_status integer The HTTP status of that attempt, or null when nothing answered.
data[].created_at string required When the endpoint was created. UTC, ISO 8601.
has_more boolean required True when more objects exist after the last one in `data`. Pass that object's id as `starting_after` to get the next page.
GET /v1/webhook_endpoints/{id}
One endpoint, by its `we_` id. Never includes the secret.
response:
object string Always `webhook_endpoint`.
id string required The endpoint id.
url string required Where deliveries are posted. `https` only in live mode.
description string Your own label for this endpoint.
enabled_events array of string required The event types this endpoint receives. Never empty.
enabled boolean required False while the endpoint is switched off. Nothing is delivered to a disabled endpoint.
disabled_reason manual|delivery_failures
mode live|test required
livemode boolean required Convenience mirror of `mode`.
secret_last4 string required The last four characters of the signing secret, so you can tell which one is configured.
secret string The signing secret, in full. Present only on the response that created the endpoint and on the response to `rotate_secret`. Store it then; it is null on every subsequent read and cannot be recovered.
last_delivery_at string When the most recent attempt to this endpoint happened, or null if there has not been one.
last_delivery_status integer The HTTP status of that attempt, or null when nothing answered.
created_at string required When the endpoint was created. UTC, ISO 8601.
PATCH /v1/webhook_endpoints/{id}
Change an endpoint's URL, description, event selection, or whether it is enabled.
Only the fields you send are changed. Disabling an endpoint also cancels the retries already queued for it; enabling one clears an automatic disable and its failure count.
request:
url string Omit to leave the URL alone.
description string Omit to leave the description alone; send an empty string to clear it.
enabled_events array of string Omit to leave the selection alone. When given it replaces the whole set.
enabled boolean Omit to leave it as it is. Enabling clears an automatic disable and its failure count.
response:
object string Always `webhook_endpoint`.
id string required The endpoint id.
url string required Where deliveries are posted. `https` only in live mode.
description string Your own label for this endpoint.
enabled_events array of string required The event types this endpoint receives. Never empty.
enabled boolean required False while the endpoint is switched off. Nothing is delivered to a disabled endpoint.
disabled_reason manual|delivery_failures
mode live|test required
livemode boolean required Convenience mirror of `mode`.
secret_last4 string required The last four characters of the signing secret, so you can tell which one is configured.
secret string The signing secret, in full. Present only on the response that created the endpoint and on the response to `rotate_secret`. Store it then; it is null on every subsequent read and cannot be recovered.
last_delivery_at string When the most recent attempt to this endpoint happened, or null if there has not been one.
last_delivery_status integer The HTTP status of that attempt, or null when nothing answered.
created_at string required When the endpoint was created. UTC, ISO 8601.
DELETE /v1/webhook_endpoints/{id}
Remove an endpoint.
Deliveries still waiting to be retried for it are dropped. Past deliveries stay in the log. Returns the endpoint as it was at the moment it was removed.
response:
object string Always `webhook_endpoint`.
id string required The endpoint id.
url string required Where deliveries are posted. `https` only in live mode.
description string Your own label for this endpoint.
enabled_events array of string required The event types this endpoint receives. Never empty.
enabled boolean required False while the endpoint is switched off. Nothing is delivered to a disabled endpoint.
disabled_reason manual|delivery_failures
mode live|test required
livemode boolean required Convenience mirror of `mode`.
secret_last4 string required The last four characters of the signing secret, so you can tell which one is configured.
secret string The signing secret, in full. Present only on the response that created the endpoint and on the response to `rotate_secret`. Store it then; it is null on every subsequent read and cannot be recovered.
last_delivery_at string When the most recent attempt to this endpoint happened, or null if there has not been one.
last_delivery_status integer The HTTP status of that attempt, or null when nothing answered.
created_at string required When the endpoint was created. UTC, ISO 8601.
POST /v1/webhook_endpoints/{id}/rotate_secret
Issue a new signing secret for this endpoint.
The response carries the new secret in full — the last time it is ever shown. The old secret stops verifying immediately, so deploy the new one before rotating, or accept a gap in which your receiver rejects deliveries.
response:
object string Always `webhook_endpoint`.
id string required The endpoint id.
url string required Where deliveries are posted. `https` only in live mode.
description string Your own label for this endpoint.
enabled_events array of string required The event types this endpoint receives. Never empty.
enabled boolean required False while the endpoint is switched off. Nothing is delivered to a disabled endpoint.
disabled_reason manual|delivery_failures
mode live|test required
livemode boolean required Convenience mirror of `mode`.
secret_last4 string required The last four characters of the signing secret, so you can tell which one is configured.
secret string The signing secret, in full. Present only on the response that created the endpoint and on the response to `rotate_secret`. Store it then; it is null on every subsequent read and cannot be recovered.
last_delivery_at string When the most recent attempt to this endpoint happened, or null if there has not been one.
last_delivery_status integer The HTTP status of that attempt, or null when nothing answered.
created_at string required When the endpoint was created. UTC, ISO 8601.
## Webhooks
Register endpoints with POST /v1/webhook_endpoints. Up to five per mode, each with its
own event selection and its own signing secret, which is shown when the endpoint is
created and when it is rotated and at no other time.
The event types, and there are no others:
payment_paid a charge succeeded
payment_failed a charge was declined or errored
payment_refunded money went back, in full or in part
subscription_activated the first payment landed; the agreement started
subscription_charged a renewal was charged
subscription_payment_failed a renewal attempt failed; a retry may follow
subscription_past_due every retry failed
subscription_canceled it stopped, whoever stopped it
Every delivery is a POST with this body:
{
"object": "event",
"id": "evt_...",
"type": "payment_paid",
"created": "2026-09-04T12:13:24Z",
"livemode": false,
"data": { ...the /v1 object, exactly as a GET would return it... }
}
and these headers:
Nalpay-Signature: t=1757000004,v1=6f2c... the signature (see below)
Nalpay-Delivery: evt_... the event id, identical on every retry
Nalpay-Endpoint: we_... which of your endpoints this is for
x-nalpay-event: payment_paid the type, so you can route before parsing
x-nalpay-signature: deprecated: a bare HMAC, no timestamp
## Verifying a signature
1. Read the RAW request body as bytes. Do not parse and re-serialise it first: a
re-serialised body has different bytes and will not verify.
2. Parse Nalpay-Signature into t and v1.
3. Compute HMAC-SHA256 over the ASCII string t + "." + rawBody keyed with your
endpoint's whsec_ secret, hex-encoded, lowercase.
4. Compare with v1 using a constant-time comparison.
5. Reject if |now - t| is more than 300 seconds.
De-duplicate on Nalpay-Delivery: retries repeat the same event id. Answer 2xx quickly and
do your work afterwards. A failed delivery is retried seven times — eight attempts in
all — over about 23 hours, and an endpoint whose last three events all exhausted their
attempts is switched off.
## Recurring charges
POST /v1/subscriptions gives you a subscribe link, and that is the only way one starts.
It stays incomplete until a person opens that link in a browser, agrees to the recurring
charge and pays the first period. Nothing you can call will start it, so do not create one
and wait: get the link to the customer.
Pass customer when you know them: the page opens with their details filled in and the
subscription is recorded against them. They still enter a card — a card already on file
with this merchant is never offered on that page, because identifying there is a mobile
number and anybody can type one. Do not promise a merchant one-tap returning customers.
source and consent are refused, with stored_card_not_chargeable (403, permission_error):
"source": {"type": "card", "card": "card_..."} refused
"consent": {"accepted_at": ..., "reference": ...} refused
A card the customer has saved is charged by Nalpay's scheduler, on the schedule of a
subscription they agreed to on the subscribe page, and by nothing else at all — not this
API, not any key, not the dashboard, not a page anybody can open. The same code answers
POST /v1/payments sent a saved card, or
sent a token Nalpay already holds on file. Do not build a flow that finds a card and
charges it, and do not offer to: it cannot be done here. To take money now, send a payment
link or charge a token the merchant's own page minted from the card being typed.
## Test mode
Test cards, on any hosted Nalpay page, with any future expiry and any CVC:
4111 1111 1111 1111 Visa, succeeds
5555 5555 5555 4444 Mastercard, succeeds
4000 0000 0000 0002 declined
Fees are NOT zero in test mode. The same schedule prices both modes, so `fee` and `net`
on a test payment are the numbers you will see live. Do not report a test fee as zero and
do not hard-code a rate; the rate depends on the card network. What test mode removes is
the money moving, not the arithmetic.
A card is tokenised in the browser, so nothing on this API can pay a link. With a test key:
POST /v1/test/payment_links/{id}/pay
records a payment against that link through the same ledger a real one goes through: the
link settles, the fee is priced, payment_paid is delivered to your registered test endpoints
signed with their real secret, and the payment lists and refunds like any other. Optional
body: card_brand (visa | mastercard | mada | amex, default visa) and outcome (paid | failed,
default paid); `failed` produces a recorded failed payment, a payment_failed webhook and a
link that stays open. There is no amount field - the link's own amount is used, so the
exact-match rule that settles a link is not something a caller can aim at. The payment
carries "simulated": true, in the API and in the webhook body, permanently. Refused with a
live key: Nalpay never records a live payment that did not happen.
Renewals are a month away, which would make recurring billing untestable in a sitting, so:
POST /v1/test/subscriptions/{id}/advance
runs the subscription's next cycle immediately through the same billing code the nightly
job runs. It is refused with a live key.
To exercise a webhook receiver without waiting for a customer, connect an agent to the MCP
server above and call simulate_event with an event_type argument (one of the types listed
under Webhooks), which sends a real signed delivery of that type to your registered test
endpoints. It too is refused with a live key.
## Integration paths
Each is the shortest correct sequence of calls for one job:
Take a one-off payment — Charge a customer once, by sending them a hosted payment link.
https://paywithnal.com/.well-known/agent-skills/one-off-charge.md
uses: GET /v1/account, POST /v1/payment_links, GET /v1/payment_links/{id}, POST /v1/test/payment_links/{id}/pay, GET /v1/payments, GET /v1/payments/{id}, POST /v1/payments/{id}/refunds
Save a card for a subscription — Store a customer's card from a payment they made, so the subscription it was saved by can renew on it.
https://paywithnal.com/.well-known/agent-skills/save-a-card.md
uses: POST /v1/customers, GET /v1/customers/{id}, GET /v1/customers, GET /v1/customers/{id}/cards, POST /v1/payment_links, POST /v1/subscriptions, DELETE /v1/cards/{id}
Set up a recurring charge — Charge a customer every 30, 90, 180 or 365 days, with retries and cancellation handled for you.
https://paywithnal.com/.well-known/agent-skills/subscription.md
uses: GET /v1/account, POST /v1/subscriptions, GET /v1/subscriptions/{id}, GET /v1/subscriptions, GET /v1/subscriptions/{id}/cycles, POST /v1/subscriptions/{id}/cancel, POST /v1/subscriptions/{id}/resume, POST /v1/test/subscriptions/{id}/advance
Receive and verify webhooks — Register an endpoint, verify the signature on the raw body, and handle each event exactly once.
https://paywithnal.com/.well-known/agent-skills/webhook-handling.md
uses: POST /v1/webhook_endpoints, GET /v1/webhook_endpoints, GET /v1/webhook_endpoints/{id}, PATCH /v1/webhook_endpoints/{id}, DELETE /v1/webhook_endpoints/{id}, POST /v1/webhook_endpoints/{id}/rotate_secret, GET /v1/events, GET /v1/events/{id}, POST /v1/events/{id}/resend
## Guides for people
https://paywithnal.com/docs/agents Nalpay for agents — The MCP server, the four machine-readable surfaces, and the prompt that carries a coding agent from an empty repo to a paid test payment.
https://paywithnal.com/docs/errors-and-idempotency Errors and idempotency — One error shape to branch on, and the header that makes a retry safe to send.
https://paywithnal.com/docs/going-live Going live — What changes when real money starts moving, and the list to work through before it does.
https://paywithnal.com/docs/introduction Introduction — What Nalpay is, the two modes everything runs in, and where to start reading.
https://paywithnal.com/docs/one-off-charge A one-off charge — Charge a card once, either by sending a payment link or by charging a token from your own page.
https://paywithnal.com/docs/quickstart Quickstart — From an empty project to a paid test payment, in about ten minutes.
https://paywithnal.com/docs/saving-a-card Saving a card — How a customer's card comes to be on file, what it is for, and why only a subscription can charge it.
https://paywithnal.com/docs/subscriptions Subscriptions — Charge a customer the same amount every 30, 90, 180 or 365 days, with retries, dunning and cancellation handled.
https://paywithnal.com/docs/test-mode Test mode — The cards, the pay endpoint, the clock and the simulated events that let you finish an integration in one sitting.
https://paywithnal.com/docs/webhooks Handling webhooks — Receive what happens on your account, and verify that it really came from us.