The Checkout API behind your embedded payment step
Naturpay is a small, predictable API and a typed SDK built around a single object: the payment intent. Create it on your server, confirm it from the element in the browser, then capture, refund or replay against the same resource. Cards, wallets and authentication run underneath — you just move the intent through its states.
What the API is
A request/response model built around one object
There is no sprawling surface to learn. You create a payment intent, attach a method to it from the embedded element, and confirm. Every later action — capture, refund, retry, dispute — references that same intent id, so your code and your ledger always point at one source of truth.
- REST over HTTPS with JSON bodies, predictable resource paths and standard status codes — nothing exotic to special-case.
- A secret key signs server calls; a publishable key initializes the element in the browser. Raw card data only ever flows between the buyer and Naturpay.
- Amounts are integers in the smallest currency unit, so 12800 is €128.00 — no floating point ever touches a charge.
- Every object carries a stable id, a status, a created timestamp and an expandable set of related resources you can request inline.
const intent = { id: 'pi_3Qm0…', object: 'payment_intent', status: 'requires_confirmation', amount: 12800, amount_captured: 0, currency: 'eur', capture: 'automatic', method: null, client_secret: 'pi_3Qm0_secret_…', created: 1718870400, metadata: { order: '8841' }, };
The payment-intent lifecycle
Five states, and you drive the transitions
An intent moves through a known set of statuses. You can read the current status at any time, and every transition emits a signed event. Nothing happens implicitly that you can't observe.
requires_confirmation
You created the intent on the server with an amount and currency. The element collects a method and holds the client secret, ready to confirm.
server createrequires_action
Confirmation triggered authentication. The SDK runs the 3-D Secure step inline in the browser and returns control once the challenge resolves.
SCA handled by SDKprocessing
The method is authorized and the charge is moving through the network. The intent is locked against conflicting writes until the result lands.
network in flightsucceeded
Funds are captured, or authorized and held if you chose manual capture. A signed payment.succeeded event arrives so you can fulfill the order.
webhook firesServer-side calls
Create, confirm, refund — same object
Three calls cover the common path. Each is a write against the intent, each accepts an idempotency key, and each returns the full, updated object so you never have to re-fetch to know the new state.
- create returns a client secret you hand to the element — and nothing more sensitive leaves your server.
- confirm and capture let you split authorization from settlement when you ship later than you charge.
- refund accepts a partial amount and reuses the same intent, so a return is one call, not a new resource.
// create an intent for the order const intent = await wv.intents.create( { amount: 12800, currency: 'eur', capture: 'automatic', metadata: { order: order.id }, }, { idempotencyKey: `create_${order.id}` }, ); // hand the secret to the element return { clientSecret: intent.client_secret };
// confirm a held intent on the server const result = await wv.intents.confirm( intent.id, { method: 'pm_1Nv…' }, { idempotencyKey: `confirm_${order.id}` }, ); if (result.status === 'requires_action') { // element finishes 3-D Secure inline }
// refund part of a captured intent const refund = await wv.refunds.create( { intent: 'pi_3Qm0…', amount: 4000, reason: 'requested_by_customer', }, { idempotencyKey: `refund_${rma.id}` }, ); // refund.status -> 'succeeded'
How the element talks to the API
The browser holds the secret, never the keys
Your server creates the intent and returns only its client secret. The embedded element uses that secret to tokenize the card and confirm directly with Naturpay, so sensitive details bypass your backend entirely while you keep full control of the surrounding UI.
- The publishable key scopes the element to confirmation only — it cannot create, capture or refund.
- The client secret is single-intent and short-lived, so a leaked secret can do nothing but settle that one order.
- Confirmation returns either the updated intent or a typed error you branch on — there is no ambiguous in-between.
What the API gives you
The capabilities a payments integration actually needs
These are the primitives that keep an integration correct under retries, partial network failures and real-world money movement — not afterthoughts bolted on later.
Idempotent writes
Pass an idempotency key on any write and a retried request returns the original result instead of charging twice. Keys are stored for 24 hours, so a timed-out request is always safe to repeat.
Idempotency-Key · 24hSigned webhooks
Every state change emits an event signed with your endpoint secret and verifiable in one SDK call. Deliveries are retried with backoff and stay replayable, so a missed event never desyncs your records.
HMAC · retries · replayTyped errors
Failures return JSON with a stable type, code and human message — card_declined, validation_error, rate_limited — never an HTML page. Decline codes map straight to the message you show the buyer.
type · code · messageMulti-currency
Set the currency on the intent and present prices in the buyer's money while you settle in your own. Zero-decimal currencies are handled correctly, and the element formats amounts to each locale.
135+ currenciesSaved methods and tokenization
Attach a method to a customer once and reuse the token for one-tap returns or off-session charges. Cards become network tokens that survive expiry, so saved-card success rates stay high over time.
vault · network tokensManual and partial captures
Authorize at checkout and capture later when you ship, capture less than you authorized, or release the hold entirely. The authorized amount and the captured amount live on the intent as separate fields.
auth then captureBuilt for production
What the API guarantees
Webhooks and idempotency
Make every charge exactly-once
Verify the signature, key your fulfillment off the event id, and a duplicate delivery becomes a no-op. The same intent that confirmed in the browser is the one carried in the event, so your reconciliation never has to guess.
- Verify with the endpoint secret and reject anything whose signature or timestamp does not check out.
- Store the event id and skip events you have already processed — deliveries are at-least-once by design.
- Replay any past event from the dashboard or CLI while you build, so testing never waits on real traffic.
// verify the signature, then dedupe const event = wv.webhooks.verify( rawBody, req.headers['naturpay-signature'], process.env.WV_WHSEC, ); if (await seen(event.id)) return 200; switch (event.type) { case 'payment.succeeded': await fulfill(event.data.intent); break; case 'payment.failed': await notify(event.data.intent); break; } await remember(event.id);
The error model
Failures you can branch on, not parse
Errors are part of the contract. Each one has a category you can switch on, a machine code that never changes wording, and a buyer-safe message — so declines, validation failures and rate limits each get the handling they deserve.
The card was declined
Carries a decline_code like insufficient_funds you can map to copy. Show the message, keep the intent, let the buyer try another method.
The request was malformed
Names the offending field and reason, returned as a 400 before any money moves. Fix the call and retry safely with the same key.
Slow down and retry
A 429 with a Retry-After header tells you exactly when to come back. The SDK respects it automatically with jittered backoff.
Before you ask
Questions about the API
Do I confirm the intent on the server or in the browser?
Either, depending on your flow. Most integrations let the element confirm in the browser with the client secret, which keeps card data off your servers and handles 3-D Secure inline. For off-session or merchant-initiated charges you confirm on the server with a saved method instead. Both paths land on the same intent and emit the same events.
What exactly does the idempotency key protect against?
A timed-out or retried write. If a request never returns — a dropped connection, a proxy timeout — you can safely send it again with the same key and Naturpay returns the original result rather than creating a second intent or a second charge. Keys are scoped to your account and remembered for 24 hours.
How do I tell a real decline from a temporary failure?
By the error type. A card_error with a decline_code is a genuine decline you surface to the buyer; a rate_limited or a 5xx is transient and safe to retry with backoff and the same idempotency key. The SDK already distinguishes the two and retries only the ones that should be retried.
Can I capture a different amount than I authorized?
Yes. With manual capture you authorize at checkout and later capture the full amount, a smaller amount, or nothing at all if you release the hold. The intent tracks the authorized and captured amounts separately, which is exactly what you need for ship-on-fulfillment or weight-based pricing.
Is the webhook the only way to know a payment succeeded?
No, but it should be the source of truth. The confirm call returns the updated intent immediately, which is fine for showing the buyer a result. For fulfillment, key off the signed webhook so a closed browser tab or a flaky connection never leaves an order unshipped — and dedupe on the event id since delivery is at-least-once.
Which SDKs wrap the API?
Server SDKs cover Node, Python, Ruby, Go and PHP, each with typed methods for intents, refunds, customers and webhooks. In the browser, the same API sits under the React and Vue packages, the Web Components build and a vanilla JS SDK. If your language is not on the list, the REST API is fully documented and every SDK is a thin wrapper over it.
Move your first intent through Naturpay
Grab a test key, create an intent and confirm it from the element in a few lines. Trigger declines and replay webhooks against the sandbox, then talk to an engineer when you are ready to go live.