Home
Getting Started

Quickstart

From zero to a test payment in under five minutes.

This guide takes you from a fresh sandbox key to a successful test payment, then to the 3-D Secure flow. Don't have an account yet? You can try it on staging with a shared test key first.

Prerequisites

You'll need:

  • An HTTP client (curl, Postman, or your language's HTTP library)
  • A basic understanding of JSON
  • A Paayed dashboard account, or the shared staging key below

Try it on staging without an account

Want to try the API before you sign up? Use the shared sandbox key below with the same base URL as the rest of this guide. Test keys (pyd_secret_test_…) are routed to the sandbox automatically, so nothing here touches real money.

SettingValue
Base URLhttps://gateway.uk.paayed.com
Secret keypyd_secret_test_v1m2eyFX4KpouhOxfatUKwtjcbPKkxNp
curl 'https://gateway.uk.paayed.com/v1/payments' \
  -H 'Authorization: Bearer pyd_secret_test_v1m2eyFX4KpouhOxfatUKwtjcbPKkxNp'

You can skip step 1 below and use this key wherever the guide says pyd_secret_test_your_key_here. Use only the test cards on this page, never real card numbers.

Warning

This key is public and shared by everyone who reads these docs, so other people can see whatever you create with it. Only use test cards and made-up details — never real customer data. Staging can be reset or changed at any time. When you're ready to build for real, create your own key in the dashboard.

1. Get your API key

Open Developers → API keys in the dashboard and create a sandbox key. It's shown once — copy it now. It looks like this:

pyd_secret_test_4f2c9d8e1f7a91RSdSRRzr8PMRU...

Sandbox and live keys use the same hostname, gateway.uk.paayed.com — the key decides the mode: a test key (pyd_secret_test_…) goes to the sandbox and a live key (pyd_secret_live_…) to production. They are created separately, in the sandbox and in your production account, so a sandbox key never works against live data.

The first time an account is used in a mode, the gateway takes up to a couple of minutes to activate it. We start that when your key is created, so you will rarely notice — but if a request in the first few minutes returns `403 usage_plan_missing`, wait a moment and send it again. It happens once for sandbox and once for live.

2. Take your first payment

curl -X POST 'https://gateway.uk.paayed.com/v1/payments' \
  -H 'Authorization: Bearer pyd_secret_test_your_key_here' \
  -H 'Idempotency-Key: 5f1e6a6c-9d3f-4a2f-9a1e-2b7c1d9e4f88' \
  -H 'Content-Type: application/json' \
  -d '{
    "amount": 42.50,
    "currency": "GBP",
    "reference": "ORDER-10023",
    "payment_method": {
      "type": "card",
      "card": {
        "number": "4242424242424242",
        "expiry_month": 12,
        "expiry_year": 2029,
        "cvc": "123",
        "holder_name": "A Cardholder"
      }
    }
  }'

You should get back:

{
  "id": "pay_01JBQ7X2F9KDNW3M8T",
  "object": "payment",
  "status": "succeeded",
  "amount": 42.50,
  "amount_captured": 42.50,
  "currency": "GBP",
  "reference": "ORDER-10023",
  "livemode": false
}

Idempotency-Key is mandatory on every write — a request without it is rejected with 400.

For payments that submit card details, you can provide the cardholder's IPv4 or IPv6 address in the optional root ip_address field. If omitted or null, Paayed uses the API caller's IP resolved through its trusted proxy configuration. For server-to-server requests, this fallback is your server's public IP; supply ip_address to send the cardholder's IP instead. Paayed's hosted checkout captures the submitting browser's IP automatically, including the staff browser for manual card entry.

Note

Authorization without capture ("capture": false), partial capture and cancelling an authorization are not in this version. A request with capture: false is a 422 that says so. They arrive in a later dated version and will be announced in the changelog.

For an initial scheduled recurring card payment, add the following fields to the payment request, along with the customer, card and browser details:

{
  "store": true,
  "recurring": {
    "interval": "month",
    "end_date": "2027-10-19"
  }
}

recurring requires store: true, an interval of day, week or month, and an explicit end date on or after today in UTC. Choose the end date agreed with the cardholder; do not invent one for an open-ended subscription. The initial DNA request then uses periodicType: recurring, sequenceType: initial, initiator: customer, a minimum frequency of 1, 7 or 28 days, and the chosen expiration date formatted as YYYYMMDD. Monthly billing still follows the subscription's calendar-month schedule.

This declares the schedule during authentication; it does not create a subscription. After the payment succeeds and returns a stored payment method, create the subscription separately with the same interval and end date and your next charge date. Supplying only store: true keeps the existing unscheduled credential-on-file (ucof) setup. Omit the setup object when charging an already stored card; that request uses initiated_by and sequence instead. Retries must reuse the same schedule and idempotency key.

3. Try 3-D Secure

For a cardholder-present payment, include browser_details collected from the cardholder's browser. The object is optional for compatibility, but all ten fields must be supplied together when present. Do not include it for merchant-initiated stored-card payments.

FieldBrowser value or source
screenWidth, screenHeightscreen.width, screen.height (integers from 1 to 999999)
screenColorDepthscreen.colorDepth (1, 4, 8, 15, 16, 24, 32 or 48)
timeZonenew Date().getTimezoneOffset() (minutes, -840 to 840)
languagenavigator.language (1 to 8 characters)
javaEnablednavigator.javaEnabled() where supported, otherwise false
javaScriptEnabledtrue when collected by JavaScript
challengeWindowSize01 for 250x400, 02 for 390x400, 03 for 500x600, 04 for 600x400, or 05 for full screen
acceptHeader, userAgentOriginal browser Accept and User-Agent headers (non-empty, at most 2048 characters each)

Send these fields inside a top-level browser_details object in POST /v1/payments. Your server must forward the browser's headers rather than its own HTTP client headers. Save the submitted browser values for the payment attempt and reuse them unchanged with the same Idempotency-Key, even if the browser is resized or reloaded.

Sandbox outcomes are deterministic, driven by the card number. Use any future expiry and any three-digit CVC:

Card numberOutcomedecline_code
4242 4242 4242 4242Approved
4000 0000 0000 0002Declined, genericdo_not_honor
4000 0000 0000 9995Declined, insufficient fundsinsufficient_funds
4000 0000 0000 9987Declined, lost or stolenlost_or_stolen
4000 0000 0000 3220Requires 3-D Secure, then succeeds
4000 0000 0000 3063Requires 3-D Secure, then fails authenticationauthentication_failed
4000 0000 0000 0069Expired cardexpired_card
4000 0000 0000 0119Processing error — exercise your retry pathprocessing_error
4000 0000 0000 0259Succeeds. Reserved for a dispute the next day once disputes ship
4000 0000 0000 5126Succeeds, but every refund against it fails

Any other number that passes the Luhn check is approved. A payment whose amount in minor units ends in 13 (for example 10.13) is declined for insufficient funds whatever the card — handy for testing subscription retries.

A declined or failed attempt comes back with HTTP 402 and the complete payment object, including its ID. Read status and decline_code; GET /v1/payments/{id} returns 200 with the payment's current status.

Most cards in the UK and Europe require the cardholder to authenticate before a payment is approved, so your integration has to handle it.

Send the 4000 0000 0000 3220 card and you'll get back 200 with status: "requires_action" and a next_action.challenge. Submit a browser form to next_action.challenge.acs_url using POST and application/x-www-form-urlencoded, with these fields:

Form fieldValue from next_action.challenge
creqcreq, unchanged
threeDSSessionDatathree_ds_session_data, unchanged when present; omit when null

Keep both values opaque: do not decode, re-encode or replace them. Opening acs_url in a new tab with a normal link does not submit these fields; use a form POST whether the challenge opens in a tab or an iframe.

With managed authentication callbacks, the payment platform receives the issuer's result. If your own integration receives a cres result instead, submit it from your server to POST /v1/payments/{id}/confirm. Read the final payment status from the webhook or GET /v1/payments/{id}.

To bring the cardholder back to your site after the challenge, include an optional HTTPS return_url in the original POST /v1/payments request:

{
  "return_url": "https://fundd.org/payment/donation-123/result"
}

This is an additional field on your normal card-payment request. Paayed receives the issuer's authentication result, confirms it and returns the browser to that URL with a 303 redirect. You do not need to host an ACS callback endpoint or send a separate confirmation request for this flow. Continue forwarding both challenge form fields as described above. If you omit return_url or send null, the existing managed callback flow remains in use.

The URL is a navigation destination, not proof of payment. Your result page must retrieve the payment using the ID from the original response, or wait for the webhook. The payment may still be processing if confirmation could not be resolved immediately. A payment that completes without a challenge returns its outcome in the original API response; your application handles navigation in that case.

In the sandbox, acs_url is a page of ours that lets you complete or abandon the challenge and returns to the supplied URL afterwards.

Warning

The cardholder may close the tab before returning to your site. Use webhooks to receive the final outcome and retrieve the payment from your server to display its status. For live DNA payments awaiting authentication or reconciliation, GET /v1/payments/{id} can check the provider and recover a completed payment when its callback was missed. Checks are throttled; if the provider is unavailable or authentication is unfinished, the saved status is returned. This does not submit a new charge or complete the bank challenge. Never treat a browser redirect alone as proof of payment.

In the sandbox you can seed your account from the dashboard, under Developers, Sandbox, with three customers, six payments and two refunds (one of which fails), all with real events and deliveries. The same page resets the sandbox: customers, stored cards, subscriptions, endpoints and events go; payments and refunds are kept.

4. Refund it

curl -X POST 'https://gateway.uk.paayed.com/v1/payments/pay_01JBQ7X2F9KDNW3M8T/refunds' \
  -H 'Authorization: Bearer pyd_secret_test_your_key_here' \
  -H 'Idempotency-Key: 3d70a1b2-...' \
  -H 'Content-Type: application/json' \
  -d '{ "reason": "requested_by_customer" }'

Omit amount to refund what remains. Refunds are asynchronous — the response is 201 with status: "pending", and it resolves to succeeded or failed via webhook (refund.succeeded, refund.failed). Don't tell the customer the money is on its way until it has succeeded.

5. Or let Paayed take the card

Sending card numbers from your server puts that server in PCI DSS scope. A hosted session avoids it: the cardholder enters their card on Paayed's page, and your systems only ever see identifiers (typically PCI DSS SAQ A).

curl -X POST 'https://gateway.uk.paayed.com/v1/hosted_sessions' \
  -H 'Authorization: Bearer pyd_secret_test_your_key_here' \
  -H 'Idempotency-Key: 8c2d4e1a-7b3f-4c9d-a1e2-3f4b5c6d7e8f' \
  -H 'Content-Type: application/json' \
  -d '{
    "amount": 25.00,
    "currency": "GBP",
    "success_url": "https://shop.example.com/thanks",
    "cancel_url": "https://shop.example.com/basket"
  }'

The response has a url: redirect the cardholder there. 3-D Secure happens on the page. When they finish, they come back to your success_url with session_id and reference added, or to cancel_url if they leave. Confirm the outcome from the hosted_session.completed webhook or by retrieving the session — never from the return alone. An unpaid session expires after 24 hours.

To start a subscription without touching card data, add recurring:

"recurring": { "interval": "month", "total_payments": 12 }

The cardholder pays the first installment on the page and agrees to the schedule. When that payment succeeds the session's subscription is set, and the rest are charged automatically (subscription.charged each time). Intervals are day, week and month; end it with end_date or total_payments, or leave both out to run until cancelled.

The Hosted sessions guide covers the whole flow: every field, the return URL, webhooks, statuses and a go-live checklist.

6. Explore the API reference

Head to the API Reference for every endpoint, request schema and response format, with code samples in cURL, JavaScript, Python and more.

Next steps

Built with Sourcey