Server-Confirmed Payments

Flint is the confirmation authority for every standalone PaymentIntent. The browser collects payment details with Stripe Elements and creates a short-lived ConfirmationToken. Your backend sends that token to Flint. The browser never confirms a PaymentIntent directly.

This single flow supports cards, wallets, and one-time ACH debit while keeping risk evaluation, idempotency, provider recovery, and final state in one durable operation.

The Sequence#

text
backend creates PaymentIntent
  -> browser initializes Elements from payment_collection
  -> browser creates ConfirmationToken
  -> backend calls Flint /confirm
  -> browser performs current_payment_action when required
  -> backend calls Flint /confirm without a replacement credential
  -> webhook or GET reports the final state

1. Create from Your Backend#

Every standalone create declares the exact allowed options. The response contains an immutable PaymentIntent and a collection bootstrap.

Bash
curl -X POST https://api.withflintpay.com/v1/payment-intents \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: payment-1001" \
  -d '{
    "amount_money": {"amount": 4200, "currency": "USD"},
    "payment_options": ["card"]
  }'

Use data.payment_collection.stripe exactly as returned. Its elements.next_step is create_confirmation_token, submit_to is confirm_payment_intent, mode is payment, and payment_method_creation is manual. The initial response does not expose a confirmable PaymentIntent client secret.

2. Collect in the Browser#

Initialize Stripe.js with the returned publishable key and connected account. Initialize deferred Elements from the returned amount, currency, method types, and method options. Mount a Payment Element.

Before creating a token, call elements.submit(). Pass accurate billing details with the token request. ACH requires both name and email.

JavaScript
const {error: submitError} = await elements.submit();
if (submitError) throw submitError;

const {error, confirmationToken} = await stripe.createConfirmationToken({
  elements,
  params: {
    payment_method_data: {
      billing_details: {name, email},
    },
  },
});
if (error) throw error;

await sendTokenToYourBackend(confirmationToken.id);

The token is a payment credential. Do not log it, store it in analytics, or send it anywhere except your backend over TLS.

3. Confirm through Flint#

Bash
curl -X POST https://api.withflintpay.com/v1/payment-intents/pi_1kmn0aExample/confirm \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: payment-1001-confirm" \
  -d '{"confirmation_token":"ctoken_Example"}'

The create and update endpoints reject payment credentials. The initial confirm accepts exactly one credential arm. For a newly collected flow, use confirmation_token.

Common validation errors teach the required correction:

CodeCorrection
CONFIRMATION_TOKEN_REQUIREDCreate a new token for an initial confirmation.
CONFIRMATION_TOKEN_EXPIREDCreate a fresh token and retry.
CONFIRMATION_TOKEN_ALREADY_USEDThe token belongs to another PaymentIntent. Create a fresh token.
CONFIRMATION_TOKEN_SCOPE_MISMATCHRecreate the token with the returned environment and account bootstrap.
PAYMENT_OPTION_NOT_ALLOWEDCollect one of the PaymentIntent's declared options.
ACH_MANDATE_ACCEPTANCE_REQUIREDUse the hosted bank collection flow and complete mandate acceptance.
ACH_BILLING_DETAILS_REQUIREDProvide accurate billing name and email when creating the token.

A token already consumed by the same PaymentIntent is recovery evidence. Retrying reconciles and returns the current resource instead of reporting it as used.

4. Handle a Typed Client Action#

A card can return status: "requires_action". The response and later GET /v1/payment-intents/{id} expose the same current_payment_action:

JSON
{
  "current_payment_action": {
    "pending_action_id": "pendact_1kmn0aExample",
    "subject": {
      "payment_intent": {"payment_intent_id": "pi_1kmn0aExample"}
    },
    "action_type": "payment_authentication",
    "client_action": {
      "stripe": {
        "account_id": "acct_Example",
        "publishable_key": "pk_test_Example",
        "payment_intent": {
          "stripe_js_call": "handle_next_action",
          "client_secret": "pi_Example_secret_Example"
        }
      }
    }
  }
}

Dispatch on the typed stripe_js_call value. For handle_next_action, initialize Stripe.js with the action's publishable key and account, then call stripe.handleNextAction(client_secret).

Do not infer an action from status alone. Do not use collection bootstrap fields as action authority. The typed action carries the narrowly scoped client secret needed for this step.

5. Continue without a New Credential#

After the browser action completes, your backend calls the same confirm endpoint with an empty JSON body:

Bash
curl -X POST https://api.withflintpay.com/v1/payment-intents/pi_1kmn0aExample/confirm \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: payment-1001-continue" \
  -d '{}'

Flint first retrieves the authoritative provider state. It reconfirms only if that state is requires_confirmation. If the state is still requires_action, is processing, or is terminal, Flint performs no provider confirmation and returns the current PaymentIntent.

The continuation reuses the original payment attempt and frozen risk decision. It does not accept a replacement credential. Concurrent continuations, even with different public idempotency keys, converge on one durable provider operation. A later transition that leaves and re-enters requires_confirmation receives a new internal operation.

Order-linked PaymentIntents are different: continue only with PayOrder(payment_attempt_id). The standalone /confirm endpoint rejects an order-linked intent.

Recovery and Idempotency#

Send an Idempotency-Key for every backend mutation and reuse it when retrying the same HTTP request. Flint also owns a separate stable provider idempotency key inside its durable operation, so correctness does not depend on a caller preserving the public key across a continuation.

If a response is lost:

  1. Retrieve the PaymentIntent from Flint.
  2. If it exposes current_payment_action, complete that action.
  3. If the browser action already completed, call /confirm without a credential.
  4. Stop when the resource is processing, succeeded, requires_payment_method, or another terminal state.

Never create another token merely because your backend lost a response. A token consumed by the intended PaymentIntent lets Flint recover the existing operation.

Webhook Checklist#

Subscribe to:

  • payment_intent.requires_action
  • payment_intent.processing
  • payment_intent.succeeded
  • payment_intent.payment_failed
  • payment_intent.canceled
  • Existing dispute.* events
  • refund.created, refund.updated, and refund.failed

Webhook delivery can be duplicated or delayed. Verify signatures, deduplicate by event ID, and fetch the current resource when ordering matters. See Webhooks and ACH Debit Payments.

Rate this doc