Built for developers
One REST API for payments, refunds, payouts and subscriptions. Predictable JSON, idempotent requests, signed webhooks and a sandbox that behaves exactly like production — so your first payment takes an afternoon, not a sprint. Sandbox keys are issued the moment you sign up; a live account is approved in 2–5 business days, and the code you wrote against the sandbox does not change.
Your first payment in four steps
Everything below runs against the sandbox with no contract, no card of your own and nothing to install beyond an HTTP client. Amounts are integers in minor units (4990 is €49.90), every response is JSON and every request that creates something accepts an Idempotency-Key, so a retried call never charges twice.
Get sandbox keys
Sign up in the dashboard and open Developers → API keys. You get a public key for the browser widget and a secret key for your server. Sandbox keys start with sk_test_; live keys with sk_live_.
Create a payment
Call POST /v1/payments with an amount, a currency and a return URL. The response carries the payment id, its status and a checkout_url for the hosted page — or a client secret if you render the widget yourself.
Redirect / confirm 3DS
Send the customer to checkout_url. Target tokenises the card and runs 3-D Secure 2.2 — frictionless where the issuer allows it, a challenge screen where it insists — then returns the customer to your return_url.
Listen to webhooks
Register an HTTPS endpoint, verify the signature and mark the order paid on payment.succeeded. Never trust the redirect alone: browsers close, networks drop — the webhook is the source of truth.
Create a payment
bash# Basic auth: secret key as username, empty password curl https://api.target.co.uk/v1/payments \ -u "sk_test_9f2b4c7e1a6d3e8f:" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-10482" \ -d '{ "amount": 4990, "currency": "EUR", "description": "Order #10482", "customer": { "email": "anna@example.com" }, "return_url": "https://shop.example.com/return", "metadata": { "order_id": "10482" } }'
json · 201 Created{ "id": "pay_7f3c2a9e", "object": "payment", "status": "pending", "amount": 4990, "currency": "EUR", "description": "Order #10482", "checkout_url": "https://pay.target.co.uk/c/7f3c2a9e", "three_ds": { "required": true, "version": "2.2.0" }, "livemode": false, "metadata": { "order_id": "10482" }, "created_at": "2026-09-14T09:12:41Z" }
javascriptimport Target from '@target-fs/node'; const target = new Target(process.env.TARGET_SECRET_KEY); const payment = await target.payments.create({ amount: 4990, // minor units → €49.90 currency: 'EUR', description: 'Order #10482', customer: { email: 'anna@example.com' }, return_url: 'https://shop.example.com/return', metadata: { order_id: '10482' }, }, { idempotencyKey: 'order-10482' }); res.redirect(payment.checkout_url);
json · 201 Created{ "id": "pay_7f3c2a9e", "object": "payment", "status": "pending", "amount": 4990, "currency": "EUR", "description": "Order #10482", "checkout_url": "https://pay.target.co.uk/c/7f3c2a9e", "three_ds": { "required": true, "version": "2.2.0" }, "livemode": false, "metadata": { "order_id": "10482" }, "created_at": "2026-09-14T09:12:41Z" }
pythonimport os import target client = target.Client(api_key=os.environ["TARGET_SECRET_KEY"]) payment = client.payments.create( amount=4990, # minor units → €49.90 currency="EUR", description="Order #10482", customer={"email": "anna@example.com"}, return_url="https://shop.example.com/return", metadata={"order_id": "10482"}, idempotency_key="order-10482", ) return redirect(payment.checkout_url)
json · 201 Created{ "id": "pay_7f3c2a9e", "object": "payment", "status": "pending", "amount": 4990, "currency": "EUR", "description": "Order #10482", "checkout_url": "https://pay.target.co.uk/c/7f3c2a9e", "three_ds": { "required": true, "version": "2.2.0" }, "livemode": false, "metadata": { "order_id": "10482" }, "created_at": "2026-09-14T09:12:41Z" }
php<?php require 'vendor/autoload.php'; $target = new \Target\Client(getenv('TARGET_SECRET_KEY')); $payment = $target->payments->create([ 'amount' => 4990, // minor units → €49.90 'currency' => 'EUR', 'description' => 'Order #10482', 'customer' => ['email' => 'anna@example.com'], 'return_url' => 'https://shop.example.com/return', 'metadata' => ['order_id' => '10482'], ], ['idempotency_key' => 'order-10482']); header('Location: ' . $payment->checkout_url);
json · 201 Created{ "id": "pay_7f3c2a9e", "object": "payment", "status": "pending", "amount": 4990, "currency": "EUR", "description": "Order #10482", "checkout_url": "https://pay.target.co.uk/c/7f3c2a9e", "three_ds": { "required": true, "version": "2.2.0" }, "livemode": false, "metadata": { "order_id": "10482" }, "created_at": "2026-09-14T09:12:41Z" }
The same code goes live when you swap sk_test_ for sk_live_. A payment stays pending until the customer finishes 3-D Secure, then becomes succeeded or failed — poll GET /v1/payments/{id} if you must, but a webhook will reach you first. Prefer not to redirect? The embedded widget keeps the customer on your page and uses the same payment object.
Your language, your platform
Official libraries wrap the API with typed models, automatic retries and idempotency keys generated for you. If your shop runs on a platform, install the plugin instead and write no code at all: payment methods, refunds and order statuses sync straight into your admin panel. Everything is open source and versioned with semver.
Server libraries
Node.js, Python, PHP and Java on npm, PyPI, Packagist and Maven Central. Each release is tested against the live API before it ships; breaking changes only in a major version, with a migration note.
Client & mobile
The JavaScript widget for the browser plus native Swift and Kotlin SDKs: card form, Apple Pay and Google Pay, 3-D Secure challenge handled in a sheet. Card data goes from the device straight to Target — your servers stay out of PCI scope.
Plugins
Shopify, WooCommerce, Magento, OpenCart, PrestaShop and Tilda. Install, paste your keys, pick the payment methods — live in about twenty minutes. Building a platform of your own? See the partner programme.
Every event,in real time
Target pushes a signed JSON event to your endpoint the moment anything changes: an authorisation, a refund, a dispute, a payout leaving our bank. Events are delivered at least once and in order per object; if your endpoint does not answer 2xx we retry with exponential back-off for 72 hours, and you can replay any event from the dashboard.
| Event | When it fires | What you usually do |
|---|---|---|
| payment.succeeded | The issuer approved the authorisation and, unless capture is manual, the amount was captured. | Mark the order paid and start fulfilment. Store the payment id against the order for refunds later. |
| payment.failed | The issuer declined, 3-D Secure failed or the customer abandoned checkout after 30 minutes. | Release the reserved stock and show a retry with the decline_code from the payload. |
| refund.created | A full or partial refund was accepted for processing — from the API, the dashboard or a plugin. | Update the order total, credit loyalty points and e-mail the customer; funds reach the card in 3–5 days. |
| chargeback.opened | The cardholder disputed a payment with their bank. The amount plus the €15 fee is held from your balance. | Upload evidence in the dashboard within 7 days. Our alerts often reach you before the dispute does. |
| payout.sent | A settlement batch left Target's bank account towards your card, IBAN or e-wallet. | Reconcile the batch against the payout report attached to the event. |
| subscription.renewed | A recurring charge succeeded for the next billing period on a saved card or network token. | Extend access to the next period and send the invoice. A failed renewal fires payment.failed instead. |
The envelope
Every event has the same shape: an id, a type, a timestamp and the full object in data. Use the id to de-duplicate — the same event may arrive twice, never a different one under the same id.
json · POST https://shop.example.com/hooks/target{ "id": "evt_01j7q3x8k2", "type": "payment.succeeded", "created_at": "2026-09-14T09:12:58Z", "livemode": true, "data": { "id": "pay_7f3c2a9e", "status": "succeeded", "amount": 4990, "currency": "EUR", "captured": true, "three_ds": { "result": "authenticated", "flow": "frictionless" }, "metadata": { "order_id": "10482" } } }
Verify the signature
Each call carries a Target-Signature header: a timestamp and an HMAC-SHA256 of timestamp.body keyed with your endpoint secret. Check it on the raw body, before parsing, and reject anything older than five minutes.
- Answer 2xx within 10 seconds; do the work afterwards
- One secret per endpoint, rotated from the dashboard without downtime
- Sandbox events carry
livemode: false— same shape, same signature
javascriptimport crypto from 'node:crypto'; export function verify(rawBody, header, secret) { // Target-Signature: t=1726305178,v1=5f1c…e2 const { t, v1 } = Object.fromEntries( header.split(',').map(part => part.split('=')) ); const expected = crypto .createHmac('sha256', secret) .update(`${t}.${rawBody}`) .digest('hex'); const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300; return fresh && crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(v1) ); }
Sandbox & test cards
The sandbox is a full copy of production — the same endpoints, the same dashboard, the same webhooks — only the money is not real. Use the cards below to script every outcome your integration must handle, from a clean approval to a 3-D Secure challenge and a decline. Any future expiry date and any three-digit CVC will do.
| Card number | Brand | Result | Use it to test |
|---|---|---|---|
| 4111 1111 1111 1111 | Visa | Succeeded | The happy path: frictionless 3-D Secure, instant authorisation, a payment.succeeded webhook within a second. |
| 4000 0000 0000 0002 | Visa | Declined | Your decline handling and retry screen. The payment fails with decline_code: do_not_honour and fires payment.failed. |
| 4000 0000 0000 3220 | Visa | 3DS challenge | The challenge flow: the customer sees a one-time-code screen (use 1234), then returns to your return_url. |
| 5555 5555 5555 4444 | Mastercard | Succeeded | Brand detection in the card form, Mastercard routing and the scheme-specific fields in the payment object. |
- Base URL
- The same host as production,
https://api.target.co.uk. The environment is chosen by the key:sk_test_talks to the sandbox,sk_live_to live. - Special amounts
- Any amount works on the cards above. End it in 05 for
insufficient_funds, 10 forfraud_suspected, 20 forexpired_card— the decline codes you will see in production. - Refunds & payouts
- Refunds succeed instantly. Payouts move to
sentafter 60 seconds so you can test thepayout.sentwebhook without waiting for a bank. - Webhooks
- Register up to 10 endpoints per environment. Every event can be re-sent from the dashboard, and a “Send test event” button lets you check your signature code before the first real payment.
- Data
- Sandbox objects are kept for 90 days and are never mixed with live data. Teams can share one sandbox with per-user roles, exactly as in the live dashboard.
The endpoints you will call every day
The API is resource-oriented: create an object with POST, read it with GET, and every list supports cursor pagination and filtering by status, date and metadata. Amounts are integers in minor units, timestamps are ISO 8601 in UTC, and every error carries a machine-readable code next to a sentence a human can act on.
- POST/v1/payments
- Create a payment. Returns a
checkout_urlfor the hosted page or aclient_secretfor the widget. Supports automatic or manual capture, 3-D Secure, saved cards, network tokens and up to 20 metadata keys. - GET/v1/payments/{id}
- Retrieve a payment with its current status, the 3-D Secure result, the acquirer response code, fees charged and the list of refunds. Add
?expand=customerto inline related objects. - POST/v1/refunds
- Refund a captured payment in full or in part. Several partial refunds may be issued up to the captured amount; each one fires
refund.createdand appears on the customer’s statement in 3–5 days. - POST/v1/payouts
- Send funds from your balance to a card, IBAN or e-wallet in any of 25 settlement currencies. Batch up to 1,000 recipients in one request; each item is validated and reported separately.
- POST/v1/subscriptions
- Start a recurring plan on a saved card: interval, trial period, proration and a retry schedule for failed renewals. Renewals fire
subscription.renewed; see recurring payments. - GET/v1/balances
- Available and pending balance per settlement currency, the date and amount of the next scheduled payout and the sum currently held against open chargebacks.
Authentication
Your secret key over HTTP Basic auth, TLS 1.2 or higher. Platforms that act for many merchants use OAuth2 client credentials and receive a short-lived token scoped to one merchant account.
Versioning
Date-based. Your account is pinned to the version current when you created it; send Target-Version: 2026-06-01 to try a newer one per request before you upgrade the account.
Errors
Conventional HTTP codes — 4xx for something in the request, 5xx for something on our side — with a JSON body carrying error.code, error.message and, where relevant, the offending error.param. Card declines are 402 with the issuer’s decline code.
Boring, on purpose
Payments infrastructure should be the least interesting part of your stack. The API runs in three European regions behind independent acquirer connections, deploys without downtime and is measured from outside every ten seconds. The numbers below are the trailing twelve months, not a launch-day snapshot.
Rate limits
Every API key may make 600 requests per minute, counted separately for sandbox and live. Above that we answer 429 Too Many Requests with a Retry-After header instead of dropping the call; short bursts of up to 100 requests per second are tolerated for five seconds so a flash sale does not trip the limit. Three headers on every response tell you where you stand, and the SDKs back off automatically. Marketplaces and ticketing merchants that need more get a higher ceiling on Enterprise — ask your manager.
httpHTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1726305180
Idempotency
Send an Idempotency-Key header — any string up to 255 characters, your order id is a good one — with every POST. If the connection drops and you retry with the same key and the same body within 24 hours, you receive the original response; the customer is never charged twice. The same key with a different body is rejected with 422 idempotency_key_reused, which is how you catch a bug before it costs money. The SDKs generate a key per call unless you pass your own.
httpPOST /v1/payments Idempotency-Key: order-10482 → 201 Created first call → 201 Created retry, same body → 422 Unprocessable new body
Live status and a 90-day incident history are in the dashboard under Developers → Status, and every incident is announced to your webhook endpoint as it opens and closes. Questions about the API go to the same 24/7 team that runs it; on Business and Enterprise your personal manager can put an engineer on the call.