Payment Intents
Charge a customer's saved payment method from your server with the Payment Intents API - create, confirm, capture and cancel.
A payment intent is how your server takes a payment through the RevKeen API. It tracks one attempt to collect one amount from one customer, from creation through to success or cancellation.
Use a payment intent when your own server decides when to charge, for example a one-off charge on a card the customer has already saved. When the customer needs to enter card details, send them to hosted checkout or a checkout link instead: the API never accepts raw card numbers.
Before you start
- A secret API key with the
payment_intents:readandpayment_intents:writescopes. See Authentication. - A customer ID, and the ID of a payment method saved for that customer. See Customers.
- Use staging (
https://staging-api.revkeen.com/v2) and anrk_sandbox_*key until the flow works end to end. See Environments.
Lifecycle
requires_payment_method ─► requires_confirmation ─► processing ─► succeeded
▲ │ ▲
└──────────── declined ◄────────────────────┤ │
▼ │
requires_action (3D Secure)
Any status except succeeded ─► canceledAn intent created with a payment_method starts in requires_confirmation; without one it starts in requires_payment_method.
| Status | Meaning | What you do next |
|---|---|---|
requires_payment_method | No payment method yet, or the last attempt was declined | Confirm with a payment_method, or cancel |
requires_confirmation | Ready to charge | Confirm |
requires_action | The customer must authenticate (3D Secure) | Send the customer to next_action.redirect_to_url.url |
processing | Being charged, or authorised and waiting for capture (capture_method: manual) | Capture, or wait |
succeeded | Paid. Terminal | Fulfil the order |
canceled | Cancelled. Terminal | Nothing; create a new intent to try again |
succeeded and canceled are final: an intent in either status cannot be confirmed, captured, cancelled or changed.
Amounts and currency
Amounts are integers in the minor unit of the currency: 2500 in gbp is £25.00. If you leave currency out, RevKeen uses your account's default currency. Currency codes are returned in lower case.
Create and confirm
Create the intent, then confirm it to charge. The TypeScript examples use @revkeen/sdk.
import { RevKeen } from "@revkeen/sdk";
const client = new RevKeen({
apiKey: process.env.REVKEEN_API_KEY!,
baseUrl: "https://staging-api.revkeen.com/v2",
});
const intent = await client.paymentIntents.create({
amount: 2500, // £25.00
currency: "GBP",
customer: "3f0c5b2e-8a41-4d6f-9c1e-2b7a9d4e6f10",
payment_method: "7a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
description: "Sports massage, 60 minutes",
metadata: { order_id: "order_1042" },
});
const confirmed = await client.paymentIntents.confirm(intent.id);
if (confirmed.status === "succeeded") {
// Paid. Fulfil the order.
} else if (confirmed.status === "requires_action") {
// Send the customer to confirmed.next_action.redirect_to_url.url
} else if (confirmed.status === "requires_payment_method") {
// Declined. Read confirmed.last_payment_error.
}curl https://staging-api.revkeen.com/v2/payment-intents \
-H "x-api-key: $REVKEEN_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 2500,
"currency": "GBP",
"customer": "3f0c5b2e-8a41-4d6f-9c1e-2b7a9d4e6f10",
"payment_method": "7a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"description": "Sports massage, 60 minutes"
}'
curl -X POST https://staging-api.revkeen.com/v2/payment-intents/pi_.../confirm \
-H "x-api-key: $REVKEEN_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{}'The response is the payment intent object. Its id starts with pi_. You can also pass the payment_method on confirm instead of on create.
Authorise now, capture later
Set capture_method to manual to authorise the amount on confirm and take the money later. After a successful confirm the intent stays in processing, and amount_capturable shows how much you can capture.
const hold = await client.paymentIntents.create({
amount: 12000, // £120.00 authorised
currency: "GBP",
customer: "3f0c5b2e-8a41-4d6f-9c1e-2b7a9d4e6f10",
payment_method: "7a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
capture_method: "manual",
});
await client.paymentIntents.confirm(hold.id);
// Later: capture all or part of the authorised amount.
const captured = await client.paymentIntents.capture(hold.id, {
amount_to_capture: 9500, // £95.00
});Leave out amount_to_capture to capture the full authorised amount. You cannot capture more than amount_capturable, and you cannot capture an intent created with capture_method: automatic.
Cancel
Cancel an intent you no longer intend to charge. Any status except succeeded and canceled can be cancelled.
await client.paymentIntents.cancel("pi_...", {
cancellation_reason: "requested_by_customer",
});cancellation_reason is one of duplicate, fraudulent, requested_by_customer, abandoned or failed_invoice. To return money from a payment that has already succeeded, use a refund, not a cancel.
Retrieve and list
const current = await client.paymentIntents.get("pi_...");
const recent = await client.paymentIntents.list({
customer: "3f0c5b2e-8a41-4d6f-9c1e-2b7a9d4e6f10",
status: "succeeded",
limit: 20,
});Retrieve the intent whenever you need its current status, for example after a timeout or after the customer returns from authentication.
Customer authentication (3D Secure)
When the card issuer asks the customer to authenticate, the intent moves to requires_action and next_action.type is redirect_to_url. Send the customer to next_action.redirect_to_url.url. Pass return_url on confirm to choose where they come back to. When they return, retrieve the intent to read the outcome; do not treat the return itself as proof of payment.
Idempotency
Send an Idempotency-Key header on every create, confirm, capture and cancel, and reuse the same key when you retry the same operation. A retried request with the same key returns the stored result instead of charging again. Keys are kept for 24 hours. See Idempotency.
With the TypeScript SDK, set the header on a client you use for that one operation:
import { randomUUID } from "node:crypto";
// Store this key with your order so a retry reuses it.
const idempotencyKey = randomUUID();
const scoped = new RevKeen({
apiKey: process.env.REVKEEN_API_KEY!,
baseUrl: "https://staging-api.revkeen.com/v2",
headers: { "Idempotency-Key": idempotencyKey },
});
const intent = await scoped.paymentIntents.create({
amount: 2500,
currency: "GBP",
customer: "3f0c5b2e-8a41-4d6f-9c1e-2b7a9d4e6f10",
});If a create request times out, retry it with the same key, or list the customer's intents before creating another. Creating a second intent without the key risks charging twice.
Errors
Errors use the standard RevKeen shape: error.type, error.code and error.message. The SDK throws RevKeenAPIError, which carries the HTTP status and the response body.
| Status | error.code | Meaning | What to do |
|---|---|---|---|
400 | payment_intent_unexpected_state | The action is not allowed in the intent's current status, for example capturing an automatic-capture intent | Retrieve the intent and act on its current status |
400 | varies | The request body failed validation | Fix the request |
401 | authentication_failed, invalid_api_key, api_key_revoked and similar | Missing, invalid or revoked key | Check the key and its scopes |
404 | resource_missing | No such intent for this account | Check the ID and the environment |
429 | rate_limit_exceeded | Too many requests | Wait for the Retry-After seconds, then retry with the same idempotency key |
5xx | internal_error | The outcome is unknown | Retrieve the intent before doing anything else |
A declined card is not an HTTP error. The confirm call returns the intent with status requires_payment_method and a last_payment_error holding code, message and, for cards, decline_code. Ask the customer for another payment method, then confirm again or cancel.
import { RevKeenAPIError } from "@revkeen/sdk";
try {
const result = await client.paymentIntents.confirm("pi_...");
if (result.status === "requires_payment_method") {
console.warn("Declined:", result.last_payment_error?.message);
}
} catch (error) {
if (error instanceof RevKeenAPIError && error.status >= 500) {
// Unknown outcome: retrieve the intent before retrying.
const current = await client.paymentIntents.get("pi_...");
console.log("Current status:", current.status);
} else {
throw error;
}
}Webhooks
There are no payment_intent.* webhook events. The confirm and capture responses, and retrieving the intent, are the source of truth for an intent you create through the API.
Related
Owner: david · Reviewed 30 Sep 2026 · Next review 30 Oct 2026