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:read and payment_intents:write scopes. 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 an rk_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 ─► canceled

An intent created with a payment_method starts in requires_confirmation; without one it starts in requires_payment_method.

StatusMeaningWhat you do next
requires_payment_methodNo payment method yet, or the last attempt was declinedConfirm with a payment_method, or cancel
requires_confirmationReady to chargeConfirm
requires_actionThe customer must authenticate (3D Secure)Send the customer to next_action.redirect_to_url.url
processingBeing charged, or authorised and waiting for capture (capture_method: manual)Capture, or wait
succeededPaid. TerminalFulfil the order
canceledCancelled. TerminalNothing; 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.

Statuserror.codeMeaningWhat to do
400payment_intent_unexpected_stateThe action is not allowed in the intent's current status, for example capturing an automatic-capture intentRetrieve the intent and act on its current status
400variesThe request body failed validationFix the request
401authentication_failed, invalid_api_key, api_key_revoked and similarMissing, invalid or revoked keyCheck the key and its scopes
404resource_missingNo such intent for this accountCheck the ID and the environment
429rate_limit_exceededToo many requestsWait for the Retry-After seconds, then retry with the same idempotency key
5xxinternal_errorThe outcome is unknownRetrieve 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.

Owner: david · Reviewed 30 Sep 2026 · Next review 30 Oct 2026

On this page