Skip to main content

Gift cards and co-payment

Gift cards, loyalty cards and vouchers are secondary methods: they draw a slice of the amount and a primary method pays the rest. Several can stack, and gift cards covering everything make the payment fulfilled with no primary.

Setup​

ProductPurseHeadlessCheckout. Illicado, Buybox, Ogloba, MyBeezBox, Easy2Play (gift card and voucher) and Maxxing loyalty ship in it
InstallNothing. The seven issuers are registered in SecondaryConfigs.shared
DashboardThe issuer activated on the entity as a secondary method
Project, keysNone
UIYou render the PAN and CVV fields yourself: there is no payment element for a secondary method

Check the balance and apply​

func applyGiftCard(pan: String, cvv: String?) async throws {
guard let giftCard = checkout.secondaryMethods.first(where: { $0.partner == .illicado }) else { return }
let rules = checkout.secondaryConfig(for: giftCard) // requiresCVV(pan:), validatePAN(_:), validateCVV(_:)

let token = try await checkout.getSecondaryToken(method: giftCard, pan: pan, cvv: cvv) // /init, reads the balance
try checkout.applySecondary(token) // as much as the order needs…
try checkout.applySecondary(token, amount: Decimal(2)) // …or a capped slice (splittable issuers only)
checkout.removeSecondary(token)
}

secondaryConfig(for:) gives you the issuer's rules to validate the form before calling the network: whether this PAN needs a CVV, the PAN and CVV formats, and the takePolicy that says whether a partial slice is allowed at all. getSecondaryToken validates locally first, then tokenizes and reads the balance:

CodeMeaning
SECONDARY_METHOD_WRONG_PAN_FORMATThe PAN does not match the issuer's rule
SECONDARY_METHOD_REQUIRED_CVVThis PAN needs a CVV: check rules.requiresCVV(pan:) before calling
SECONDARY_METHOD_WRONG_CVV_FORMATThe CVV does not match the issuer's rule
FAILED_TO_INIT_SECONDARY_TOKEN/init was refused
SECONDARY_TOKEN_EMPTY_BALANCEThe card has no balance
METHOD_NOT_IMPLEMENTEDNo SecondaryConfig is registered for this (partner, method)

applySecondary with an explicit amount throws PurseCheckoutError.invalidTake:

CodeMeaning
INVALID_TAKE_AMOUNTNegative, above the balance, or above what is left to pay
MAX_AMOUNT_REACHEDAbove the issuer's maximum per payment
USAGE_LIMIT_ERRORThe issuer's per-session usage limit is spent by other cards in the split

remainingAmountToPay, amountSplit and isPaymentFulfilled follow every change. When gift cards cover the whole amount there is no primary element to drive, so submit the checkout directly:

if checkout.isPaymentFulfilled && checkout.remainingAmountToPay == 0 {
let result = try await checkout.submitPayment()
}

With a primary selected, element.submit() sends the gift cards together with it.

Keep the gift card​

When the issuer lets the shopper choose, offer to keep the card for the next session:

if token.canBeRegistered {
try checkout.registerSecondary(token, registered: true, name: "gift one")
}

The decision is applied at /validate, and checkout.registrationOf(token) reads it back. Saving needs a wallet session in the client session, as for saved cards; the label rules are the same (1–25 alphanumeric or space characters, unique).

Saved gift cards​

Cards kept earlier arrive on savedSecondaryTokens, their balance fetched in the background:

ForEach(checkout.savedSecondaryTokens, id: \.id) { card in
SavedGiftCardRow(
balance: card.balanceResolved ? card.balance : nil, // nil → "checking…"
unavailable: card.disabled != nil, // the issuer refused it this session
onUse: { do { try checkout.applySecondary(card) } catch { show(error) } },
onRemove: { checkout.removeSecondary(card) },
onRename: { name in Task { try await checkout.editToken(card, name: name) } }
)
}

applySecondary(_ token: SavedSecondaryToken) re-reads the checkout's current copy rather than the value you captured, and throws secondaryTokenizationFailed when the card is disabled, when its balance has not resolved yet, or when the balance is zero.

What a saved gift card cannot do on iOS

There is only one wallet operation for a SavedSecondaryToken: editToken(_:name:). deleteToken and setTokenFavorite have no overload for it, so you cannot delete a saved gift card individually; deleteAllTokens() removes every wallet token, saved gift cards included.

An issuer the SDK does not ship​

Implement SecondaryConfig and register it once at start-up; the session still decides whether the method is offered.

struct AcmeVoucherConfig: SecondaryConfig {
let takePolicy = SecondaryTakePolicy.max
func requiresCVV(pan: String) -> Bool { false }
func validatePAN(_ pan: String) -> Bool { pan.hasPrefix("ACME-") }
func buildInitPaymentData(pan: String, cvv: String?) -> [String: JSONValue] {
["voucher_code": .string(pan)]
}
func buildValidatePaymentData(_ token: SecondaryToken) -> [String: JSONValue] {
["voucher_code": .string(token.pan)]
}
}

SecondaryConfigs.shared.register(
partner: .other("acme"), method: .other("voucher"), config: AcmeVoucherConfig()
)

Every other member is defaulted: parseToken reads the generic card shape, validateCVV accepts anything, shouldPoll is false, classifyInitError returns nil. Registering an existing key replaces the shipped config.

ANCV Connect

The ANCV Connect flow (the shopper confirms in the ANCV app while the checkout polls) ships on the web SDK only. The SecondaryConfig hooks shouldPoll and classifyInitError exist so an app can register such an issuer.

Troubleshooting​

SymptomCause
getSecondaryToken throws SECONDARY_METHOD_REQUIRED_CVVThis PAN needs a CVV: check rules.requiresCVV(pan:) before calling
applySecondary throws USAGE_LIMIT_ERRORToo many cards of this issuer in the split
A card reads as empty although it has money on itThe balance is computed against the amount being asked for; a card applied when nothing is left to pay answers zero
A saved gift card shows disabledIts balance could not be read this session. Let the shopper enter it again
applySecondary on a saved card throws SECONDARY_TOKEN_EMPTY_BALANCEIts background balance lookup has not answered yet. Gate the "Use" button on balanceResolved
registerSecondary throws TOKEN_NOT_REGISTERABLEThe issuer's save_token_mode forbids saving, or the card is already a saved instrument
The card was applied with registered: true but is not in the wallet afterwardsregisterSecondary records a decision locally and never checks the wallet: it travels to /validate as payment_preferences.registration, and a session created without a wallet_session has nowhere to save it
getSecondaryToken throws METHOD_NOT_IMPLEMENTEDNo config for this issuer. Register one with SecondaryConfigs.shared.register