GOAT Network
GOAT Flow

Checkout

Checkout adds payment to your website or application's purchase flow. Use Hosted Checkout for GOAT Flow's ready-made payment page or components, or Custom Integration to build your own payment interface and interactions using GOAT Flow APIs and SDKs.

Choose an Integration

PathBest forPrimary component
Hosted CheckoutMerchants that want a complete hosted payment page, an embedded checkout, or a payment element with minimal payment UI developmentPayKit React SDK; direct session URL or optional goatflow-checkout for Hosted Page
Custom IntegrationMerchants that build their own payment interface, wallet interactions, and payment-state handlinggoatflow-sdk-server and payment APIs; optional Browser SDK for Crypto, Stripe.js for custom Card forms

In both paths, your application owns its products, cart, pricing, and fulfillment. The choice is who builds the payment interface, not who owns the cart or order. Confirm payment on your backend before fulfillment.

For merchant-configured products and shareable payment links, see QuickPay.

Integration Setup

Server SDK Setup

Create sessions and payment orders on your backend. Both integration paths can use the Server SDK to authenticate merchant API requests.

npm install goatflow-sdk-server
server.ts
import { GoatFlowClient } from 'goatflow-sdk-server'

const goatFlow = new GoatFlowClient({
  baseUrl: process.env.GOATX402_API_URL!,
  apiKey: process.env.GOATX402_API_KEY!,
  apiSecret: process.env.GOATX402_API_SECRET!,
})

Use the API URL and credentials for the selected environment. Keep merchant keys and secrets on the server; never reuse them across environments.

Crypto Prerequisites

The Crypto examples on this page use DIRECT receiving: the buyer pays the merchant's configured receiving address.

  • The merchant account must be enabled and configured for DIRECT receiving.
  • Configure at least one supported receiving chain and Token. The price must be representable at that Token's precision and satisfy the supported minimum and merchant amount limits; the chain must have a fee configuration.
  • Where an order fee applies, keep enough merchant fee balance.
  • For DIRECT Checkout Sessions offering Crypto, the current backend also requires the merchant's quickpay_enabled setting to be on. This requirement applies to session-based Crypto checkout, not ordinary createOrder() calls.

See Merchant Portal for account and receiving setup.

Card Prerequisites

  • Card payments must be enabled for the merchant account.
  • The merchant's connected Stripe account must be authorized, not disabled, and ready to accept charges.
  • The selected GOAT Flow deployment must have its Stripe integration configured.

A Card-only Checkout Session created through the authenticated merchant API (paymentRails: ['fiat']) does not require quickpay_enabled or Crypto receiving Tokens. These account prerequisites apply to both Hosted Checkout and custom Card forms.

Checkout Sessions

A Checkout Session stores the payment terms for a purchase. Create it on your backend for all three Hosted Checkout UI options and for custom Card forms. Custom Crypto payments can instead create a payment order directly, as shown under Custom Integration.

Create a Checkout Session

Use the Server SDK configured above and derive the amount and line items from your own backend order. Select the payment methods explicitly.

Crypto only

const session = await goatFlow.createCheckoutSession({
  checkoutType: 'DIRECT',
  paymentRails: ['crypto'],
  price: '19.95',
  lineItems: [{ name: 'Coffee mug', quantity: 1, amount: '19.95' }],
  clientReferenceId: 'cart_9f31',
  expiresIn: 1800,
})

The Crypto payment order and its fee are created when the buyer binds a payment option, not when the session is created. A created session does not guarantee that later order creation will pass the account, Token, limit, and fee checks.

Card only

const cardSession = await goatFlow.createCheckoutSession({
  checkoutType: 'DIRECT',
  paymentRails: ['fiat'],
  fiatCurrency: 'USD',
  fiatAmount: '19.95',
  lineItems: [{ name: 'Coffee mug', quantity: 1, amount: '19.95' }],
  clientReferenceId: 'order_2026_00043',
  expiresIn: 3600,
})

paymentRails, fiatCurrency, and fiatAmount are GOAT Flow Server SDK parameters, not Stripe API parameters. The SDK value for Card is fiat, not card. fiatAmount is a decimal amount in fiatCurrency, not a Token amount in wei. Connecting Stripe does not automatically add Card to a session; omitting paymentRails defaults to Crypto only.

Card and Crypto

Set paymentRails: ['crypto', 'fiat'] and supply both the Crypto price and the Card fiatCurrency and fiatAmount. For example, use price: '19.95' alongside fiatCurrency: 'USD' and fiatAmount: '19.95'. Both quotes are set on your backend; GOAT Flow does not automatically convert one into the other. The buyer chooses one payment method for the purchase.

Return the session's checkoutId, url, and expiresAt to the browser. Keep API keys and secrets on the server, and avoid logging the Checkout Session URL.

Optional Redirect URLs

The starter example intentionally omits successUrl and cancelUrl. Add them only after the permitted return origins or URL prefixes have been configured in your merchant's redirect allowlist. Arrange this with your GOAT Flow integration contact; the current Merchant Portal does not provide a self-service allowlist editor.

A non-empty redirect URL must match an allowed origin and any configured path prefix. Production URLs must use HTTPS. If no allowlist is configured, sending either URL causes session creation to fail; do not assume your website is allowed automatically. If your integration does not need automatic return navigation, omit both fields. A successful return is a UX signal, not proof of payment.

Session References and Recovery

clientReferenceId is optional, but a non-empty value must be unique for that merchant while the existing session record is retained. Repeating the same reference returns HTTP 409, even with identical request parameters; it does not return the original session. This is a duplicate-creation guard, not an idempotent replay API. Expiration alone does not release the reference.

Persist the returned checkoutId, url, and expiresAt together with your clientReferenceId and internal order before presenting the payment entry point. Reuse that session while it remains valid and payable instead of creating one on every button click or page refresh.

If creation times out or its outcome is unknown, first check your saved session and reconcile the original attempt. A 409 response does not recover the original ID or URL: the platform stores a hash of the session handle, not the raw handle. Do not automatically change the reference and create another payable session. Use your integration contact for unresolved cases. Create a replacement only after confirming the previous attempt is no longer payable and reconciling any payment; give the replacement its own reference and keep fulfillment idempotent for the same internal order.

Session Expiration

expiresIn is a requested lifetime in seconds, not a guaranteed exact deadline. Crypto-only sessions default to 30 minutes and are bounded to 60 seconds through 24 hours. Any session that offers Card (fiat), including a Card-and-Crypto session, has a minimum lifetime of 60 minutes: the backend raises a shorter request to that minimum. Always use the returned Unix-seconds expiresAt for display and reconciliation rather than calculating a deadline from expiresIn.

The session expiry and the deadline for starting a Card payment are different. A new Card payment attempt currently requires more than 32 minutes remaining on the session: a 30-minute provider requirement plus a 2-minute safety buffer. If that window is too short, initialization returns HTTP 410, even if the session itself has not expired. Do not promise that a buyer can start a new Card payment at any point before expiresAt. Recovery of an already-created payment attempt follows its existing state; do not replace it blindly.

Hosted Checkout

Hosted Checkout provides ready-made payment UI for the server-created session. Choose the surface that fits your application's layout:

UI optionWhat it providesHow it fits into your site
Hosted PageA complete, standalone GOAT Flow-hosted payment pageOpen the session URL directly, use the PayKit React SDK's Hosted Page link, or use optional goatflow-checkout.
Embedded CheckoutA complete checkout block with merchant information and payment UIEmbed QuickPayEmbeddedCheckout from the PayKit React SDK.
Payment ElementThe GOAT Flow payment section without the surrounding product or cart UIEmbed QuickPayPaymentElement from the PayKit React SDK alongside your own content.

The available payment methods depend on the session's payment rails, merchant configuration, and the selected component's Card support.

The React examples use the current source exports from the draft package @goatx402/quickpay-react. It is not currently published to npm; obtain a release-approved package artifact before using those examples. This applies to the Hosted Page link, Embedded Checkout, and Payment Element. Opening the returned session URL directly does not require the React package.

Hosted Page

Send the buyer to the complete GOAT Flow payment page using the url returned when your backend creates the session. This works for Crypto, Card, or both, according to the session's payment methods.

export function openHostedCheckout(session: { url: string }) {
  window.location.assign(session.url)
}

You can also use a normal link to open that URL in another browser tab.

The React link component opens the Checkout Session created by your backend; it does not create the session or set the price.

HostedCheckoutLink.tsx
import { PayKitHostedPageLink } from '@goatx402/quickpay-react'

export function HostedCheckoutLink({
  session,
}: {
  session: { checkoutId: string; url: string }
}) {
  return (
    <PayKitHostedPageLink
      basePath={new URL(session.url).origin}
      source={{
        type: 'checkout-session',
        checkoutId: session.checkoutId,
        checkoutType: 'direct',
      }}
      target="_blank"
      rel="noopener noreferrer"
    >
      Continue to payment
    </PayKitHostedPageLink>
  )
}

basePath uses the hosted origin from the backend response, not your storefront origin or a guessed merchant URL. The helper builds the standard hosted session route. If your deployment uses a custom route, URL prefix, or additional URL parameters, navigate to the complete session.url instead.

The component renders a link, so it does not need QuickPayProvider or apiBase. Omit target="_blank" to navigate in the current tab. This link does not provide a payment-success callback; confirm the payment and fulfillment conditions on your backend.

Drop-in Checkout SDK

This is an optional alternative to the React link or direct navigation, not a prerequisite for Hosted Page.

Install the framework-free browser package:

npm install goatflow-checkout

Use the checkoutId and url returned by your backend to open that Checkout Session. The returned URL supplies the hosted page's origin.

checkout.ts
import { GoatCheckout } from 'goatflow-checkout'

export function openCheckout(session: { checkoutId: string; url: string }) {
  const checkout = GoatCheckout({
    origin: new URL(session.url).origin,
  })

  checkout.open({
    checkoutId: session.checkoutId,
    display: 'tab',
    onSuccess(result) {
      // Update the UI only. Confirm fulfillment on the backend.
      console.log(result.status, result.tx_hash)
    },
  })
}

Load the session from your backend before enabling the payment button, then call openCheckout(session) synchronously from its click or tap handler. Do not wait for a network request inside that handler before opening a new tab or window, because the browser may block the new window.

Choose how to open the complete Hosted Page:

  • display: 'redirect' navigates the current browser tab to the full payment page.
  • display: 'tab' opens the full payment page in a new browser tab.

See SDK Reference for the supported display options.

For a full-page redirect without the browser SDK, navigate directly to the session.url returned by your backend.

Embedded Checkout

Use the PayKit React SDK's QuickPayEmbeddedCheckout when you want a complete GOAT Flow checkout block inside your application's page. Pass the server-created Checkout Session; the component loads its merchant details and payment terms.

For embedded components on a third-party merchant site, explicitly pass the GOAT Flow API base for the selected environment as apiBase, as in the example below. Use the API address provided for your integration, not an assumed storefront or payment-page origin. The API must allow your site's origin through CORS. This is public connection configuration; never pass merchant API keys or secrets to this provider.

If apiBase is omitted and no build-time API base is configured, requests are same-origin relative. Your site must then reverse-proxy /quickpay/v1, /checkout/v1, and /mpp/v1 to the GOAT Flow backend. Without an API base or those proxies, requests will go to your storefront instead of GOAT Flow. This requirement applies to both Crypto and Card embeds.

The Card presentation depends on the deployed component and backend: an enabled Stripe Elements integration can render the Card form in-page, while a hosted-only deployment hands the buyer off to the Hosted Page. Verify the Card behavior of the package and deployment you use rather than assuming that every embedded package supports in-page Card entry.

import {
  QuickPayEmbeddedCheckout,
  QuickPayProvider,
} from '@goatx402/quickpay-react'
import '@goatx402/quickpay-react/styles.css'

export function Checkout({
  checkoutId,
  apiBase,
}: {
  checkoutId: string
  apiBase: string
}) {
  return (
    <QuickPayProvider apiBase={apiBase}>
      <QuickPayEmbeddedCheckout
        source={{
          type: 'checkout-session',
          checkoutId,
          checkoutType: 'direct',
        }}
        onPaymentSuccess={() => {
          // UX signal only; verify on the backend before fulfillment.
        }}
      />
    </QuickPayProvider>
  )
}

Payment Element

Use QuickPayPaymentElement when your application already displays the product, cart, and order summary and only needs GOAT Flow's ready-made payment section. It uses the same session, provider, API-base configuration, and Card availability requirements as Embedded Checkout.

import {
  QuickPayPaymentElement,
  QuickPayProvider,
} from '@goatx402/quickpay-react'
import '@goatx402/quickpay-react/styles.css'

export function PaymentSection({
  checkoutId,
  apiBase,
}: {
  checkoutId: string
  apiBase: string
}) {
  return (
    <QuickPayProvider apiBase={apiBase}>
      <QuickPayPaymentElement
        source={{
          type: 'checkout-session',
          checkoutId,
          checkoutType: 'direct',
        }}
        onPaymentSuccess={() => {
          // UX signal only; verify on the backend before fulfillment.
        }}
      />
    </QuickPayProvider>
  )
}

Custom Integration

Use Custom Integration when you build the payment interface and coordinate its actions and state yourself. The Server SDK is the primary backend integration, but Crypto and Card use different buyer-side integrations:

  • Crypto: create a payment order on your backend, then let the buyer pay through their wallet. The Browser SDK is optional and handles wallet transfers.
  • Card: create a Card Checkout Session through GOAT Flow, initialize its Elements payment attempt, and build your form around Stripe's secure input components. Your application owns the layout, submit action, and payment-state handling; Stripe handles card details and authentication.

PaymentHelper.pay() is for Crypto, not Card. Do not add Card fields to createOrder() or collect card details through the Browser SDK.

Crypto Payments

This example is for merchants configured with DIRECT receiving: the buyer transfers directly to the merchant's configured receiving address. It handles ERC20_DIRECT orders only and is not a generic handler for every payment flow.

Create an Authoritative Order

Derive price and product data from your backend. Do not accept the amount, receiving address, or token contract directly from an untrusted request.

const payerAddress = '0xBuyerAddress'

const order = await goatFlow.createOrder({
  dappOrderId: 'order_2026_00042',
  chainId: 2345,
  tokenSymbol: 'USDC',
  tokenContract: '0xTokenContract',
  fromAddress: payerAddress,
  amountWei: '1000000',
})

if (order.flow !== 'ERC20_DIRECT') {
  throw new Error('This integration requires a DIRECT payment order')
}

// Return this payment data to the browser, not the merchant credentials.
const browserOrder = {
  orderId: order.orderId,
  flow: order.flow,
  tokenSymbol: order.tokenSymbol,
  tokenContract: order.tokenContract,
  fromAddress: payerAddress,
  payToAddress: order.payToAddress,
  chainId: order.fromChainId,
  amountWei: order.amountWei,
  expiresAt: order.expiresAt,
}

Use a unique dappOrderId for each payment order and persist the returned GOAT Flow orderId against your own order. Ordinary createOrder() requests do not accept idempotencyKey; repeating a dappOrderId is rejected as a duplicate, not returned as an existing order. If creation times out, reconcile the original request before issuing another payable order.

The Server SDK returns fromChainId, whereas the Browser SDK expects chainId and fromAddress. Preserve the payer used at creation and send the mapped browserOrder above through your own backend endpoint. Replace the sample addresses with the validated payer and configured token for the selected chain.

Pay with a Browser Wallet

npm install goatflow-sdk ethers
import { PaymentHelper, type Order } from 'goatflow-sdk'
import { ethers } from 'ethers'

export async function payWithWallet(
  walletProvider: ethers.Eip1193Provider,
  browserOrder: Order,
) {
  if (browserOrder.flow !== 'ERC20_DIRECT') {
    throw new Error('This integration requires a DIRECT payment order')
  }

  const provider = new ethers.BrowserProvider(walletProvider)
  const signer = await provider.getSigner()
  const network = await provider.getNetwork()

  if (network.chainId !== BigInt(browserOrder.chainId)) {
    throw new Error('Switch to the payment chain before continuing')
  }
  if ((await signer.getAddress()).toLowerCase() !== browserOrder.fromAddress.toLowerCase()) {
    throw new Error('Connect the wallet used to create this order')
  }
  if (browserOrder.expiresAt <= Math.floor(Date.now() / 1000)) {
    throw new Error('This payment order has expired')
  }

  const result = await new PaymentHelper(signer).pay(browserOrder)
  if (!result.success) throw new Error(result.error ?? 'Payment failed')
  return result
}

Pass the connected wallet's EIP-1193 provider and the payment data returned by your backend. Verify payer, chain, token, recipient, amount, and expiry before payment. A successful wallet transfer is not a substitute for GOAT Flow's backend confirmation.

Card Payments

For a merchant-built Card form, use Stripe Elements with a GOAT Flow-created payment attempt. This is separate from embedding GOAT Flow's QuickPayPaymentElement: here you build and maintain the form and its interaction logic yourself.

In addition to the Card prerequisites, the deployment must expose the Elements initialization endpoint, have its Stripe publishable key configured, and allow your site's origin through CORS when using a different API origin. Use a Stripe.js version compatible with that deployment. The current frontend implementation uses @stripe/stripe-js 8.7.0.

Initialize the Card Payment Attempt

First create and save a Card Checkout Session on your backend. Then initialize its payment attempt through the GOAT Flow public Checkout API:

POST /checkout/v1/sessions/{checkoutId}/fiat/elements

The request has no amount or card-details body; the amount comes from the server-created session. Use the GOAT Flow API base supplied for your integration, not api.stripe.com. This buyer endpoint uses the opaque Checkout Session handle and does not require merchant API credentials in the browser.

Elements initialization response
{
  "client_secret": "<Stripe Checkout Session client secret>",
  "publishable_key": "pk_live_<platform-publishable-key>",
  "stripe_account_id": "acct_<connected-account>",
  "order_id": "<GOAT Flow payment order ID>",
  "ui_mode": "elements"
}

GOAT Flow creates the associated order and Stripe session. Do not create an unrelated Stripe session or PaymentIntent yourself: it would not belong to this GOAT Flow payment attempt. Keep client_secret and the session handle out of logs, analytics, and unrelated clients.

Build and Submit Your Card Form

  1. Load the compatible Stripe.js client using the returned publishable_key and stripe_account_id as its connected-account context.
  2. Initialize Stripe Checkout with client_secret. In the current frontend's Stripe.js version, this is stripe.initCheckout({ clientSecret }).
  3. Mount Stripe's Payment Element inside your own form. Build the surrounding layout, order summary, customer-email field, pay button, and error/pending states yourself. The sensitive card fields remain inside Stripe's component.
  4. Load Checkout actions, read and display the session total, and update the customer email. Confirm through those same actions from your submit handler. Handle authentication and immediate errors through Stripe.js; disable repeated submits while confirmation is pending.
  5. Use GOAT Flow's session and order status to confirm the result on your backend before fulfillment, as described in Confirm and Fulfill.

See Stripe's custom checkout documentation for the secure input components and form lifecycle. GOAT Flow, rather than your application's Stripe server code, creates and associates the payment attempt; use the integration contract above for that step.

The current backend configures Stripe authentication returns through the GOAT Flow return page. Do not assume every authentication flow stays inside your form or that Stripe returns directly to your storefront.

An Elements attempt and a hosted Card attempt cannot be opened independently for the same active payment attempt. A different UI-mode claim can return 409 with code: "fiat_ui_mode_locked". If initialization times out, retry or reconcile the same session and mode rather than switching to a second payment flow. A missing Elements configuration can return 503 with code: "fiat_elements_unavailable"; do not treat an arbitrary failure as permission to create a replacement charge. The shared session expiry and recovery rules also apply.

Confirm and Fulfill

For the DIRECT receiving examples on this page, both payment methods require backend confirmation before fulfillment. Persist your internal order reference, the Checkout Session ID when used, and the GOAT Flow order ID once available.

Creating a Card Checkout Session returns a checkoutId, not a payment orderId. The payment order is created when Card payment is initialized. Your backend can read the session's status at GET /checkout/v1/sessions/{checkoutId}/status to obtain its bound order_id and order_status when present. Bind that result to the session recorded for your internal order; do not accept an unrelated order ID from the browser.

Once the payment order ID is known, read its current state from your backend and repeat safe reads or process completion webhooks as needed:

export async function isOrderCompleted(orderId: string) {
  const result = await goatFlow.getOrderStatus(orderId)
  return result.status === 'INVOICED'
}

Pass a GOAT Flow payment orderId, not a Checkout Session checkoutId. Check the order's ownership, expected payment terms, and association with your internal order before delivering goods, services, or access. Do not fulfill from a wallet result, a Card return URL, or a browser callback alone.

PAYMENT_CONFIRMED is not a universal permission to fulfill. A Card charge can be real while the platform withholds fulfillment because of duplicate payment, payment conflict, or refund/dispute handling. Wait for the associated order to reach INVOICED or its Checkout Session to report COMPLETED, then apply the merchant purchase checks above. Keep platform completion and your own delivery status separate and reconcile any hold instead of taking another payment.

waitForConfirmation() also returns at PAYMENT_CONFIRMED; it is not a helper that waits through such a hold until the order is invoiced. A signed fiat.payment.succeeded notification alone does not authorize delivery either. See the two status contracts and Webhook event meanings.

Webhooks can be used with Checkout, QuickPay, or API Payments whenever the merchant account and environment expose the corresponding event. They are not limited to one integration path. Make fulfillment idempotent because polling and webhook delivery can observe the same completion.

Security and Launch Checklist

  • Keep merchant credentials on a trusted server.
  • Bind price and product data to a server-authoritative record.
  • Validate the connected wallet, chain, token, recipient, amount, and expiry.
  • Fulfill only from verified backend status or an authenticated webhook.
  • Reconcile a lost response or known transaction before retrying payment.
  • Test failure, cancellation, expiration, late payment, and duplicate delivery.

Continue with Merchant Portal, SDK Reference, or API Reference.

On this page