Choose a Payment Flow
Choose between stored-card, GoTab Wallet, and Apple Pay checkout based on who owns customer identity and payment submission.
Start with one decision: who should own the customer relationship at checkout? That choice determines which saved cards the customer can use, how you create the tab, and whether your server or a GoTab component submits the charge.
Choose a checkout flow
Section titled “Choose a checkout flow”Choose the row that matches the experience you want to provide. Each flow links directly to its implementation path:
| Flow | Choose it when | Create the tab with | Payment submitted by |
|---|---|---|---|
| Stored card | Your application signs the customer in and displays cards from their integration customer profile. | The integration customer’s customerId. | Your server, through the Integrations API. |
| GoTab Wallet | GoTab should authenticate the customer and display cards from their Wallet account. | A supported guest identifier, but no integration customerId; create a payment session after the tab. | GoTab Wallet. |
| Apple Pay | You want a standalone Apple Pay button without Wallet sign-in or a saved-card picker. | A supported guest identifier, but no integration customerId. | The Apple Pay component. |
Use one flow for each payment attempt. Wallet and Apple Pay submit the payment themselves, so your server should not also submit a stored-card payment for the same checkout.
Whichever row you choose, treat browser callbacks as progress—not proof that you can fulfill the order. You are done only after your server retrieves the latest tab and confirms that it is closed with no balance due.
Keep the two card stores separate
Section titled “Keep the two card stores separate”An integration customer profile and a GoTab Wallet account can belong to the same person, but their saved cards are separate.
| Card owner | How the customer is identified | How cards are managed | Used by |
|---|---|---|---|
| Integration customer profile | Your server resolves or creates the customer through /api/integrations/customers. | Your checkout uses the Payment SDK to add, list, edit, or delete cards. | Stored-card payments submitted by your server. |
| GoTab Wallet account | The customer authenticates inside Wallet. | Wallet displays and manages the customer’s cards. | GoTab Wallet checkout. |
Cards from one account type are not available through the other. Choose the account model that matches the checkout experience you want to provide.
Stored-card payment
Section titled “Stored-card payment”Use this flow when your application owns customer sign-in and saved-card selection. It currently requires one saved card to cover the tab’s entire outstanding balance. It does not support partial or split payments, refunds, preauthorizations, item-group payments, or authorization transfers.
Before you build this path, contact GoTab API Support to confirm that the location is eligible. Stored-card payments are available for eligible US locations processing in USD.
- Server: Resolve or create the integration customer and retain both
customer_idandcustomer_profile_uuid. - Server: Create the tab with
customerIdset tocustomer_id. - Browser: Use the Payment SDK and
customer_profile_uuidto add or fetch cards, then let the customer select onepayment_method_uuid. - Browser → server: When the customer confirms payment, send the selected
payment_method_uuidand tab identifier to your server. For 3DS2-enabled payments, include fresh CVC and browser context. - Server → GoTab: Load the current balance and submit one payment through the Integrations API.
- Server: Retrieve the latest tab. Fulfill the order only when the tab is closed with no balance due.
Build the payment request
Section titled “Build the payment request”Your checkout sends the selected card’s identifier to an endpoint in your application. Keep the amount and GoTab credentials on your server:
await fetch('/checkout/pay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tabUuid, paymentMethodUuid: selectedCard.payment_method_uuid, }),});In this example, /checkout/pay is your endpoint. It authenticates the customer, loads the current tab as tab, and submits one payment with your server-side Integrations API client:
const { paymentMethodUuid } = request.body;
const paymentBody = [{ payAmount: tab.balanceDue, tipAmount: 0, displayName: 'Online checkout', methods: [{ type: 'payment_method', paymentMethodUuid, amount: tab.balanceDue, }],}];
await gotab.post( `/api/v2/loc/${locationUuid}/tabs/${tab.tabUuid}/payments`, paymentBody,);Here, gotab represents the authenticated Integrations API client in your server application. Load the tab and its balance on the server instead of accepting an amount from the browser.
Confirm the final tab state
Section titled “Confirm the final tab state”The payment endpoint returns the updated tab with HTTP 200 when processing completes. Retrieve the latest tab on your server and complete the order only after confirming that the tab is closed with no balance due.
Add 3D Secure 2
Section titled “Add 3D Secure 2”3D Secure 2 (3DS2) is an additional configuration for eligible stored-card payments. GoTab enables it per location; contact GoTab API Support to confirm availability and activation. The standard request above does not need payment customer context or payment-time CVC. A billing-address requirement alone does not enable the CVC requirement.
Collect fresh CVC
Section titled “Collect fresh CVC”For the eligible Adyen CardOnFile payment path used by this 3DS2 flow, ask the customer for the selected card’s security code (CVC) before every new payment attempt. This includes newly saved cards and payments that complete without a challenge. The CVC entered when saving a card does not satisfy the payment-time requirement.
Send the raw code in methods[].cvc as a string of exactly three or four ASCII digits, preserving leading zeros. This write-only field belongs to the selected payment method in the request; it is not part of customer. A stored CVC alias is not accepted.
Your checkout owns this input. Use CVC only for the immediate HTTPS payment request, then discard it. Do not put it in saved-card data, browser storage, cookies, databases, logs, analytics, or background jobs. Clear the input after dispatch and whenever the selected card changes. Collect it again for each new attempt; recovering an existing payment does not need CVC.
Collect fresh browser context
Section titled “Collect fresh browser context”Once GoTab confirms that 3DS2 is enabled for a location, collect fresh browser context each time the customer confirms a new payment and send it to your server with the selected card and CVC. In this example, cvcInput is your checkout’s security-code input for that card. Validate its value and keep card selection and payment controls disabled while collecting context and submitting:
const paymentCustomerContext = await PaymentsSDK.collectPaymentCustomerContext(clientApiAccessId);
const paymentRequest = fetch('/checkout/pay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tabUuid, paymentMethodUuid: selectedCard.payment_method_uuid, cvc: cvcInput.value, paymentCustomerContext, }),});cvcInput.value = '';await paymentRequest;Pass the browser SDK access ID issued to the same integration that owns the stored card. The SDK collects local browser measurements and may also return a device fingerprint from a temporary GoTab-owned frame. Using the helper requires the checkout page’s origin in the integration’s gotab_wallet_allowed_origins setting, even when the integration does not otherwise use Wallet. You can instead construct the same browser context yourself. See Collect browser context for the shared origin registration, manual example, fallback behavior, and Content Security Policy requirements.
Add server-owned customer context
Section titled “Add server-owned customer context”On your server, combine those browser values with information from the same checkout request. The example below uses your authenticated customer’s server-side record for emailAddress, the request source for ipAddress, and the incoming headers for acceptHeader and userAgent:
const { paymentMethodUuid, cvc, paymentCustomerContext } = request.body;
const customer = { emailAddress: customerRecord.email, ipAddress: request.ip, browser: { acceptHeader: request.headers.accept, colorDepth: paymentCustomerContext.browser.colorDepth, language: paymentCustomerContext.browser.language, screenHeight: paymentCustomerContext.browser.screenHeight, screenWidth: paymentCustomerContext.browser.screenWidth, timezoneOffsetMinutes: paymentCustomerContext.browser.timezoneOffsetMinutes, userAgent: request.headers['user-agent'], },};
if (paymentCustomerContext.deviceFingerprint) { customer.deviceFingerprint = paymentCustomerContext.deviceFingerprint;}
const paymentBody = [{ payAmount: tab.balanceDue, tipAmount: 0, displayName: 'Online checkout', customer, methods: [{ type: 'payment_method', paymentMethodUuid, amount: tab.balanceDue, cvc, }],}];
await gotab.post( `/api/v2/loc/${locationUuid}/tabs/${tab.tabUuid}/payments`, paymentBody,);Include customer only after 3DS2 has been enabled for the location. It is a write-only payment property and is accepted on a request containing exactly one payment. If your application runs behind a proxy, configure your framework so request.ip resolves the customer’s original request IP.
Billing address is not part of customer or the payment request. It is not required or submitted unless both 3DS2 and the billing-address requirement are enabled for the location. When required, GoTab uses the address associated with the selected payment method. You can collect it while adding the card or add or modify it on an existing card.
Continue an incomplete payment
Section titled “Continue an incomplete payment”Even with 3DS2 enabled, the card issuer may authenticate a payment without showing anything to the customer. When verification is required, the API returns HTTP 202 with status: "requires_action"; your integration then displays Payment Action. HTTP 202 can also return status: "pending" while the existing attempt is still in progress. Both responses include the current Payment Action handoff and its expiresAt:
type IncompletePayment = | { status: 'requires_action'; paymentId: string; handoff: string; expiresAt: string; } | { status: 'pending'; paymentId: string; handoff: string; expiresAt: string; };If your server does not receive the payment response because the request times out or the connection drops, resend the original request to the same payment endpoint with methods[].cvc omitted. Keep the tab, selected card, amounts, and customer context unchanged. The API checks for the existing attempt before validating CVC for a new one and returns its current paymentId, handoff, and expiresAt, or its terminal state. Do not retain or recollect CVC to recover an existing attempt.
If there is no existing attempt to recover, a new payment on this path requires fresh CVC and the request is rejected with PAYMENT_CVC_REQUIRED. Reconcile the latest tab and payment state on your server before asking the customer to enter CVC and explicitly confirm a new attempt.
| Response | What your integration does next |
|---|---|
HTTP 200 | Retrieve the latest tab. Complete the order only after confirming the tab is closed with no balance due. |
HTTP 202 with a handoff (requires_action or pending) | Retain the handoff while the payment remains incomplete, keep other payment controls disabled, and mount the Payment SDK’s initPaymentAction with it. |
Recover from 3DS2 errors
Section titled “Recover from 3DS2 errors”For CVC validation failures, the API returns HTTP 400 with errors[].type set to PAYMENT_VALIDATION_ERROR and errors[].message set to PAYMENT_CVC_REQUIRED or INVALID_PAYMENT_CVC.
Common 3DS2 payment errors are:
| Error | What to do |
|---|---|
PAYMENT_CVC_REQUIRED | CVC is missing, null, or empty for a new payment that requires it. Ask the customer to enter the selected card’s CVC and confirm payment again. |
INVALID_PAYMENT_CVC | CVC must be a string of exactly three or four ASCII digits. Ask the customer to re-enter it; numbers, aliases, whitespace, and other formats are rejected. |
BILLING_ADDRESS_REQUIRED | The location’s 3DS2 billing-address requirement is enabled, but the selected card does not have a complete address. Use initEditMethodForm to add or update it, then return to checkout so the customer can supply fresh CVC and confirm payment again. |
PAYMENT_CUSTOMER_REQUIRED | The location requires 3DS2 context. Collect it when the customer confirms payment and include the complete server-merged customer object. |
INVALID_PAYMENT_CUSTOMER | Send one payment and match the documented customer shape exactly. Do not include a billing address or extra properties. |
INVALID_CUSTOMER_INITIATED_PAYMENT | Confirm that the card belongs to the tab’s customer, the payment covers the full balance, and the location supports this flow. |
PAYMENT_PENDING | Retrieve the latest tab and continue checking the existing payment instead of creating another charge. |
After correcting a customer-context or billing-address error, collect fresh CVC before the customer confirms the replacement request. Do not automatically resubmit a previous CVC value.
GoTab Wallet checkout
Section titled “GoTab Wallet checkout”Use Wallet when you want GoTab to authenticate the customer, present their Wallet cards, and complete the payment inside an embedded checkout.
- Server: Create the tab with a supported guest identifier, but without an integration
customerId. - Server: Create a payment session for the tab.
- Server → browser: Send the payment session ID—not your Integrations API bearer token—to the browser.
- Browser: Mount Wallet with the tab UUID and payment session ID. Wallet authenticates the customer and submits the payment.
- Browser → server: After Wallet reports success, ask your server to retrieve the latest tab before completing the order.
Wallet collects payment-time CVC when required, displays any required 3D Secure verification, and can resume the payment after the customer returns to your checkout. See the GoTab Wallet guide for payment-session setup, checkout and card-management modes, configuration, and recovery behavior.
Apple Pay checkout
Section titled “Apple Pay checkout”Use Apple Pay when you want to offer a standalone Apple Pay button without asking the customer to sign in to Wallet or select an integration customer’s saved card.
- Server: Create the tab with a supported guest identifier, but without an integration
customerId. - Browser: Mount
initApplePaywith the tab UUID and the registered hostname serving your checkout. - Browser: Let the component submit the payment; do not also submit one through the Integrations API.
- Browser → server: After
onPaymentSuccess, ask your server to retrieve the latest tab before completing the order.
Apple Pay must be enabled for the location, and the checkout must use a registered HTTPS domain. The button appears only in supported Apple and Safari environments. See Apple Pay in the Payment SDK guide for the browser-side configuration.
Test the shared completion rule
Section titled “Test the shared completion rule”All three flows rejoin at the same stop condition: your server has retrieved the latest tab and confirmed that it is closed with no balance due. Before release, verify that your checkout preserves this rule when:
- A customer clicks the payment control more than once.
- The payment remains pending or awaits verification.
- The customer refreshes, leaves, or returns to the page.
- A browser callback is delayed or never arrives.
- Server reconciliation fails temporarily.
In every case, keep payment controls disabled while an existing payment may still be active, keep Integrations API credentials on the server, and keep fulfillment blocked until server reconciliation reaches the stop condition.