Build your integration
Not starting from scratch? How do I…? maps a specific question to the one page that answers it.
A sequence, not a topic list. Every page linked below is worth reading on its own; 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.
A 403 auth_key_type_forbidden on step 2 or 3 means a publishable key was
sent where a secret key is required: creating a session and reading it back
both need your vp_sk_* key. Only capabilities (step 1) accepts either key
type. So a 403 here means the wrong key, not a broken one. See
API key types for the full split.
→ 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" |
Which card or amount produces each row depends on your account's test path: Test mode.
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 10 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.
Do not skim the observability rows. Logging the X-Request-Id header from
every error response is the one people leave for later; 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.