GOAT Network
GOAT FlowReference

API Reference

This reference summarizes the public contracts used by GOAT Flow integrations. Prefer the Server SDK for authenticated requests so signing, timestamps, nonces, and response normalization remain consistent.

Environments

Production and test are separate environments. They use separate merchant IDs, credentials, receiving wallets, links, orders, products, webhook secrets, and chain/token configuration.

.env.production
GOATX402_API_URL=https://flow-api.goat.network
GOATX402_API_KEY=your_api_key
GOATX402_API_SECRET=your_api_secret
GOATX402_MERCHANT_ID=your_merchant_id
.env.test
GOATX402_API_URL=https://flow-api.testnet3.goat.network
GOATX402_API_KEY=your_test_api_key
GOATX402_API_SECRET=your_test_api_secret
GOATX402_MERCHANT_ID=your_test_merchant_id
EnvironmentAPI URLMerchant Portal
Productionhttps://flow-api.goat.networkhttps://flow-merchant.goat.network
Testhttps://flow-api.testnet3.goat.networkhttps://flow-merchant.testnet3.goat.network

Do not infer supported chains, token contracts, decimals, limits, or payment methods from examples. Read them from the active environment and merchant configuration.

Authentication

Authenticated merchant requests under /api/v1 use HMAC-SHA256. This scheme signs parameters, not the raw JSON body; it is different from Webhook verification.

HeaderContract
X-API-KeyMerchant API key in the selected environment
X-TimestampDecimal Unix timestamp in seconds; the server permits at most 300 seconds of clock difference in either direction
X-NonceFresh random value for every request, including retries; use a UUID. Accepted length after trimming is 8–128 bytes
X-SignHMAC-SHA256 digest encoded as lowercase hexadecimal, without a sha256= prefix

Canonical Parameters

  1. Collect decoded query parameters and, for POST/PUT, the top-level JSON body fields. Body fields override same-named query fields; avoid duplicate names.
  2. Convert scalar values to strings. Send monetary amounts as decimal strings and numeric identifiers/expiries as integers. Omit absent optional fields rather than sending null.
  3. Add api_key, timestamp, and nonce using the exact corresponding header values; these overwrite same-named business parameters.
  4. Remove the sign field and entries whose value is the empty string "". Do not drop zero or false values.
  5. Sort parameter names in ASCII order and join as key=value&key=value. Do not URL-encode this final string or include the HTTP method/path.
  6. HMAC the UTF-8 bytes of that string with the merchant API secret and encode the result as lowercase hexadecimal.

Checkout arrays and objects must first be serialized into the documented JSON-string fields, such as line_items_json. Sign the resulting string value exactly as transmitted, not a nested object or a re-serialized variant. See the SDK-to-HTTP mapping.

The current legacy canonical format does not escape & or = inside values, so unrestricted scalar values can make its representation ambiguous. Do not invent a different escaping scheme in one client; coordinate any protocol change with the platform and avoid unreviewed signing inputs. Non-integer JSON numbers also have backend-specific formatting; use decimal strings for prices.

This server-side helper covers flat strings, booleans, and safe integers:

merchant-auth.ts
import { createHmac, randomUUID } from 'node:crypto'

export function signMerchantRequest(
  params: Record<string, string | number | boolean>,
  apiKey: string,
  apiSecret: string,
) {
  if (!apiKey || !apiSecret) throw new Error('Merchant API credentials are required')
  const timestamp = Math.floor(Date.now() / 1000).toString()
  const nonce = randomUUID()
  const fields: Record<string, string | number | boolean> = {
    ...params, api_key: apiKey, timestamp, nonce,
  }
  const canonical = Object.keys(fields)
    .filter(key => key !== 'sign' && fields[key] !== '')
    .sort()
    .map(key => {
      const value = fields[key]
      if (!['string', 'number', 'boolean'].includes(typeof value)) {
        throw new Error('Serialize complex fields and omit null/undefined before signing')
      }
      if (typeof value === 'number' && !Number.isSafeInteger(value)) {
        throw new Error('Use decimal strings instead of fractional or unsafe JSON numbers')
      }
      return `${key}=${value}`
    })
    .join('&')

  return {
    'X-API-Key': apiKey,
    'X-Timestamp': timestamp,
    'X-Nonce': nonce,
    'X-Sign': createHmac('sha256', apiSecret).update(canonical, 'utf8').digest('hex'),
  }
}

Sign the actual flattened HTTP field names, not SDK camelCase parameters. Generate fresh headers immediately before each authenticated attempt: a previous nonce may already have been consumed even if the handler returned an error. Reusing it returns 401 Replay request detected, not 409. Fresh authentication does not make creating another order/session safe after an unknown result; the operation's retry contract still applies. Keep credentials on the server and synchronize its clock.

Merchant Configuration

GET /merchants/{merchant_id}

Use merchant configuration to discover payment capabilities enabled in the selected environment. A token entry can include:

FieldMeaning
chain_idPayment chain identifier
token_symbolSupported token symbol
token_contractToken contract on that chain

If token decimals are absent, resolve them from reviewed local metadata or the token contract. Keep that data scoped to the selected environment.

Orders

Create an Order

POST /api/v1/orders

Common request fields:

FieldTypeRequiredNotes
dapp_order_idstringYesYour stable internal order identifier; ordinary duplicates are rejected, not replayed
chain_idnumberYesPayment chain selected from current configuration
token_symbolstringYesConfigured token symbol
token_contractstringEnvironment-dependentConfigured token contract
from_addressstringYesPayer address
amount_weistringYesInteger amount in the token's smallest unit
merchant_idstringEnvironment-dependentMerchant identifier

HTTP 402 Payment Required is the expected response when an order is created and payment is required. It is not an application failure. The response contains the order identifier and authoritative payment terms.

Server SDK 0.4.0 does not accept or send idempotencyKey on ordinary order creation. Repeating dapp_order_id returns an order already exists error, not the original order. Save the returned order ID and payment requirements against your business record. If creation has an unknown outcome, reconcile the original attempt before creating another payable order; see application request and order recovery.

Read Status

GET /api/v1/orders/{order_id}

Get Payment Record

GET /api/v1/orders/{order_id}/proof

Use the returned record for reconciliation. Verify its transaction independently on-chain when your trust model requires chain evidence. The historical signature field is an unsigned hash of selected payment fields, not a platform private-key signature or a signed MPP Payment-Receipt; see payment-record and receipt limits.

Cancel an Eligible Order

POST /api/v1/orders/{order_id}/cancel

Cancellation applies only to eligible Crypto orders still in CHECKOUT_VERIFIED. FIAT_STRIPE orders are rejected through this endpoint in every state and must use the Stripe-aware lifecycle. This is not a refund API and does not reverse a transfer already submitted on-chain. See SDK cancellation limits.

Checkout Sessions

Checkout Sessions bind server-authoritative price, line items, metadata, and redirect URLs to an opaque checkout identifier. Give the browser the returned ID, URL, and expiry, not merchant credentials.

Create a Checkout Session

POST /api/v1/checkout/sessions
Content-Type: application/json
X-API-Key: <merchant-api-key>
X-Timestamp: <current-unix-seconds>
X-Nonce: <new-uuid>
X-Sign: <lowercase-hmac-hex>

This endpoint requires merchant HMAC authentication. The merchant is derived from the API key, not a request-body merchant_id. Public integrations use checkout_type: "DIRECT".

SDK parameterHTTP JSON field and typeContract
checkoutTypecheckout_type: stringRequired; use "DIRECT"
paymentRailspayment_rails: string['crypto', 'fiat'] becomes "crypto,fiat", not a JSON array. Omission defaults to Crypto; explicitly select the intended rails
priceprice: decimal stringRequired when Crypto is offered; omit for a Card-only session
fiatCurrencyfiat_currency: stringRequired with fiat; use a supported three-letter currency code
fiatAmountfiat_amount: positive decimal stringRequired with fiat; independent of the Crypto quote, with no automatic FX conversion
clientReferenceIdclient_reference_id: stringOptional merchant correlation reference, trimmed and limited to 200 characters; same-merchant duplicates conflict
expiresInexpires_in: integer secondsRequested lifetime; use returned expires_at, including Card-specific effective expiry and initialization rules
lineItemsline_items_json: stringJSON-serialize the array once into this field
publicMetadatapublic_metadata_json: stringJSON-serialize an object; exposed publicly, so exclude secrets and private customer data. Reserved keys may be rejected
privateMetadataprivate_metadata_json: stringJSON-serialize an object; not part of the public session view
successUrl / cancelUrlsuccess_url / cancel_url: stringOptional; must match the merchant's preconfigured return-URL allowlist

Example JSON body offering both payment methods:

{
  "checkout_type": "DIRECT",
  "payment_rails": "crypto,fiat",
  "price": "19.95",
  "fiat_currency": "USD",
  "fiat_amount": "19.95",
  "client_reference_id": "cart_9f31",
  "expires_in": 3600,
  "line_items_json": "[{\"name\":\"Coffee mug\",\"quantity\":1,\"amount\":\"19.95\"}]"
}

The escaped inner JSON is a string value in the outer request. Sign that decoded string value as line_items_json; do not send line_items as a nested array. Use the Crypto prerequisites and Card prerequisites for each enabled method.

Success is HTTP 200, not the ordinary order-create 402 response:

{
  "checkout_id": "cs_<opaque-handle>",
  "checkout_type": "DIRECT",
  "url": "<platform-returned-hosted-url>",
  "expires_at": 1800003600
}

The response values above are illustrative. Persist the actual returned ID, URL, and Unix-second expiry against the purchase. A Checkout ID is not a payment order ID; the associated order is created during payment initiation.

Read a Checkout Session

GET /checkout/v1/sessions/{checkout_id}
GET /checkout/v1/sessions/{checkout_id}/status

These are public, handle-keyed endpoints, not merchant-HMAC endpoints. Treat the high-entropy Checkout ID as a bearer capability and keep it out of logs and unrelated customers' responses. Read it only for the purchase to which your application saved it.

The first endpoint returns the safe public terms and session view. The second returns a compact projection, for example:

{
  "status": "COMPLETED",
  "order_id": "<associated-goat-flow-order-id>",
  "order_status": "INVOICED"
}

status is the session status; order_status is the associated order status. order_id and order_status can be absent before an order is linked. tx_hash is optional and is not a Card settlement identifier. A status read can return HTTP 200 with a terminal session status; HTTP 410 instead describes a refused operation on a session, not every read of an expired session.

SDK Example

The following is a Crypto-only DIRECT session; first satisfy the Crypto session prerequisites.

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

An existing non-empty client_reference_id for the same merchant causes HTTP 409; repeating the create request does not recover the original session. Save and reuse an eligible session and follow the recovery rules after an unknown outcome.

Optional success_url and cancel_url must match the merchant's preconfigured redirect allowlist. Omit them when return navigation is not needed. For Card session parameters, include the fiat rail and a fiat quote, and follow the effective expiry and Card initialization rules.

Treat the session ID and URL as bearer capabilities. Do not include secrets or private customer data in public metadata.

Status Values

Order Status

GET /api/v1/orders/{order_id} returns an order's status under merchant HMAC authentication. Do not compare this value with Checkout COMPLETED.

Order statusMeaningFulfillment
CHECKOUT_VERIFIEDOrder created and awaiting paymentDo not fulfill
PAYMENT_CONFIRMEDPayment evidence is confirmed, but platform processing or fulfillment restrictions may remainNot a universal permission to fulfill; Card may be parked here for conflict/refund handling
INVOICEDPlatform order completionUse as the completed-order gate, then check purchase ownership, terms, current business eligibility, and idempotent delivery
FAILEDTerminal failureDo not fulfill
EXPIREDPayment window expiredReconcile before any retry
CANCELLEDOrder cancelled while eligibleDo not fulfill

For Card, a real charge can be booked while fulfillment is withheld because of a duplicate payment, payment conflict, refund, dispute, or related balance condition. Such orders can remain at PAYMENT_CONFIRMED; payment confirmation and payment-level notifications do not override that restriction. Do not try to bypass a platform hold or treat it as a reason to collect another payment.

Use the flow's completed-order/session state and applicable authenticated fulfillment event, together with your own purchase checks. A completed order does not guarantee that a later refund or dispute can never occur.

A poller may observe INVOICED without first observing PAYMENT_CONFIRMED. Design state handling for valid transitions rather than assuming every intermediate state is visible.

Checkout Session Status

GET /checkout/v1/sessions/{checkout_id}/status returns these values in status, separately from its optional order_status:

Session statusMeaningApplication action
OPENSession is open; Card may also retain this value while its associated payment is processing or heldNot proof of being unpaid; inspect the associated order and existing attempt before offering another payment
BOUNDA payer/payment order has been linked in the Crypto flowNot completion; query the same purchase and order
COMPLETEDAssociated order has reached INVOICEDApply merchant purchase checks and deliver idempotently
EXPIREDSession/payment window is expiredDo not start a new payment on this session; reconcile any already-submitted payment
CANCELLEDSession is cancelledDo not start another payment on it; cancellation is not a refund
FAILEDAssociated order failedDo not fulfill; reconcile the original attempt

The wire format also retains a compatibility-only SIGNED value. It is not a public DIRECT integration step and does not indicate completion. Do not add another signing flow merely because this value exists.

Card does not need to pass through BOUND. COMPLETED is derived from INVOICED, not simply from PAYMENT_CONFIRMED; a held Card payment must not be fulfilled by treating the two state machines as interchangeable. QuickPay's separate Crypto session API has its own state contract; see QuickPay payment status.

QuickPay Payment Status

Crypto and Card use different session types and status values:

Payment methodSession identifier and confirmed/completed stateAssociated payment order
CryptoQuickPay Crypto session_id: PAYMENT_CONFIRMEDOrder PAYMENT_CONFIRMED or INVOICED maps to the confirmed Crypto session state
CardCheckout checkout_id: COMPLETEDCheckout completion is reported when the order reaches INVOICED; payment confirmation and session completion are separate states

For Card, your backend can read GET /checkout/v1/sessions/{checkout_id}/status to obtain status, order_id, and order_status when bound. Do not compare Card session status only with PAYMENT_CONFIRMED, or compare an order status with COMPLETED.

Apply the order completion rules and payment and fulfillment event rules to Card; payment confirmation alone does not authorize fulfillment.

Fulfill from a verified backend status or an authenticated webhook available for that payment method, not from hosted-page copy, CLI output, a link-creation response, or a frontend callback alone. Bind the session and order to your internal purchase, and verify the merchant, product or reference, amount, and currency. For Crypto, also verify the chain and Token.

For an any-amount link, confirmation proves the amount paid; it does not prove that the amount matches a product or invoice. Reconcile the amount and your own reference before fulfillment. Use a fixed-price product or server-created Checkout Session when the price must be authoritative.

Webhooks

Webhooks provide event-driven updates for integrations that enable them. They can support Checkout, QuickPay, and API Payments; they are not restricted to a single solution.

Delivery Contract

The platform POSTs a JSON envelope to the configured merchant Webhook URL:

Content-Type: application/json
X-Webhook-Signature: sha256=<lowercase-hmac-hex>
X-Webhook-Event: <event-name>
{
  "event": "order.invoiced",
  "payload": {
    "order_id": "<goat-flow-order-id>"
  },
  "created_at": "2026-10-17T00:00:00Z"
}

This is a minimal envelope illustration, not a complete order.invoiced payload schema. payload varies by event and payment method. created_at is a UTC RFC3339 envelope timestamp generated for delivery, not a universal payment timestamp or stable event ID; a retry can have a different value.

Webhook Signature

Use the secret configured for that Webhook endpoint, not the merchant API secret. Calculate HMAC-SHA256 over the exact raw request body bytes, encode the digest as lowercase hexadecimal, and prefix it with sha256=. JSON parsing and re-serialization can change bytes and must not happen before verification.

webhook-signature.ts
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyWebhookSignature(
  rawBody: Uint8Array,
  signatureHeader: string | undefined,
  webhookSecret: string,
): boolean {
  if (!webhookSecret || !signatureHeader || !/^sha256=[0-9a-f]{64}$/.test(signatureHeader)) {
    return false
  }
  const supplied = Buffer.from(signatureHeader.slice(7), 'hex')
  const expected = createHmac('sha256', webhookSecret).update(rawBody).digest()
  return timingSafeEqual(supplied, expected)
}

In Express, capture this route with express.raw({ type: 'application/json' }) before a global express.json() parser. Verify that Buffer first, then parse its JSON. Dispatch using the verified body's event; do not trust the convenience X-Webhook-Event header alone. The merchant-API timestamp/nonce scheme is not the Webhook protocol.

Payment Events and Fulfillment Events

Subscribe only to events enabled for your merchant and deployment. These implementation-defined event names have different meanings:

EventMeaning and delivery rule
fiat.payment.succeededA Card payment-level notification; can still be sent while fulfillment is withheld. Record/reconcile it, but do not ship merely because this event or its signature is valid
order.invoicedOrder-completion notification. Resolve it to the saved purchase and verify current business eligibility before idempotent delivery
quickpay.checkout.completedCheckout-completion notification, not a generic payment-success alias. Match it to the saved Checkout Session and associated order before delivery

When a Card payment is held, the platform suppresses order.invoiced and Checkout completion while still reporting the payment-level event. Do not replace that distinction with a generic "any payment success means fulfill" handler.

Validation and Recovery

  1. Verify the raw-body signature with the endpoint's secret and reject invalid deliveries. Enforce your receiver's freshness policy without assuming the merchant API's five-minute authentication rule is a Webhook delivery limit.
  2. Read fields according to the event schema. A payload is not guaranteed to include merchant, amount, Token, and transaction hash on every event.
  3. Resolve available identifiers to the purchase saved by your backend. Query the authoritative order/payment state when the notification lacks facts needed for authorization or reconciliation; do not silently skip checks.
  4. Validate terms appropriate to the rail: Crypto chain/Token/amount and any observed transfer, or Card currency/amount and provider payment reference. Card notifications do not have to contain a blockchain transaction hash.
  5. Process atomically and idempotently using a stable business-operation key. The envelope's event is a name, not a unique delivery ID. Do not use created_at as a stable deduplication key or collapse all event types for an order into one already-processed flag.
  6. Return a 2xx response only after durable acceptance. Handle retries and out-of-order notifications without regressing state or delivering twice.

Polling remains a valid recovery path. Polling and webhook delivery can race, so both must converge on the same idempotent fulfillment operation.

Error Semantics

HTTP statusMeaning
200Request succeeded
202MPP verification is pending_inclusion or pending_finality; not completion. Honor Retry-After and retry verification of the original payment, not the transfer
400Validation or business error
401Invalid/expired merchant authentication, reused nonce (Replay request detected), or an MPP credential/challenge error; inspect the endpoint-specific body
402Payment is required; expected in create and challenge flows
403Authenticated caller is not authorized
404Resource not found
409Resource/business conflict: duplicate Checkout reference, payer/rail conflict, or an already-claimed MPP transaction. Merchant authentication nonce replay is 401, not 409
410A Checkout operation cannot proceed, for example an expired/cancelled/already-completed session or too little time to start Card payment; a status read may instead return 200 with a terminal state
429Rate limited; honor Retry-After when present
500 / 503Service error or temporary unavailability

An HTTP status alone is not a payment or fulfillment result. In particular, 202 does not authorize delivery, and 410 does not prove that no funds moved. Preserve the original purchase, session/order IDs, and payment evidence for reconciliation.

Retry only operations documented as safe. An application request ID is not a universal platform idempotency parameter: ordinary orders reject duplicate dapp_order_id, and Checkout Sessions reject duplicate non-empty client_reference_id. Neither create API recovers the original response on an ordinary retry. Preserve the original request and any known IDs or transaction hashes, then reconcile an unknown create result before issuing another payable order/session or initiating another payment. Follow the MPP recovery rules for challenge and receipt errors; an expired challenge is not proof of non-payment.

Public Integration Rules

  • Use generated QuickPay and Checkout URLs; do not guess a service origin.
  • PayKit SDKs may expose QuickPay* class and component names because QuickPay is the solution being integrated. Keep those code identifiers unchanged.
  • Keep merchant credentials and signing secrets off the browser and client apps.
  • Treat browser callbacks and redirects as interface signals only.
  • Fulfill from verified status, authenticated webhooks, or a verified receipt.
  • Keep production and test credentials and resources strictly separate.

See SDK Reference, Checkout, and API Payments.

On this page