Skip to main content

Card on file: customer-initiated (CIT) and merchant-initiated (MIT) charges

Charging a card you have already seen once is a two-part sequence. The first part is a customer-initiated transaction (CIT), where the customer is at checkout and the charge runs on a fresh token. The second part is a merchant-initiated transaction (MIT), which is an upsell or a later charge made without the customer present using the payment instrument returned by the CIT.

The rule that governs which one you send:

Customer present: send a fresh token. No customer present: send the stored paymentInstrumentId.

Customer-initiated (CIT)Merchant-initiated (MIT)
paymentInstruction{ "token": "<fresh token>" }{ "paymentInstrumentId": "<id>" }
isMerchantInitiatedfalsetrue
redirectUrlStrongly recommended (not enforced)Not needed
3DSMay trigger a challengeOut of scope

If you already attach tokens before charging:

That path still works, and nothing has been removed. The attach call creates the instrument as before, and the first charge against it succeeds. What fails is the next customer-present charge on that instrument: the CVV is spent, so the CIT declines. Merchant-initiated charges on the same instrument are unaffected, because they never needed a CVV.

Integrations that create one instrument per checkout will not have noticed. Integrations that reuse a stored instrument at checkout are already hitting this.

Moving to the flow below removes the extra call and the failure mode at once.

Why a CIT always uses a fresh token

The token carries the CVV the customer typed at checkout, and that CVV covers exactly one charge. What decides whether a stored instrument still works is how many charges it has taken, not how it was created:

  • On an instrument created by a CIT, that single charge was the CIT itself, so a later customer-present charge declines immediately.
  • On an instrument created by the attach call, the first charge succeeds and the next one declines.

Either way you cannot rely on a stored instrument for a customer-present charge. A genuine CIT always has a fresh, customer-entered CVV available, so there is no reason to use the instrument path for a CIT. This includes returning customers: someone standing at your checkout is customer-present, regardless of how many times you have charged them before.

The CIT is also the only leg that carries Strong Customer Authentication (SCA) / 3DS. An MIT has no cardholder present to authenticate, so it never produces a challenge.

isMerchantInitiated: true unconditionally skips SCA:

The flag alone decides this. Sending true with a fresh token on a card that would otherwise force a 3DS challenge still returns an approval with has3ds: false and no approvalLink. The token's freshness does not re-enable authentication.

Treat the flag as a deliberate declaration that no cardholder is present, not as a hint. Setting it on a customer-present charge silently bypasses 3DS, which for EU, UK, and Japan merchants means bypassing a regulatory requirement. See 3DS2 guide.

Step 1: Run the customer-initiated charge

Tokenize the card (FramePay or the tokenization endpoint), then send the token straight to POST /transactions. Do not attach it first (see When to use attach instead).

curl -X POST \
"https://staging-api.payments.ai/v1/public-api/organizations/${ORGANIZATION_ID}/transactions" \
-H 'Content-Type: application/json' \
-H "Authorization: ApiKey ${API_KEY}" \
-d '{
"type": "sale",
"customerId": "${CUSTOMER_ID}",
"currency": "USD",
"amount": 10,
"paymentInstruction": { "token": "${TOKEN}" },
"isMerchantInitiated": false,
"redirectUrl": "https://yoursite.com/{id}/{result}"
}'

The token is single-use and is consumed by this transaction. The response returns a paymentInstrument object whose id is the instrument you will use for MITs.

Do not also call the attach endpoint with this token:

POST /customers/{customerId}/payment-instruments with a token already spent on a transaction returns 422. The transaction has already created the instrument for you. See Token lifecycle.

Step 2: Resolve the final result before storing anything

201 Created does not mean the customer was charged (see HTTP 201 does not mean logical success). A CIT that goes through a 3DS challenge returns a non-final state at create:

{
"data": {
"id": "txn_abc123",
"status": "waiting",
"result": "unknown",
"combinedStatus": "waiting",
"paymentLink": "https://3ds.example/..."
}
}

Resolve the final state one of three ways: read {result} on the redirectUrl the customer returns to, retrieve the transaction by ID, or handle the transaction-processed / transaction-declined webhook. See the 3DS2 guide for the full challenge flow.

Final resultWhat to do
approvedStore paymentInstrument.id. The card is now usable for MITs.
declined, abandoned, canceledDiscard the instrument. Re-tokenize to try again.
unknownNot a final state. Keep resolving; do not store the instrument yet.

Step 3: Run merchant-initiated charges

Once the CIT is approved, charge the stored instrument without the customer present. No redirectUrl, no CVV, no 3DS.

curl -X POST \
"https://staging-api.payments.ai/v1/public-api/organizations/${ORGANIZATION_ID}/transactions" \
-H 'Content-Type: application/json' \
-H "Authorization: ApiKey ${API_KEY}" \
-d '{
"type": "sale",
"customerId": "${CUSTOMER_ID}",
"currency": "USD",
"amount": 10,
"paymentInstruction": { "paymentInstrumentId": "${PAYMENT_INSTRUMENT_ID}" },
"isMerchantInitiated": true
}'

Required: charge an MIT only after an approved CIT

A merchant-initiated transaction runs only on a payment instrument whose customer-initiated transaction reached result: "approved". Resolve the CIT to its final result first, store the instrument only then, and charge only instruments that passed. This ordering is part of the flow, not an optional check.

Full sequence

Common mistakes

MistakeResult
Charging a stored paymentInstrumentId while the customer is present (including a returning customer at checkout)Works at most once, then declines like an ordinary payment decline: status: "completed", result: "declined", and cvvResponse.originalMessage: "No CVC/CVV provided, but was required". This is not a 400, so check result rather than catching an HTTP error.
Calling attach with a token already spent422 (UnprocessableContent), whether it was spent on a transaction or on an earlier attach. The reverse also fails: charging a token already attached returns PaymentInstrumentInvalidToken.
Storing paymentInstrument.id from a CIT that returned result: "unknown"Not a final state. A 3DS CIT is unknown at create; the charge may still decline.
Firing the MIT before the CIT reached approvedBreaks the required ordering. Resolve the CIT to a final result first, and charge only instruments whose CIT was approved.
Sending isMerchantInitiated: true on a customer-present chargeSkips SCA entirely (see the warning above). The charge is approved with has3ds: false and no approvalLink, even on a card that would otherwise force a challenge.

When to use attach instead

POST /customers/{customerId}/payment-instruments is the subscription path: it turns its own fresh token into an instrument you pass to POST /subscriptions, which then activates on the hosted payment form. See Attach a token to a customer.

For one-off charges, card-on-file, and upsells, use the CIT flow on this page instead. The attach step is not part of it.