Skip to main content

Payment Intent Object

A payment intent is the server-side record of one payment's lifecycle — authorize, capture, void, refund — driven entirely from your backend. You create one with POST /v1/payment_intents; it's what every integration path ultimately charges through. This page documents the object's shape. For the how-to (auth/capture/void/refund, MIT, 3DS), see the Payment Intents guide.

{
"id": "vpi_live_8x4n2pq7m1",
"status": "succeeded",
"amount": 4999,
"currency": "USD",
"capture_method": "automatic",
"card": { "brand": "visa", "last4": "4242" },
"created_at": "2026-06-23T15:30:00.000Z"
}

Core fields

FieldTypeAlways PresentDescription
idstringYesPayment intent ID — vpi_test_* (sandbox) or vpi_live_* (live).
statusstringYesCurrent lifecycle status. One of requires_action, authorized, captured, succeeded, voided, failed (see Status lifecycle).
amountintegerYesAmount in minor units (cents), ≥ 0.
currencystringYesISO 4217 currency code (3-letter uppercase).
capture_methodstringYesautomatic — capture immediately on a successful authorization. manual — authorize now, capture later via /capture.
next_actionobject | nullNoPresent when a server-redirect 3DS challenge is required: { "type": "redirect_to_url", "redirect_to_url": { "url": "…" } }. Redirect the buyer to url; the intent settles after the challenge.
client_confirmobject | nullNoEmbedded-checkout 3DS handle: { binder, client_secret }. Populated when status is requires_action on gateways that support client-side confirmation. Forward client_secret to the browser SDK (collection.submit({ paymentIntent: { id, action: { client_secret } } })). Coexists with next_action.
decline_codestring | nullNoNormalized decline reason on a failed intent (see Decline codes).
rule_codestring | nullNoWhich of your own provider rules refused the charge. Present only when decline_code is blocked_by_rule; null on every other outcome. Carries the custom code you set on that rule, with the provider's own prefix stripped — max 64 chars, and null if you left the rule unnamed. See Telling your rules apart.
actionstring | nullNoWhat to do about a decline — soft, hard, fix_and_retry or generic, derived from decline_code through one published mapping (see action).
decline_messagestring | nullNoBuyer-safe wording for the decline — the field to show a shopper. Not the same string as failure_reason, which is written for you and names the fraud, lost and stolen signals. See Showing a decline to the buyer.
payment_method_idstring | nullNoThe saved card this payment vaulted (vp_pmt_*), reusable for later merchant-initiated charges. null when the payment vaulted nothing — a guest payment with no buyer attached, or one that never succeeded. Returned by GET /v1/payment_intents/{id} only — it is not on the create response, because the card is vaulted when the charge completes. That makes this the surface to use when the token never reached your server at charge time: a payment sent through 3-D Secure finishes after your charge call has already returned, so the charge response carries no token for it. Unlike next_action, it is not tied to status — a saved card outlives the payment that saved it, so later reads keep returning it. Same value as token on the charge-at-submit response and id on a POST /v1/tokens response.
cardobject | nullNoPCI-safe card presentation — { "brand": …, "last4": … }. brand is one of visa, mastercard, amex, discover, diners, jcb, unionpay, unknown; last4 is 4 digits. No PAN, BIN, or fingerprint is ever included.
avs_result_codestring | nullNoNormalized Address Verification (AVS) result: match, partial_postal, partial_address, no_match, unavailable, or not_supported. null when no billing address was sent or no result was returned. Gateway-independent — raw processor codes are not exposed.
cvv_result_codestring | nullNoNormalized card security-code result: match, no_match, not_provided, or unavailable.
created_atstring | nullNoISO 8601 creation timestamp.
metadataobjectNoMerchant-provided key-value pairs you set at creation.
buyer_emailstringNoEchoes back the buyer_email you sent, confirming it was accepted for buyer attribution. Omitted entirely when you did not send one — it is never null, so test for the key's presence rather than comparing to null.
buyer_idstringNoOmitted when no effective buyer exists — never null. Echoes the effective buyer this charge was attributed to — which is not always the one you sent. If the stored payment_method you charged carries its own buyer, that one wins; otherwise it is the buyer_id on your request. Present only when an effective buyer exists. ⚠️ On an idempotency-key replay this echoes what this request asked for, not what is stored — the buyer write is skipped entirely on a replay, so a re-sent create that adds or changes a buyer returns the new value while the stored attribution keeps the original. On a first (non-replay) request it is a genuine confirmation that the best-effort linkage landed; on a replay it is not.
buyerobjectNoEchoes the nested buyer object you sent at creation, confirming it was accepted. buyer.metadata is echoed after privacy scrubbing, so it reflects what was stored rather than what you sent. Omitted when you did not send one — never null. See Buyers.
resolved_descriptorobject | nullNoWhat actually went to the payment provider as the statement descriptor for this charge — present on the create response. Either { "kind": "skipped", "reason": … } when none was applied, or the applied record with the provider-formatted descriptor and its dynamic tail. ⚠️ The brand-name prefix is best-effort and is usually fixed by your processor's merchant-account registration, not by this value; the dynamic tail is the part you reliably control per transaction. See Statement descriptors.
pulseTokenstring | nullNoShort-lived signed token for the real-time status subscription — forward it to the browser so the SDK's subscribeToPaymentIntent can resolve handleAction() over Server-Sent Events the instant a terminal status lands. null when the real-time substrate is unavailable; fall back to your timeout path. Opaque to your code — don't log it. ⚠️ The stream this token is for is not generally available today, so in practice this arrives null and the fallback path is the live path — see Streaming endpoints.
Vendor-neutral by design

The payment intent has the same shape regardless of which gateway processes it — your code never branches on a processor. Raw vendor decline codes are normalized into decline_code; client_confirm exposes only a client_secret you forward verbatim. Processor selection happens server-side via VORA.

Status lifecycle

StatusMeaning
requires_actionA 3D Secure (or other) challenge must complete before the intent can settle. Resolve next_action (redirect flow) or client_confirm (embedded flow).
authorizedFunds are held but not captured. Only reachable with capture_method: "manual" — capture with /capture or release with /void.
capturedA previously authorized intent was captured — money has moved.
succeededAuthorized and captured in one step (capture_method: "automatic"). Money has moved.
voidedAn authorized intent was cancelled before capture. No money moves.
failedThe authorization or capture failed. See decline_code for the reason.
                          ┌──► captured ──► (refund via POST /v1/refunds)
requires_action ──► authorized ──► voided
│ └──► (manual capture only)

succeeded ◄── (automatic capture)

failed (decline_code set)

The client-side result of a charge is a UX signal only. Always confirm settlement server-side via the payment_intent.succeeded / charge.succeeded webhook before fulfilling.

Decline codes

When status is failed, decline_code carries the normalized reason (branch on this rather than a raw issuer code), and action tells you what to do about it. Both are null on any non-failed intent.

A decline is a 201, not an error

A declined payment intent returns 201 Created with status: "failed"200 on an idempotent replay. The request succeeded; the payment did not. There is no 4xx for a decline.

So parse the response body on success statuses. A client that only reads the body when the status is an error will never see decline_code, action, rule_code or decline_message — and the failure is silent: no exception is raised, the decline branch simply never runs, and every decline looks like a blank result.

action — retry or don't

action is derived from decline_code through one published mapping, so you can build retry and dunning logic without maintaining your own code table.

actionMeaningWhat to do
softTransient.A later retry of the same card can legitimately succeed.
hardTerminal.Do not retry. The buyer needs a different payment method.
fix_and_retryThe buyer mistyped something.Retry only after they correct it — don't retry unchanged.
genericNo reason supplied.Treat as hard unless you have a reason not to.
decline_codeaction
insufficient_funds, issuer_unavailable, processing_error, card_velocity_exceededsoft
stolen_card, lost_card, fraudulent, do_not_honor, blocked_by_rulehard
expired_card, incorrect_cvc, incorrect_zipfix_and_retry
card_declined, generic_declinegeneric

This is worth wiring up in both directions. Retrying a hard decline wastes the attempt and can attract additional issuer-side flags; not retrying a soft one throws away a sale that would have gone through.

action is additive

Existing integrations that read only decline_code keep working unchanged.

One decline now reports a different code than it used to

A decline the provider reported as a plain "card declined" previously surfaced as generic_decline ("no reason supplied"). It now correctly reports card_declined ("declined, no further detail").

Both values were already in the list, so nothing new appears — but if you branch on generic_decline for that specific case, you will now see card_declined. The resulting action is generic either way, so logic built on action is unaffected.

blocked_by_rule is your own rule, not a bank decline

decline_code can report blocked_by_rule, meaning a routing rule you configured on your own account refused the charge before it reached an issuer. No bank saw it, so telling the buyer to call theirs is wrong advice.

Its action is hard — deliberately, not generic. The reason for the refusal is known exactly, and the same card will be refused for as long as the rule stands.

Which of your rules fired is reported separately, as rule_code — every rule produces the same decline_code, so a brand rule and a country rule are indistinguishable without it. It carries the custom code you set on the rule, and is null if you left that blank.

Reading it back is surface-dependent. decline_code here carries the full value, as does the charge.failed webhook — both are secret-key authenticated. Note the webhook names the same value failure_code, not decline_code. Browser-facing responses — anything a buyer's browser can receive, including the charge-at-submit call — report this case as the generic card_declined instead, and never include rule_code.

Capture both on the failure. Retrieving the intent later with GET /v1/payment_intents/{id} returns persisted state only and does not re-serve the decline detail.

Reporting it at all depends on your payment provider — on a provider that does not distinguish a rule refusal, it arrives as generic_decline. Full explanation and handling.

3D Secure: next_action vs client_confirm

A requires_action intent populates one or both of these, depending on how you integrate:

  • Server-redirect integrations read next_action.redirect_to_url.url and send the buyer there.
  • Embedded-checkout integrations read client_confirm.client_secret and forward it to the browser SDK, which renders the harmonized 3DS modal in place.

Both can populate on the same response — consume the one that matches your integration. client_confirm.client_secret is bearer-equivalent for the intent it's bound to; never log or persist it beyond the request/response round-trip. See Embedded Fields → 3D Secure.