Payment SDK
Use the GoTab Payment SDK to manage saved cards, customize card forms, support 3D Secure verification, and mount Apple Pay.
Use the Payment SDK after you have chosen a payment flow. It owns browser-side card forms, saved-card access, 3D Secure verification, and Apple Pay. Your server still owns customer lookup, tab creation, stored-card payment submission, and final payment confirmation.
If GoTab should own customer sign-in and saved-card selection, leave this page and follow the GoTab Wallet guide.
Choose an SDK path
Section titled “Choose an SDK path”| Your checkout needs to | Start here |
|---|---|
| Add, display, or delete cards from an integration customer profile | Manage saved cards |
| Collect a billing address for a location that requires one | Collect a billing address when required |
| Add 3D Secure verification to a stored-card payment | 3D Secure 2 and Payment Action |
| Display a standalone Apple Pay button | Apple Pay |
| Match card and address fields to your checkout | Style card and address fields |
| Let GoTab authenticate the customer and display Wallet cards | GoTab Wallet |
Prepare credentials and identifiers
Section titled “Prepare credentials and identifiers”GoTab issues separate credentials for server API calls and browser SDK methods. If you have not received the browser credentials or need a payment feature enabled, contact GoTab API Support.
| Credential | Where to use it | Purpose |
|---|---|---|
| Integrations API access secret and bearer token | Server only | Make Integrations API requests. These are not Payment SDK credentials. |
clientApiAccessId | Browser | Identifies your integration to the Payment SDK. Payment Action requires this value. |
clientApiAccessSecret | Browser | Authorizes card-management and Wallet SDK methods. It is not your Integrations API access secret. |
An integration customer is resolved or created through /api/integrations/customers. The Payment SDK uses identifiers from that customer and the selected card:
| Identifier | SDK use |
|---|---|
customer_profile_uuid | Scope card forms and saved-card methods to the integration customer. |
payment_method_uuid | Identify the card selected by the customer when sending it to your server. |
locationUuid | Identify the GoTab location where a new card will be used. |
tabUuid | Identify the tab when mounting Apple Pay. |
The integration customer’s customer_id is used by your server when creating a tab, not by the Payment SDK. See the stored-card payment flow for the complete relationship between the customer, tab, card, and payment request.
Load the SDK
Section titled “Load the SDK”Include the SDK from the GoTab CDN after you have the browser credentials required by your chosen path:
<script src="https://sdk.gotab.io/payments-sdk.umd.js"></script>The script exposes window.PaymentsSDK. If the object is missing, confirm that the script loaded successfully before debugging credentials or method configuration.
Manage saved cards
Section titled “Manage saved cards”These methods manage an integration customer’s saved cards. They do not create or pay a tab. A basic stored-card checkout moves through three observable states:
fetchPaymentMethodsreturns the customer’s cards, or an emptycardsarray prompts you to mount the add-card form.- The customer selects a card and your browser sends its
payment_method_uuidto your server. - Your server submits the payment, retrieves the latest tab, and confirms that the tab is closed with no balance due.
Add a card
Section titled “Add a card”Mount initAddCardForm in the browser after your server has resolved or created the integration customer:
PaymentsSDK.initAddCardForm('#add-card', { clientApiAccessId, clientApiAccessSecret, customerProfileUuid, locationUuid, formProps: {}, onSuccess: (_status, { card }) => selectPaymentMethod(card), onError: showError,});onSuccess runs after the card has been stored. Here, selectPaymentMethod represents your checkout’s selection logic; the returned card.payment_method_uuid is the identifier to send to your server when the customer confirms payment.
Fetch cards
Section titled “Fetch cards”const { cards } = await PaymentsSDK.fetchPaymentMethods( clientApiAccessId, clientApiAccessSecret, customerProfileUuid,);Render the returned cards in your checkout and retain the selected card’s payment_method_uuid. An empty array means the integration customer has no saved cards yet; keep the add-card form available.
Delete a card
Section titled “Delete a card”const deleted = await PaymentsSDK.deletePaymentMethod( clientApiAccessId, clientApiAccessSecret, customerProfileUuid, paymentMethodUuid,);When deleted is true, remove the card from the checkout or fetch the list again. Fetch and delete operations are both scoped to customerProfileUuid. A listed card has this shape:
interface Card { payment_method_uuid: string; visual_cue: string; payment_type: string; expire_year: string; expire_month: string; name: string; zip: string;}Collect a billing address when required
Section titled “Collect a billing address when required”A billing address is not required by default. GoTab submits it with a stored-card payment only when both 3DS2 and the billing-address requirement are enabled for the location. Contact GoTab API Support to confirm whether that requirement applies to your integration.
The billing address belongs to the saved payment method, not to the payment request. Collect it while adding a new card, or add or modify it later with initEditMethodForm. Billing addresses are not returned in Card list results and should not be stored in browser storage.
Add a card with an address
Section titled “Add a card with an address”initAddCardForm collects only card fields by default. When adding a new card, set includeBillingAddress: true to collect an address and save it with that payment method. Omit countryCode for an editable US default, or provide a two-letter code to fill and lock the country field:
PaymentsSDK.initAddCardForm('#add-card', { clientApiAccessId, clientApiAccessSecret, customerProfileUuid, locationUuid, formProps: {}, includeBillingAddress: true, countryCode: 'CA', onSuccess: (_status, { card }) => selectPaymentMethod(card), onError: showError,});The form provides country-specific labels and validation for US, CA, and AU, and generic address labels for other two-letter country codes. The SDK uppercases the country code and any region or postal-code fields whose country format requires it. The saved card’s zip value is populated from postalCode.
Edit an existing card address
Section titled “Edit an existing card address”For an already saved card, use initEditMethodForm to add a billing address or modify the existing one. The card must belong to the integration customer, and its identity remains read-only:
PaymentsSDK.initEditMethodForm('#edit-method', { clientApiAccessId, clientApiAccessSecret, customerProfileUuid, paymentMethod: card, countryCode: 'CA', onSuccess: showPaymentMethods, onCancel: showPaymentMethods, onError: showError, onLoading: setLoading,});This example returns to the saved-card list after save or cancel. If you opened the editor in response to BILLING_ADDRESS_REQUIRED, retry the payment once from onSuccess; canceling the editor should not retry it.
The editor unmounts before it calls onSuccess or onCancel. The initializer also returns an unmount() handle if your page needs to remove it earlier; manually unmounting does not run either callback. If no address exists, countryCode becomes the locked initial country; without it, the form starts with editable US. If an address already exists, a different configured country is ignored so it cannot overwrite the stored country.
The address contract is:
interface Address { addressLine1: string; addressLine2?: string; dependentLocality?: string; locality: string; administrativeArea?: string; postalCode: string; countryCode: string;}
type BillingAddress = Address;3D Secure 2 and Payment Action
Section titled “3D Secure 2 and Payment Action”GoTab enables 3D Secure 2 (3DS2) per location for eligible stored-card payments. If it is not enabled for a location, collectPaymentCustomerContext(clientApiAccessId) and Payment Action are not part of the stored-card flow. Contact GoTab API Support to confirm activation before adding this integration path.
Once 3DS2 is enabled, your checkout needs to collect browser context for every payment attempt and display Payment Action whenever the card issuer requests verification.
The browser can observe progress, but only your server decides whether checkout is complete. Keep the current payment controls disabled from submission until reconciliation reaches either a paid or safely retryable state:
flowchart TD confirm[Customer confirms payment] --> context[Collect fresh browser context] context --> submit[Server sends the payment request] submit -->|HTTP 200| reconcile[Fetch latest tab and payment state] submit -->|HTTP 202 with handoff| action[Retain handoff and mount initPaymentAction] submit -->|Response lost| recover[Retransmit the same request] recover --> submit action --> observed[Complete, cancel, error, pending, or return] observed --> reconcile reconcile -->|Closed with no balance| fulfill[Fulfill the order] reconcile -->|Payment still pending| wait wait[Keep payment controls disabled] --> reconcile reconcile -->|Safe to retry| retry[Offer another payment method]
Collect browser context
Section titled “Collect browser context”Call collectPaymentCustomerContext(clientApiAccessId) when the customer confirms payment, then send its result to your server with the selected payment_method_uuid:
const paymentCustomerContext = await PaymentsSDK.collectPaymentCustomerContext(clientApiAccessId);
await fetch('/checkout/pay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tabUuid, paymentMethodUuid: selectedCard.payment_method_uuid, paymentCustomerContext, }),});The helper returns:
interface PaymentCustomerContext { browser: { colorDepth: number; language: string; screenHeight: number; screenWidth: number; timezoneOffsetMinutes: number; }; deviceFingerprint?: string;}You can also construct the same object without calling the helper:
const deviceFingerprint = await collectYourDeviceFingerprint();const paymentCustomerContext = { browser: { colorDepth: window.screen.colorDepth, language: window.navigator.language, screenHeight: window.screen.height, screenWidth: window.screen.width, timezoneOffsetMinutes: new Date().getTimezoneOffset(), }, ...(deviceFingerprint ? { deviceFingerprint } : {}),};GoTab does not require a particular fingerprinting library. If you supply deviceFingerprint, use a non-empty value from your own device-identification implementation that identifies the shopper’s device and is no longer than 5,000 characters. Omit it when unavailable instead of sending placeholder data or a new random value for every payment.
When you use the helper, clientApiAccessId must identify an enabled integration, and the checkout page’s origin must appear in that integration’s gotab_wallet_allowed_origins setting. This is the same origin registration used by GoTab Wallet, even if you do not otherwise use Wallet. Use the access ID issued to the integration that owns the stored card. The SDK collects the browser measurements locally. For the optional device fingerprint, production creates a temporary hidden frame at https://gotab.io/wallet/payment-action?clientApiAccessId=<clientApiAccessId>, exchanges messages only with that origin, combines the response with those measurements, and removes the frame.
The device fingerprint is best effort. If the frame cannot load or respond, or fingerprint collection fails or times out, the helper still resolves with the local browser measurements and omits deviceFingerprint.
Supplying the context yourself avoids the helper’s hidden frame. If a payment response includes a handoff, however, initPaymentAction still requires the same registered origin and a CSP that permits the GoTab frame.
Your server adds the customer email, source IP, Accept header, and User-Agent from the same request. See Add 3D Secure 2 for the complete browser-to-server example and Integrations API request.
Continue an incomplete payment
Section titled “Continue an incomplete payment”Enabling 3DS2 does not mean every customer will be challenged. The card issuer determines the outcome for each payment:
- Frictionless: The payment is authenticated without customer interaction and continues through the normal payment response.
- Challenge: The API returns HTTP
202withstatus: "requires_action", and your integration displays Payment Action so the customer can verify the payment. - Pending: The API returns HTTP
202withstatus: "pending"while the existing attempt is still in progress. This can be the response to the original request or to the same request repeated after a timeout.
Whenever a successful HTTP 202 response includes a handoff, mount initPaymentAction and pass it unchanged, whether the status is requires_action or pending. Keep the handoff in the current checkout state while the payment remains incomplete.
Each Payment Action callback should ask your server to check the latest payment status. In this example, confirmPaymentOnYourServer represents an endpoint in your application that retrieves the latest GoTab tab and payment state, then returns paid, pending, or retryable to the browser.
async function reconcilePayment() { let state; try { state = await confirmPaymentOnYourServer(tabUuid); } catch { showPaymentStatusUnavailable(); return; }
if (state === 'paid') { fulfillOrder(); return; }
if (state === 'pending') { showPaymentStillProcessing(); return; }
showAnotherPaymentMethod();}
if (paymentResponse.handoff) { PaymentsSDK.initPaymentAction('#payment-action', { clientApiAccessId, handoff: paymentResponse.handoff, onComplete: reconcilePayment, onCancel: reconcilePayment, onError: reconcilePayment, });}If a timeout or dropped connection prevents the browser from receiving the payment result, repeat the exact original Integrations API payment request through your server. The API checks for the existing attempt before creating a payment and returns its current handoff or terminal state. Use the same recovery path if a reload loses the in-memory handoff; do not construct a different payment request.
The server should return paid only after confirming a closed tab with no balance due. It should return pending while the existing payment is still active, and retryable only after confirming that another payment can safely begin. If the server check fails, showPaymentStatusUnavailable should keep payment controls disabled and tell the customer that the payment cannot be confirmed yet.
The component unmounts before it calls onComplete or onCancel. The initializer also returns an unmount() handle if your page needs to remove it earlier. Closing the component or calling unmount() does not cancel the payment. If verification must continue on another page, the component prompts the customer to continue and returns them to your checkout afterward.
The issuer returns the customer to the checkout URL from which Payment Action launched. The URL fragment contains a one-time gotab_payment_resume value; do not copy it into application URLs, analytics, or logs. On that same checkout route, mount initPaymentAction again without handoff. The SDK consumes its fragment value, preserves your existing query and other fragment entries, and resumes the same payment:
PaymentsSDK.initPaymentAction('#payment-action', { clientApiAccessId, onComplete: reconcilePayment, onCancel: reconcilePayment, onError: reconcilePayment,});Closing the component, refreshing the page, or navigating away does not cancel an in-progress payment. A callback reports what the SDK observed; your server’s latest tab and payment state determines what the checkout can do next.
Decide whether to fulfill, wait, or retry
Section titled “Decide whether to fulfill, wait, or retry”| Result or event | What the customer sees | What your integration does next |
|---|---|---|
HTTP 200 | Confirmation or a brief finishing state | Fetch the latest tab on your server. Complete the order only when the tab is closed with no balance due. |
HTTP 202 with a handoff (requires_action or pending) | Payment Action or a processing state | Retain the handoff, keep other payment controls disabled, and mount initPaymentAction with it. |
| Payment request times out or its response is lost | A processing state | Repeat the exact original payment request through your server. Mount Payment Action if the recovered response includes a handoff; do not create a different payment. |
succeeded callback | Confirmation or a brief finishing state | Reconcile on your server before completing the order. |
pending callback | A clear “still processing” state | Retain the handoff, keep payment controls disabled, and continue checking the existing payment. Do not submit a new payment. |
refused callback | A payment-not-completed message | Reconcile on your server. Offer another method only after the server confirms it is safe to retry. |
| Cancel, SDK error, refresh, or navigation away | Checkout or a processing state | Reconcile on your server before enabling payment controls. Resume Payment Action when the customer returns from verification; if the handoff was lost, repeat the original payment request to recover it. |
| Handoff expires or cannot resume | A payment-status message | Treat expiry as an unavailable verification session, not proof of payment failure. Reconcile on your server before deciding whether to retry. |
Test interrupted payments
Section titled “Test interrupted payments”Before release, test each path your checkout supports:
- A payment that completes without a challenge.
- A challenge that succeeds and one that is refused.
- A
pendingHTTP202response that includes a handoff. - A refresh before verification begins and after the customer returns.
- A lost initial payment response. Repeat the exact request and confirm that the existing
paymentId,handoff, andexpiresAtare recovered. - A lost callback or temporary network failure during server reconciliation.
- A double click or repeated mount while the original payment is pending.
- A payment that remains pending longer than the customer’s browser session.
Ask GoTab API Support which test scenarios and payment methods are available for your integration environment.
Apple Pay
Section titled “Apple Pay”Before mounting the button, create the tab with a supported guest identifier but without an integration customerId, serve checkout over HTTPS, and register the checkout domain for Apple Pay. Ask GoTab API Support to confirm that Apple Pay is enabled for the location.
Then mount the button with the tab and registered hostname:
PaymentsSDK.initApplePay('#apple-pay', { tabUuid, domainName: window.location.hostname, onPaymentSuccess: async () => { const tab = await confirmTabOnYourServer(tabUuid); if (tab.status === 'CLOSED' && tab.balanceDue === 0) { fulfillOrder(); } else { showPaymentStillProcessing(); } },});The button renders only in supported Apple and Safari environments. onPaymentSuccess is a progress signal: complete the order only after your server retrieves the latest tab and confirms that it is closed with no balance due. See the Apple Pay flow for the full checkout sequence.
Style card and address fields
Section titled “Style card and address fields”Once the payment path works, use appearance to apply shared styling to card and billing-address fields. Use formProps for card-field options such as placeholders, card icons, and per-field CSS. CSS in formProps overrides the shared appearance for that field.
const appearance = { field: { color: '#16130f', backgroundColor: '#ffffff', borderColor: '#bcbcbc', borderRadius: '4px', fontFamily: 'Inter, sans-serif', fontSize: '15px', placeholderColor: '#777777', focusBorderColor: '#8a6d2c', errorColor: '#c62828', successColor: '#15803d', },};styleOverrides is a regular JavaScript object, not an SDK configuration property. Define it once and spread it into multiple formProps entries to reuse the same card-field styles:
const styleOverrides = { css: { boxSizing: 'border-box', fontFamily: 'Arial, sans-serif', '&::placeholder': { color: '#999999' }, }, successColor: '#2e8b57', errorColor: '#dc143c',};Combine the shared appearance with the per-field overrides, placeholders, and callbacks when mounting the form:
PaymentsSDK.initAddCardForm('#add-card', { clientApiAccessId: 'YOUR_CLIENT_API_ACCESS_ID', clientApiAccessSecret: 'YOUR_CLIENT_API_ACCESS_SECRET', customerProfileUuid: 'CUSTOMER_PROFILE_UUID', locationUuid: 'LOCATION_UUID', formProps: { first_name: { placeholder: 'First name', ...styleOverrides }, last_name: { placeholder: 'Last name', ...styleOverrides }, email: { placeholder: 'Email address', ...styleOverrides }, card_number: { placeholder: '•••• •••• •••• ••••', showCardIcon: true, ...styleOverrides }, cvc: { placeholder: 'CVC', showCardIcon: true, ...styleOverrides }, card_exp: { placeholder: 'MM / YY', ...styleOverrides }, }, includeBillingAddress: true, appearance, onLoading: (loading) => setCardFormLoading(loading), onSuccess: (_status, { card }) => selectPaymentMethod(card), onError: (error) => showCardFormError(error.message), stateCallback: (state) => updateCardFormState(state),});For card fields, the SDK applies formProps after appearance, so per-field options and CSS take precedence. Because styleOverrides is spread only into formProps, use appearance for styling that should also apply to billing-address fields. Your page CSS controls labels, buttons, and layout.
Global API
Section titled “Global API”| Method | Purpose |
|---|---|
initAddCardForm(container, config) | Add a saved card, optionally with a billing address. |
initEditMethodForm(container, config) | Add or modify a saved card’s billing address. |
fetchPaymentMethods(id, secret, customerProfileUuid) | Fetch an integration customer’s cards. |
deletePaymentMethod(id, secret, customerProfileUuid, paymentMethodUuid) | Delete an integration customer’s card. |
collectPaymentCustomerContext(clientApiAccessId) | Collect browser measurements and an optional device fingerprint for a 3DS2-enabled stored-card payment. |
initPaymentAction(container, config) | Continue or resume a 3D Secure payment action. |
initWallet(container, config) | Mount GoTab Wallet in checkout or card-management mode. |
initApplePay(container, config) | Mount a standalone Apple Pay checkout button. |
Every initializer accepts an HTMLElement or CSS selector as container. initEditMethodForm and initPaymentAction return an { unmount() } handle.
Configuration reference
Section titled “Configuration reference”PaymentsSDKConfig
Section titled “PaymentsSDKConfig”Used by initAddCardForm.
| Property | Type | Required | Description |
|---|---|---|---|
clientApiAccessId | string | Yes | Browser SDK access ID. |
clientApiAccessSecret | string | Yes | Browser SDK access credential. |
customerProfileUuid | string | Yes | Integration customer profile that owns the saved card. |
locationUuid | string | Yes | Location where the payment method will be used. |
formProps | object | Yes | Card-field placeholders, icons, validation, and per-field CSS. Use {} for defaults. |
includeBillingAddress | boolean | No | Collects a billing address and saves it with the new payment method. Defaults to false. |
countryCode | string | No | Two-letter initial country. When supplied, the country field is locked. |
appearance | PaymentFormAppearance | No | Shared appearance for card and billing-address fields. |
onSuccess | (status, data) => void | Yes | Receives the HTTP status and { card, errors } after the method is stored. |
onError | (error) => void | Yes | Receives initialization, validation, or submission errors. |
onLoading | (isLoading) => void | No | Reports initialization and submission loading state. |
stateCallback | (state) => void | No | Reports card-field focus, validity, errors, and detected card details. |
stateCallback receives:
interface FieldState { isDirty: boolean; isFocused: boolean; isValid: boolean; isEmpty: boolean; isTouched: boolean; errorMessages: string[]; last4?: string; bin?: string; cardType?: string;}EditMethodFormConfig
Section titled “EditMethodFormConfig”| Property | Type | Required | Description |
|---|---|---|---|
clientApiAccessId | string | Yes | Browser SDK access ID. |
clientApiAccessSecret | string | Yes | Browser SDK access credential. |
customerProfileUuid | string | Yes | Integration customer profile that owns the card. |
paymentMethod | Card | Yes | Existing card identity. Card fields remain read-only. |
countryCode | string | No | Two-letter fallback country when no address is stored. |
appearance | PaymentFormAppearance | No | Shared billing-address field appearance. |
onSuccess | (billingAddress) => void | Yes | Receives the saved billing address. |
onError | (error) => void | Yes | Receives load, validation, or submission errors. |
onCancel | () => void | No | Runs when the editor closes without saving. |
onLoading | (isLoading) => void | No | Reports loading and submission state. |
PaymentActionConfig
Section titled “PaymentActionConfig”| Property | Type | Required | Description |
|---|---|---|---|
clientApiAccessId | string | Yes | Public SDK access ID issued to the same integration as the API payment. |
handoff | string | No | Pass the value unchanged whenever a successful HTTP 202 response includes it, whether the status is requires_action or pending. Omit it after an issuer return so the SDK consumes gotab_payment_resume from the URL fragment. |
theme | WalletTheme | No | Theme applied to the verification screen. See the supported Wallet themes. |
onComplete | (result) => void | Yes | Reports a succeeded, refused, or pending result. Reconcile the payment on your server before changing checkout state. |
onCancel | () => void | No | Runs when the customer chooses Back to checkout. It does not cancel the payment; reconcile on your server. |
onError | (error) => void | No | Reports initialization, timeout, or invalid-response errors. Reconcile on your server before allowing another payment. |
interface PaymentActionResult { status: 'succeeded' | 'refused' | 'pending'; paymentId: string;}Treat PaymentActionResult as an update for your checkout UI. Check the latest tab and payment state through your server before changing what the customer can do next.
ApplePayConfig
Section titled “ApplePayConfig”| Property | Type | Required | Description |
|---|---|---|---|
tabUuid | string | Yes | UUID of the tab created without an integration customerId. |
domainName | string | Yes | Registered HTTPS hostname serving your checkout. |
onPaymentSuccess | (data) => void | No | Runs after Apple Pay completes. Confirm the tab on your server before completing the order. |
See GoTab Wallet configuration for WalletConfig.
Content Security Policy
Section titled “Content Security Policy”If your page sends a blocking Content Security Policy, merge the following sources into its existing directives. These are additions, not a complete policy; include only capabilities you use.
| SDK capability | Required CSP addition |
|---|---|
| Wallet, Apple Pay, or Payment Action | Add the GoTab origin provided for your environment to frame-src. |
| Cross-origin SDK API calls | Add the configured API origin to connect-src. Same-origin /api requests need no additional source. |
| Card entry fields | Add https://js.verygoodvault.com and https://js3.verygoodvault.com to script-src and frame-src; add both plus https://vgs-collect-keeper.apps.verygood.systems to connect-src. |
3DS2 context collection with collectPaymentCustomerContext(clientApiAccessId) | Add https://gotab.io to frame-src. |
| SDK forms and Payment Action | Add 'unsafe-inline' to style-src. |
For a page using every capability, the additions are equivalent to:
script-src https://js.verygoodvault.com https://js3.verygoodvault.com;connect-src <API origin> https://js.verygoodvault.com https://js3.verygoodvault.com https://vgs-collect-keeper.apps.verygood.systems;frame-src https://gotab.io https://js.verygoodvault.com https://js3.verygoodvault.com;style-src 'unsafe-inline';Replace <API origin> with an origin, not a full path, and retain existing sources such as 'self'. This example uses GoTab’s production origin; use the GoTab origin provided for another environment.
For Metro, 3DS2 context collection requires https://gotab.io in frame-src.
Verify before production
Section titled “Verify before production”- Keep Integrations API credentials and bearer tokens on your server.
- Use only the separately issued browser credentials in Payment SDK configuration.
- Ensure every checkout origin that calls
collectPaymentCustomerContext(clientApiAccessId)or mountsinitPaymentActionis covered by the integration’sgotab_wallet_allowed_originssetting. - For 3DS2-enabled locations, collect payment context only when the customer confirms payment.
- Do not add handoff values, payment-session tokens, or bearer tokens to your own URLs or logs.
- Do not start another payment while the current payment is pending or awaiting verification.
- Fetch the latest tab and verify its final state and balance before completing the order.