GOAT Network
GOAT Flow

API Payments

API Payments lets an application or agent pay for an API operation. Choose a Custom Integration when your server owns the order and delivery lifecycle, or MPP when a programmatic client must present a route-bound payment receipt on the protected request.

Choose an Integration

PathUse it whenCompletion signal
Custom IntegrationYour API owns authentication, pricing, order creation, and deliveryVerified order status or authenticated webhook
MPP IntegrationA client or agent must retry the protected request with proof of paymentVerified Payment-Receipt

Custom Integration

Request Flow

1. Client requests a paid API operation
2. Your backend authenticates the client and determines the price
3. Your backend creates a GOAT Flow order
4. Client receives HTTP 402 plus payment requirements
5. Client authorizes and submits the transfer
6. Your backend verifies status or receives a webhook
7. Your backend returns or provisions the result

Install the Server SDK:

npm install goatflow-sdk-server@0.4.0
goat-flow.ts
import { GoatFlowClient } from 'goatflow-sdk-server'

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

The example below is a Crypto payment for a public DIRECT merchant. Keep pricing and merchant credentials on the server. Validate the payer and select the chain, configured token contract, and amount from trusted server-side configuration; 1000000 is 1 USDC only for a configured six-decimal USDC token. The success predicate in this Crypto example is not a generic Card fulfillment rule: Card can be held at PAYMENT_CONFIRMED. Card integrations must use the platform completion gates rather than copying this Crypto-only condition.

Associate the Business Request and Payment

Use a stable application request ID for one report purchase. This is a key in your own database, not an idempotencyKey parameter accepted by the Server SDK. Expose the following application functions through your authenticated POST /api/reports and GET /api/reports/{reportId} handlers. Never accept customerId from an unauthenticated request body.

The ReportStore interface below is an application-owned persistence contract, not a GOAT Flow SDK API. Implement it with your own database:

  • reserveAndClaim() must atomically create or load one record per (customerId, requestId). Persist the input, price, payment terms, and a stable dappOrderId such as report:<reportId> before contacting GOAT Flow. Reject reuse of the request ID with different inputs or payment terms.
  • Only the first caller receives mayCreateOrder: true. Concurrent requests, process restarts, and timeouts must not automatically release this claim or grant it again. Existing requests return the saved payment requirements or a pending/reconciliation state instead of creating another order.
  • recordOrder() durably saves the raw response, including payment.order_id, against reportId. getOwned() must enforce customer ownership.
  • deliverOnce() must durably coordinate report generation and return the saved result on retries. Polling, webhooks, and concurrent requests must not generate or deliver the same purchase more than once.
report-store.ts
import type { CreateOrderParams, X402PaymentRequired } from 'goatflow-sdk-server'

export type PaymentTerms = Pick<
  CreateOrderParams,
  'chainId' | 'tokenSymbol' | 'tokenContract' | 'amountWei'
>

export type ReportRequest = { requestId: string; payer: string; topic: string }

export type ReportJob = {
  reportId: string
  orderParams: CreateOrderParams
  payment?: X402PaymentRequired
}

export interface ReportStore {
  reserveAndClaim(input: ReportRequest & {
    customerId: string
    terms: PaymentTerms
  }): Promise<{ job: ReportJob; mayCreateOrder: boolean }>
  recordOrder(reportId: string, payment: X402PaymentRequired): Promise<void>
  markCreationUnknown(reportId: string): Promise<void>
  getOwned(customerId: string, reportId: string): Promise<ReportJob>
  deliverOnce(reportId: string): Promise<{ downloadUrl: string }>
}

The following functions import the Server SDK setup above. The repository must construct job.orderParams from the persisted server-approved terms and validated payer, not from arbitrary fields supplied by the client.

report-payments.ts
import { goatFlow } from './goat-flow.js'
import type { PaymentTerms, ReportRequest, ReportStore } from './report-store.js'

export async function requestReport(
  reports: ReportStore,
  customerId: string,
  input: ReportRequest,
  terms: PaymentTerms,
) {
  const { job, mayCreateOrder } = await reports.reserveAndClaim({
    ...input,
    customerId,
    terms,
  })

  if (job.payment) {
    const current = await goatFlow.getOrderStatus(job.payment.order_id)
    const expiresAt = job.payment.extensions?.goatx402?.expiresAt
    if (
      current.status !== 'CHECKOUT_VERIFIED' ||
      !expiresAt || expiresAt <= Date.now() / 1000
    ) {
      return { httpStatus: 202, reportId: job.reportId, paymentStatus: current.status }
    }
    return { httpStatus: 402, reportId: job.reportId, payment: job.payment }
  }
  if (!mayCreateOrder) {
    return { httpStatus: 202, reportId: job.reportId, status: 'PAYMENT_SETUP_PENDING' }
  }

  try {
    const payment = await goatFlow.createOrderRaw(job.orderParams)
    await reports.recordOrder(job.reportId, payment)
    return { httpStatus: 402, reportId: job.reportId, payment }
  } catch {
    // A timeout or failed local save does not prove remote creation failed.
    await reports.markCreationUnknown(job.reportId)
    return { httpStatus: 202, reportId: job.reportId, status: 'RECONCILIATION_REQUIRED' }
  }
}

export async function getPaidReport(
  reports: ReportStore,
  customerId: string,
  reportId: string,
) {
  const job = await reports.getOwned(customerId, reportId)
  if (!job.payment) {
    return { httpStatus: 202, reportId, status: 'PAYMENT_SETUP_PENDING' }
  }

  // createOrderRaw() returns order_id, not the normalized orderId field.
  const orderId = job.payment.order_id
  const status = await goatFlow.waitForConfirmation(orderId, {
    timeout: 120_000,
    interval: 2_000,
  })

  if (status.status !== 'PAYMENT_CONFIRMED' && status.status !== 'INVOICED') {
    return { httpStatus: 409, reportId, paymentStatus: status.status }
  }

  const result = await reports.deliverOnce(reportId)
  return { httpStatus: 200, reportId, result }
}

Your HTTP handlers translate httpStatus into the response status and return the remaining fields as the body. The PAYMENT_SETUP_PENDING and RECONCILIATION_REQUIRED values above are your application's states, not GOAT Flow order statuses. Both prevent a second create attempt until the original outcome is known. If even the local error-state write fails, the original durable creation claim must still block another attempt.

Persist this association:

authenticated customer + requestId
  -> reportId + saved request, price, payer, and dappOrderId
  -> payment.order_id + saved payment requirements
  -> backend status/proof lookup or authenticated webhook
  -> one persisted report result

waitForConfirmation() resolves on PAYMENT_CONFIRMED, INVOICED, FAILED, EXPIRED, or CANCELLED; it does not throw merely because the returned state is a failure. The explicit success check above is required before delivery. Failure terminal states must not be treated as a still-pending payment or a reason to charge again automatically. The example's 409 is your merchant API's chosen response for that outcome, not an exception or HTTP status raised by the polling helper.

The helper throws on its overall timeout or a non-retryable request error, such as 401 or 404. Handle those exceptions in your HTTP error handler or background worker: keep the saved mapping, inspect the current order status, and do not issue another payment merely because polling stopped. For long-running work, return the report ID and use a worker or authenticated webhook to invoke the same durable, idempotent fulfillment operation. See SDK polling behavior.

Idempotency and Recovery

  • Reuse the same application request ID for a retry of one logical purchase; reject changes to its saved inputs. Use a new ID only for a genuinely new purchase, not as a way around an unresolved payment.
  • Server SDK 0.4.0 does not accept or send idempotencyKey on createOrder() or createOrderRaw(). Ordinary duplicate dappOrderId requests are rejected as order already exists; they do not return the existing order. Do not use duplicate creation as an order lookup.
  • If the GOAT Flow order ID is known, reuse the saved requirements while the order remains payable and query that order's status/proof. Do not replay expired requirements or automatically send another transfer.
  • If creation timed out, its response was lost, or saving the returned ID failed, keep the business record blocked for reconciliation. Use the saved merchant, dappOrderId, payer, chain, token, amount, and attempt time to locate the original order in the Merchant Portal or with platform support. Do not assume there is a public lookup-by-dappOrderId SDK method.
  • Save any broadcast transaction hash before verification/polling. A timeout, duplicate-order error, or expired payment window is not proof that no funds moved. Reconcile the original order and transaction before authorizing a replacement payment.
  • Fulfillment must be idempotent: polling and authenticated webhook delivery can race, and a response can be lost after delivery has completed.

MPP Integration

This section describes the GOAT Flow MPP profile for payment-gated HTTP routes. The /mpp/v1/challenge and /mpp/v1/verify endpoints, JSON fields, and three-part Payment-Receipt format below are GOAT Flow's concrete interface contract. Do not assume an arbitrary MPP SDK or receipt verifier is directly compatible; use a deployment-matched GOAT Flow client and verifier.

GOAT Flow MPP Crypto Payment Flow

This example shows the successful Crypto prepayment path. A client requests a challenge from GOAT Flow, sends the quoted on-chain payment to the configured merchant recipient, and submits the transaction for verification. After GOAT Flow verifies the payment and issues a signed Payment-Receipt, the client requests the protected resource with that receipt. The merchant API verifies the receipt and applies its own access checks before returning the resource.

Client / Agent               GOAT Flow Core              Blockchain               Merchant API
      |                            |                          |                          |
      |-- POST /mpp/v1/challenge ->|                          |                          |
      |<-- 402 + payment terms ----|                          |                          |
      |-- Transfer to merchant ------------------------------>|                          |
      |-- POST /mpp/v1/verify ---->|                          |                          |
      |   (challenge + tx hash)    |-- Verify payment ------->|                          |
      |                            |<-- Confirmed payment ----|                          |
      |<-- Signed Payment-Receipt -|                          |                          |
      |-- GET /api/data + Payment-Receipt ---------------------------------------------->|
      |                            |                          |                          | Verify receipt
      |                            |                          |                          | Apply access checks
      |<-- Protected resource (only after successful checks) ----------------------------|

The full verification request includes challenge_id, payer_addr, tx_hash, and mac; see the Buyer Flow. If verification returns 202 pending_inclusion or pending_finality, honor Retry-After and keep verifying the original challenge and transaction. Do not send another payment while waiting for confirmation; see Retry Rules.

Before You Integrate

Adding middleware to your API does not register or enable a paid route in GOAT Flow. Before requesting a challenge:

  1. Confirm with the platform operator that MPP is enabled in your target deployment and that your active merchant account has MPP enabled.
  2. Register and enable the intended route_canonical through the Merchant Portal or the platform operator. Merchant owners can manage their routes after merchant-level MPP access has been enabled by an operator.
  3. Configure the route's supported chain, registered Token, receiving address, and amount. The chain/Token must be allowed by the deployment and the receiving address must belong to the merchant's configured addresses.
  4. Use the same merchant and route identifiers in route configuration, challenge requests, and middleware configuration. Obtain the deployment's trusted Ed25519 receipt-verification public key and prepare the shared receipt store before starting your API.

GET:api:data is a registered route identifier, not a literal URL. Pricing is route-scoped; use server-configured routes for distinct prices. Do not let clients choose an arbitrary price or receiving address in a challenge request.

When the MPP runtime is disabled, /mpp/v1/* is not mounted. When it is enabled but there is no eligible registered route, challenge creation returns 404 route_not_found. Middleware alone does not resolve either condition.

Obtain the TypeScript Middleware

Do not assume @goatnetwork/mpp-middleware is installable from public npm: the public registry did not expose it when this example was verified. The public source package is marked private, while the target mainnet source configures version 1.0.0 for public publishing; a publish configuration is not proof of an available release.

Obtain an approved, deployment-matched source archive or built package from the platform team. If building from the approved source tree, use its lockfile:

cd /path/to/approved-source/goatx402-mpp-middleware-ts
pnpm install --frozen-lockfile
pnpm build
npm pack

Install the resulting archive in your merchant API project, rather than assuming a registry installation succeeds:

npm install /path/to/goatnetwork-mpp-middleware-1.0.0.tgz
npm install express@4 ioredis@5
npm install --save-dev typescript@5.9 tsx@4 @types/node@22 @types/express@4

The following ESM TypeScript example targets that approved 1.0.0 middleware build with Express 4 and Node.js 22 or later. Save it as server.mts and run it with npx tsx server.mts after configuring the application variables below. These are settings for your merchant API, not additional GOAT Flow service endpoints:

Application settingValue to provide
MPP_MERCHANT_IDYour merchant identifier in the target deployment
MPP_PUBLIC_KEY_HEXThe trusted platform receipt-signing public key, encoded as 64 hexadecimal characters (32 decoded bytes)
REDIS_URLYour shared receipt-consumption Redis connection URL

Get the public key through the platform operator's trusted distribution channel, load it at startup, and coordinate rotation with the operator. Never accept a verification key from the buyer or substitute an API secret or merchant audience identifier. No automatic public-key discovery URL is assumed by this example.

Protect a TypeScript Route

server.mts
import express from 'express'
import { Redis } from 'ioredis'
import type { ReceiptIDStore } from '@goatnetwork/mpp-middleware'
import { expressMiddleware } from '@goatnetwork/mpp-middleware/express'

function requiredEnv(name: string): string {
  const value = process.env[name]
  if (!value) throw new Error(`Missing ${name}`)
  return value
}

const merchantId = requiredEnv('MPP_MERCHANT_ID')
const publicKeyHex = requiredEnv('MPP_PUBLIC_KEY_HEX')
if (!/^[0-9a-fA-F]{64}$/.test(publicKeyHex)) {
  throw new Error('MPP_PUBLIC_KEY_HEX must encode a 32-byte Ed25519 public key')
}
const platformPublicKey = new Uint8Array(Buffer.from(publicKeyHex, 'hex'))

const redis = new Redis(requiredEnv('REDIS_URL'), {
  lazyConnect: true,
  enableOfflineQueue: false,
  maxRetriesPerRequest: 1,
})
redis.on('error', () => console.error('Receipt store unavailable'))
await redis.connect()

const receiptStore: ReceiptIDStore = {
  async tryConsume(key, ttlMs) {
    const result = await redis.set(
      `goat-flow:mpp:${merchantId}:${key}`,
      '1',
      'PX',
      Math.max(1, Math.ceil(ttlMs)),
      'NX',
    )
    return { consumed: result === 'OK' }
  },
}

const app = express()

app.get(
  '/api/data',
  expressMiddleware({
    merchantId,
    routeCanonical: 'GET:api:data',
    algorithm: 'ed25519',
    ed25519Public: platformPublicKey,
    expectedLivemode: true,
    store: receiptStore,
  }),
  (req, res) => {
    res.json({ paid: true, receiptId: req.mppReceipt?.receipt_id })
  },
)

app.listen(3001)

This route is a verification demonstration, not an implementation of customer authentication or report delivery. Use the configured merchant ID in the buyer requests below in place of merchant_123. expectedLivemode: true rejects test-mode Card receipts; use false in your separate test deployment. It has no effect on Crypto-only receipts.

The Redis operation atomically consumes the middleware-provided deduplication key and preserves its TTL across all API instances. Do not replace that key with just a transaction hash or discard a lineage: prefix. A shared store is required for single-use authorization across replicas, and for receipts with a purchase lineage on either payment rail. Configure Redis persistence and retention so consumption records cannot silently disappear before expiry. In-memory stores are for local tests, not restart-safe production recovery.

This simple ReceiptIDStore consumes before your handler runs. A handler failure or lost response therefore does not automatically make the receipt reusable. Persist delivery results and provide an authenticated result-retrieval path, or implement the middleware's TwoPhaseReceiptIDStore contract with durable reserve/commit/release semantics. Do not delete a consumed key or ask the buyer to pay again merely because delivery could not be observed.

Buyer Flow

Request a challenge using the merchant and registered route from your setup:

POST /mpp/v1/challenge
Content-Type: application/json

{
  "merchant_id": "merchant_123",
  "route_canonical": "GET:api:data",
  "request_canonical": "GET:api:data",
  "payer_addr": "0xBuyer"
}

Persist the returned challenge ID, MAC, GOAT Flow order ID, quoted payment terms, and expiry. After submitting the exact transfer, save the transaction hash and verify it with the original challenge credential:

POST /mpp/v1/verify
Content-Type: application/json

{
  "challenge_id": "chal_...",
  "payer_addr": "0xBuyer",
  "tx_hash": "0xTransactionHash",
  "mac": "..."
}

A successful response returns a Payment-Receipt. Present that receipt on the protected request:

GET /api/data
Host: merchant.example
Payment-Receipt: <body>.<signature>.ed25519

Receipt Verification

Middleware verifies the receipt format and signature, merchant audience, route binding, expiry, the configured live/test mode for Card receipts, and receipt consumption when a store is configured. Lineage-bearing receipts require a store. Use Ed25519 for multi-tenant production deployments. A shared HMAC secret is appropriate only when all verifiers are within one trusted tenant.

Route matching is an exact match or a :-delimited prefix match against the configured routeCanonical. It is not an automatic comparison of the current HTTP request's user, URL, query parameters, or body with the paid operation. The middleware does not authenticate the current customer or prove that a submitted payer address belongs to that customer.

Keep application authentication and authorization in place. If parameters, customer identity, selected data, or price change what can be delivered, maintain a server-side purchase record and explicitly check the verified receipt against that record and the current request. A valid receipt for a route must not unlock every customer's data or every operation on that route. Perform authentication and request/ownership validation before the payment middleware, and check receipt-dependent business conditions before delivering protected work. The simple store above has already consumed the receipt by then. If consumption must depend on those later checks, integrate verification and reservation with your application's authorization flow rather than assuming the stock adapter will undo consumption. Even the two-phase framework adapter commits non-5xx responses, including business rejections such as 403.

Retry Rules

ResultAction
200 or already_processedUse the returned receipt for the same purchase; do not send another payment
202 pending_inclusion / pending_finalityHonor Retry-After; retry verification with the original challenge and transaction evidence, without another transfer
400 tx_rejectedInspect the submitted transaction and order requirements: the cause may be a revert, insufficient amount, wrong recipient/Token, or other mismatch. Reconcile before correcting the evidence or authorizing a replacement payment
401 challenge_expiredStop automatic payment. First reconcile the original challenge, order, and any broadcast transaction; expiry does not prove the purchase is unpaid
401 challenge_not_issued / challenge_receipt_expired / challenge_already_consumedPreserve the original evidence and reconcile payment and delivery; do not automatically start another charge
409 tx_already_claimedFind the order already associated with that transaction and reconcile it before taking another payment
429 / 503Honor Retry-After; preserve the challenge and transaction state and retry the applicable verification operation, not the transfer

Before the first transfer, an expired, unpaid challenge can be replaced. Once a transaction has been broadcast or its outcome is uncertain, do not run a new payment loop just to obtain a fresh challenge.

The backend has separate recovery windows: a pending challenge can remain verifiable during its configured grace period, and an already-successful challenge can return its cached receipt while that receipt remains valid. Retry the original credential within the applicable window. These windows are bounded: Crypto does not guarantee receipt recovery after the challenge has been evicted from its replay store. If verification can no longer recover a receipt but the payment may have succeeded, reconcile with the platform operator rather than paying again automatically.

A consumed receipt or a lost merchant response is a delivery-recovery case, not a new purchase. Recover the saved result or the application fulfillment record instead of treating receipt_already_consumed as a reason to pay again.

Trust Boundary

ComponentResponsibility
Your APIClient authentication, pricing, request validation, delivery, refunds, and support
GOAT FlowPayment requirements, order lifecycle, matching, status, webhooks, and receipt verification services
NetworkTransaction execution and finality
Client or agentReviewing the requirement and authorizing payment

Continue with SDK Reference or API Reference.

On this page