Build your integration
A sequence, not a topic list. Every page linked below is worth reading on its own — but read in the order they appear in the sidebar, they assume each other. This page puts them in the order you actually need them, says what breaks if you skip a step, and tells you how to prove each one landed.
Follow it start to finish and you will have taken a real payment, saved a card if you need one, received the outcome reliably, and tested the failure paths before a buyer finds them.
It does not restate the reference. Each step is a short "why, and what to watch for", then a link to the page that covers it properly. If a step already looks familiar, skip it — nothing here is a prerequisite for reading the next thing.
Step 0 — Decide what you are building
Do this before writing anything. The two integration shapes differ in where the card fields live, who handles 3-D Secure, and how much PCI scope you take on. Choosing wrong is a rewrite, not a refactor.
→ Compare the integration paths — a capability matrix, a decision tree, and a per-path 3-D Secure table.
How to know you're done: you can name which path you're on, and why.
Step 1 — Get keys, and prove they work before you write code
Activate a sandbox from the dashboard. You get a secret key (vp_sk_test_*), a
publishable key (vp_pk_test_*) and a webhook signing secret (whsec_*).
Then spend five minutes proving the key works, before any integration code exists. A key problem found now is five minutes; found in Step 3 it looks like a bug in your card form.
# 1. The key authenticates, and tells you what your account can do.
curl https://checkout.vonpay.com/v1/capabilities \
-H "Authorization: Bearer vp_sk_test_YOUR_KEY"
# 2. You can create a session.
curl https://checkout.vonpay.com/v1/sessions \
-H "Authorization: Bearer vp_sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: smoke_test_session" \
-d '{"amount":1499,"currency":"USD","successUrl":"https://example.com/ok","cancelUrl":"https://example.com/no"}'
# 3. You can read it back.
curl https://checkout.vonpay.com/v1/sessions/SESSION_ID \
-H "Authorization: Bearer vp_sk_test_YOUR_KEY"
How to know you're done: three 2xx responses. A 401 on any of them means
the key is wrong or mistyped — check you copied the whole value, and that its
mode matches the environment you are calling. None of these three calls rejects a
key for being publishable: /v1/capabilities reads account metadata that either
key type may consult, and session creation accepts both by design. See
API key types for which endpoints do require a secret.
→ Sandbox setup · API keys · Capabilities
Step 2 — Take one payment, end to end
Get a single successful payment working before you add anything else. Not saved cards, not wallets, not 3-D Secure handling — one charge, one outcome.
Pick the quickstart for the path you chose in Step 0:
- Hosted checkout — you redirect, we host the page.
- Embedded fields — the card fields render inside your page.
- Payments API — you already hold a saved card or are charging server-side.
On embedded fields, the SDK must be pointed at the same environment your keys
belong to. A test key with a production apiBaseUrl fails in a way that looks
like a broken card form rather than a configuration error.
How to know you're done: one payment reaches succeeded, and you saw it in
the dashboard — not just in your own logs.
Step 3 — Receive the outcome somewhere you can trust
This is the step most integrations get wrong, and the failure is silent: orders that were paid for never get fulfilled.
The redirect back to your site is not proof of payment. It is signed, so you know it came from us — but a declined payment is signed exactly as validly as an approved one. Fulfil from the webhook.
→ Webhooks — subscribe, verify, respond. → Verifying a signature — constant-time compare, 5-minute replay window. → Which events exist.
How to know you're done: you can point at the line of your code that marks an order paid, and it is inside a webhook handler.
Step 4 — Save a card, only if you need one
Skip this entirely if you take one-off payments. If you bill again later — subscriptions, instalments, cards on file — the requirements are strict, ordered, and fail silently weeks later if you miss one.
→ Recurring and saved cards — the four steps in order, including the consent declaration that cannot be added afterwards.
A card saved without the right consent cannot be repaired later. There is no endpoint that adds it after the fact — you have to collect the card again. The first charge works; the renewal is what declines.
Step 5 — Handle what goes wrong
Everything above is the happy path. This is where a production integration is actually made or lost.
→ Reconciliation — what to do about an event you missed, a request that timed out, a response you never received. → Troubleshooting — every error code, what it means, what to do, and whether it is safe to retry.
How to know you're done: you know what your code does when a charge request
times out. "Retry it" is the wrong answer unless you sent an
Idempotency-Key.
Step 6 — Test the behaviour, not just the configuration
This is the step nobody does, and it is the one that catches real bugs.
The go-live checklist verifies your setup — keys, HTTPS, secret storage, replay windows. Every item there can pass while your handler still double-charges a buyer on a redelivered event. These cases test what your code does.
Run every one of these in sandbox before you go live.
Payment outcomes
| # | Test | Why it matters | Done when |
|---|---|---|---|
| 1 | A successful payment | The happy path | Order marked paid, exactly once |
| 2 | A declined payment | Most integrations only ever test success | Buyer sees a useful message; no order created |
| 3 | A 3-D Secure challenge that succeeds | The challenge redirects away from your page and back | Order paid after the return |
| 4 | A 3-D Secure challenge that fails | The buyer returns to your site having not paid | No order created; buyer can retry |
| 5 | An expired / insufficient-funds card | These carry distinct failure codes worth surfacing differently | Your UI distinguishes "try another card" from "try again" |
On the Vonpay sandbox emulator the test cards behave as documented. If your account routes test traffic to a real processor's sandbox instead, those numbers carry no special meaning — the processor decides, and a card listed here as a decline can come back approved.
This has been measured on a live account. If a "decline" card approves, your
integration is not broken and neither is the card table — you are on the other
route. On hosted checkout you can force a decline by amount instead: set the
session amount to 200 minor units.
Webhook handling
| # | Test | Why it matters | Done when |
|---|---|---|---|
| 6 | Send the same event twice | We retry on failure, so duplicates are normal, not exceptional | The order is fulfilled once. This is the single highest-value test on this page |
| 7 | Send an event with a tampered signature | An unverified endpoint will accept anything anyone posts to it | Rejected, nothing written |
| 8 | Send an event with a timestamp older than 5 minutes | Replay protection | Rejected |
| 9 | Make your handler return a 500 | Ours will retry; yours must survive that | Retry arrives and is handled cleanly |
| 10 | Take longer than 30 seconds to respond | Slow handlers get retried, causing duplicates | Heavy work moved out of the request |
Retries and duplicates
| # | Test | Why it matters | Done when |
|---|---|---|---|
| 11 | Send the same charge twice with the same Idempotency-Key | This is the retry your own code will do after a timeout | One charge. The second returns the original result |
| 12 | Send the same charge with a different key | Proves you understand what the key does | Two charges — which is why you must never vary it on a retry |
| 13 | Refund the same payment twice with the same key | A retried refund must not pay the buyer twice | One refund; the replay is flagged rather than issued |
It is the only test here designed to produce a wrong outcome. Doing it once, on purpose, in sandbox, is how the rule stops being abstract.
Step 7 — Go live
→ Go-live checklist — keys, HTTPS, secret storage, monitoring, and the account requirements.
One thing to add to it: log the X-Request-Id header from every error
response. It is the first thing support will ask for, and it turns a day of
back-and-forth into one message.
The short list of things not to do
Every one of these is covered in depth on the page linked beside it. Collected here because they are the mistakes that actually get made, and because they are easier to avoid than to find afterwards.
| Don't | Why | Where |
|---|---|---|
| Fulfil an order from the redirect | A decline is signed just as validly as an approval | Handle the return |
| Treat a pending result as a failure | Re-issuing on it charges or refunds twice | Payment intents |
| Mark an order failed on a client timeout | The charge may have succeeded after your request gave up | Reconciliation |
Add an attempt counter to an Idempotency-Key | Retrying is the case the key exists to collapse | Idempotency |
Mint a fresh key to get past a 409 | It removes the only thing preventing a double charge | Error codes |
| Charge again after an embedded charge-at-submit | Submit already took the money | Embedded quickstart |
Send setup_for_future_use on the charge instead of at collection | Consent is write-once and cannot be repaired later | Recurring and saved cards |
Omit return_url when 3-D Secure may trigger | The buyer completes the challenge and lands nowhere | 3-D Secure |
| Install a payment processor's own SDK alongside ours | Its tokens are not ours, and your page breaks when your account is routed elsewhere | Integration paths |
Dedupe webhooks on anything but the event id | A session can produce several events | Reconciliation |
Where to go next
- Troubleshooting — every error code with what to do about it. Bookmark this one.
- API reference — every endpoint, once you know what you're looking for.
- Building with an AI agent? — the machine-readable index and what it covers.