Skip to content

Payment intents

A payment intent is one thing a customer is paying for: an amount, a currency and your metadata. Your server creates it, the Stack sheet pays it, and webhooks tell you how it ended. One intent is paid once. A customer who fails and retries is still paying the same intent.

requires_authentication ──▶ processing ──▶ succeeded
│ ▲ │
│ └──── (declined) ◀──┘
├──▶ requires_action ──▶ processing / succeeded (card OTP, PIN, 3-D Secure)
└──▶ canceled (you cancelled it)
Status Meaning
requires_authentication Created, waiting for the customer to pay. It goes back here after a declined card or a failed attempt, so the customer can try again.
requires_action The customer is completing a card check: an OTP, the card’s PIN, or 3-D Secure.
processing Submitted and waiting to settle, for example a bank transfer waiting for the money, or a bank debit.
succeeded Paid. Stack sends payment_intent.succeeded.
canceled You cancelled it before it was paid.

An intent can be paid for 30 minutes after it’s created (expires_at). An expired intent keeps its status but can’t be paid: create a new one.

Field
id pi_…
amount Kobo, as a string: "570000" is ₦5,700.
currency "NGN"
description Yours, shown in the sheet.
status See above.
method How it was paid: balance, card, debit (a saved bank), transfer, or null before the customer chooses.
metadata Yours, returned as you sent it.
customer The { email, phone } you passed, or null.
merchant_id, merchant_name, merchant_logo_url Your project, as the sheet shows it.
expires_at When it can no longer be paid.
succeeded_at When it was paid, or null.
client_secret Only in the response to create. Give it to your app, and don’t store or log it.

The client_secret lets your app open the sheet for this one payment and nothing else. It can’t create payments, refund, or read your other payments. It works only with your publishable key, so another project’s app can’t pay your intent with it.