One-click upsells & downsells

Save a card during a one-time purchase and charge it again with a single click — no card re-entry, no CVV — to power post-purchase upsells and downsells.

A one-click upsell is the offer you show right after a purchase succeeds — an add-on, an upgrade, a bundle — that the customer accepts with a single click. A downsell is the cheaper alternative you show when they decline. Both charge the card the customer just used, without asking them to type it again and without a CVV.

Tokeflow supports this with two flags on POST /api/v1/transactions:

  • save_payment_instrument: true on the first charge saves the card as a payment instrument and keeps the provider-side credential that makes it chargeable again.
  • credential_on_file: true on a later charge bills that saved instrument as a customer-present charge — no card data, no CVV. It can also pay a checkout session once the merchant opts in to saved-card checkout.

The card itself is never stored by Tokeflow or by you. What is kept is the provider's own reusable credential for that card, on the connector that approved the first charge. Later one-click charges go to that same connector.

One-click charges are customer-initiated. They ride the same stored-credential machinery as subscription renewals, but the order is recorded with recurrence: "unscheduled" and the transaction metadata carries stored_credential_usage: "customer_initiated", so reporting can tell an upsell apart from a scheduled renewal.

Before you start

  • A customer record. Both flags require customer_id — the instrument belongs to a customer, and a one-click charge is refused for any other customer.
  • Card payments only. credit_card or debit_card. PIX, boleto and wallets have no stored credential to reuse.
  • A provider that supports stored-credential charges. The first charge must succeed on a connector whose provider can charge a stored card later. Check the provider catalog before you rely on one-click for a given connector.
  • Bridge SDK 2.6.0 or newer in the browser, so you can mint a ciphertext that is allowed to be saved (see step 1).

Step 1 — Mint a ciphertext that can be saved

A card token minted by the Bridge SDK has a purpose. A one_time ciphertext is single-use and expires; only an instrument_enrollment ciphertext can be saved as a payment instrument. Which purpose you get depends on where you tokenize:

Where the SDK runsHow to ask for an enrollable ciphertext
Inside a checkout session (checkoutSessionId set)Pass { saveCard: true }. The server derives the purpose from the offer — a recurring offer always enrolls; a one-time offer enrolls only when saveCard is set. Any purpose you send is ignored here.
Standalone (no checkout session)Pass { purpose: 'instrument_enrollment' }. Without it the SDK mints a one_time ciphertext.
// Inside a checkout session: the customer ticked "save my card"
const token = await tokeflow.cards.tokenize({ card: cardElement }, { saveCard: true });

// Standalone tokenization
const token = await tokeflow.cards.tokenize(
  { card: cardElement },
  { purpose: 'instrument_enrollment' },
);

console.log(token.purpose); // 'instrument_enrollment' — check it before you rely on it

Check token.purpose in the response. Requesting a purpose that does not survive fails nothing at tokenization time — it surfaces later as a 400 on the charge, when save_payment_instrument refuses a one_time ciphertext. See Tokenization → Options.

Collect the customer's explicit consent to keep the card on file before you tokenize with either option.

Step 2 — The first charge saves the card

Send the ciphertext to your server and create the transaction with save_payment_instrument: true. Tokeflow enrolls the card for the customer before routing the charge, then charges the ciphertext you submitted (the only one that still carries the CVV). After the provider approves, the provider's reusable credential is kept on that connector.

curl -X POST https://api.tokeflow.com/api/v1/transactions \
  -H "Authorization: Bearer sk_live_mer_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_1001_attempt_1" \
  -d '{
    "payment_method": "credit_card",
    "charge_type": "payment",
    "country": "BR",
    "amount": 9900,
    "currency": "BRL",
    "customer_id": "cust_2f9a71d4",
    "external_order_id": "order_1001",
    "card_ciphertext_id": "tok_8f3c2a1b9d4e",
    "save_payment_instrument": true
  }'

The response is the usual transaction object. The field to keep is payment_instrument_id — the pi_… you will charge in step 3.

RuleDetail
Requirescard_ciphertext_id minted with purpose=instrument_enrollment, plus customer_id.
Not combined withcheckout_session_id — inside a checkout session the session's save_card flag governs enrollment. payment_instrument_id — you are saving a new card, not charging a saved one.
Card already on fileThe existing instrument for that card and customer is reused; no duplicate is created.
one_time ciphertext400 validation_error naming the fix. Nothing is charged and nothing is silently downgraded to an unsaved card.
Declined first chargeThe instrument exists but holds no provider credential yet, so a later credential_on_file charge is refused with 400 until one cardholder-present charge or a card verification succeeds.

Already collecting cards through a checkout session? Nothing changes there: tokenize with saveCard: true and charge the session as usual. The card is enrolled by the session itself, and the resulting payment_instrument_id is on the transaction response.

Step 3 — Charge the saved card with one click

When the customer accepts the upsell (or the downsell), your server creates a new transaction against the instrument with credential_on_file: true. No ciphertext, no CVV, no checkout session.

curl -X POST https://api.tokeflow.com/api/v1/transactions \
  -H "Authorization: Bearer sk_live_mer_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_1001_upsell_attempt_1" \
  -d '{
    "payment_method": "credit_card",
    "charge_type": "payment",
    "country": "BR",
    "amount": 4900,
    "currency": "BRL",
    "customer_id": "cust_2f9a71d4",
    "external_order_id": "order_1001_upsell",
    "payment_instrument_id": "pi_a1b2c3",
    "credential_on_file": true
  }'

Example response — 201 Created

{
  "success": true,
  "data": {
    "id": "tx_812",
    "order_id": "ord_045",
    "customer_id": "cust_2f9a71d4",
    "payment_instrument_id": "pi_a1b2c3",
    "amount_authorized": 4900,
    "amount_captured": 4900,
    "currency": "BRL",
    "payment_method": "credit_card",
    "charge_type": "payment",
    "country": "BR",
    "status": "authorized",
    "timeline": [
      {
        "attempt_number": 1,
        "is_fallback": false,
        "connector_id": "conn_a1b2c3",
        "provider_slug": "acquirer_a",
        "status": "success",
        "started_at": "2026-09-24T12:30:00.000Z",
        "finished_at": "2026-09-24T12:30:01.000Z",
        "error_category": null,
        "error_code": null,
        "decline_reason": null
      }
    ],
    "metadata": { "stored_credential_usage": "customer_initiated" },
    "created_at": "2026-09-24T12:30:00.000Z",
    "updated_at": "2026-09-24T12:30:01.000Z"
  },
  "request_id": "req_2c9e",
  "timestamp": "2026-09-24T12:30:01.000Z"
}

A downsell is the same call with the alternative offer's amount and its own external_order_id and idempotency key. Each accepted offer is its own transaction and its own order.

What a credential_on_file charge does differently

BehaviorDetail
ConnectorPinned to the active connector holding the instrument's newest usable credential — the one that approved the first charge, unless that connector was since deactivated or cannot take this charge (currency, installments). Routing rules and cascade do not apply.
CaptureAlways captured in one step. capture: false is refused with 400.
OrderAuto-created with recurrence: "unscheduled".
Metadatastored_credential_usage: "customer_initiated" is added to whatever you send.
ResponseThe usual transaction object, with payment_instrument_id set. Same idempotency, envelope and webhooks as any charge.

Rules

RuleDetail
Requirespayment_instrument_id and customer_id. The instrument must belong to that customer and be active.
Not combined withcard_ciphertext_id (send the ciphertext alone for a cardholder-present charge), save_payment_instrument, or capture: false.
With a checkout sessionAllowed only when the merchant opted in to saved-card checkout — see Pay a checkout session with a saved card.
PrerequisiteThe instrument must hold a provider credential from one successful cardholder-present charge or card verification on a connector whose provider supports stored-credential charges. Otherwise: 400 validation_error.
payment_instrument_id aloneRefused with 400. A saved card is charged either with credential_on_file: true or with a fresh card_ciphertext_id (re-typed CVV).

Pay a checkout session with a saved card

The same stored credential can pay a checkout session: a returning customer picks a card they saved before and pays without typing it again. This is saved-card checkout, and it is off by default.

  1. Opt the merchant in. Set saved_card_checkout_enabled: true with PATCH /merchants/:id. By turning it on you state that your server authenticates the customer before it identifies them on a session.
  2. Offer only the cards that can be charged. In the browser, the checkout context reports savedCardCheckoutEnabled and, for each saved card, a reusable hint.
  3. Charge the session from your server with checkout_session_id, payment_instrument_id and credential_on_file: true. The session supplies the amount, currency and customer; on a recurring offer the subscription starts on the connector that holds the card's credential.
curl -X POST https://api.tokeflow.com/api/v1/transactions \
  -H "Authorization: Bearer sk_live_mer_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cks_a1b2c3d4e5_saved_card" \
  -d '{
    "checkout_session_id": "cks_a1b2c3d4e5",
    "payment_instrument_id": "pi_123",
    "credential_on_file": true,
    "payment_method": "credit_card",
    "charge_type": "payment",
    "country": "BR"
  }'

Without the opt-in, any use of a saved card on a checkout session that does not carry a fresh card_ciphertext_id — the charge above, or a zero-amount trial started from the saved card — is refused with 400. Collect the card again and charge the session with the new ciphertext instead. Full rules: Saved-card checkout.

Handle declines

A decline is not an HTTP error: the response is 201 Created with status: "failed" and a timeline entry saying why. Branch on status, as with any charge, and read the attempt's decline_reason to choose what to show the customer.

  • error_code is AUTHENTICATION_REQUIRED — the issuer insists on strong customer authentication for this card. A stored-credential charge cannot run a 3-D Secure challenge. Ask the customer to enter the card again and charge that fresh ciphertext the normal way.
  • Soft declines (insufficient funds, limits) — show the downsell, or offer another saved card from the customer's wallet (GET /payment-instruments?customer_id=…&status=active).
  • 400 with a message about a missing stored credential — the first charge never succeeded on a capable connector. Fall back to a fresh tokenization; that charge will populate the credential.

Send a distinct Idempotency-Key for every offer the customer accepts. Replaying the key of the original purchase returns the original transaction instead of charging the upsell. See Idempotency and rate limits.

Good practice

  • Charge only on an explicit click. credential_on_file is for a customer who is present and has just chosen the offer. Charges the customer did not initiate belong to subscriptions.
  • Authenticate the customer first. The instrument id is not a secret, but your endpoint that triggers the charge must only act for the customer who owns it.
  • Show what will be charged — amount, currency and the card's brand and last four (card_brand, last4 on the instrument) — before the click.
  • Let customers revoke a saved card from their account (DELETE /payment-instruments/:id or the Customer Portal).

On this page