Payment Flow
This page provides a walkthrough of how a Coal payment works end-to-end - from session creation to on-chain confirmation and webhook delivery. Understanding this flow will help you build reliable integrations and debug issues when they arise.
High-Level Flow Diagram
{ amount, productName, redirectUrl } with x-api-key header
User visits the hosted checkout URL
Shows product, amount, wallet connect
MetaMask, Coinbase Wallet, WalletConnect, or Privy embedded
Signed directly by the user — Coal never holds funds
{ sessionId, txHash }
Signed with HMAC-SHA256 — verify the Coal-Signature header
Frontend polls until status === "confirmed"
Step-by-Step Breakdown
1. Session Creation
Your server, or the Coal hosted checkout page, calls POST /api/checkout/init with a payment link slug. Coal looks up the payment link, resolves the product and price, and creates a CheckoutSession in the database with status pending.
What happens:
- Coal validates the slug and checks the link is
active - For product-linked links: price is read from the
Productrecord - For flexible (donation) links: you must supply an
amountin the request body - A session is created with a 24-hour expiry
- The session ID, amount, currency, merchant name, payout address, and expiry are returned
Request:
1POST /api/checkout/init2{3 "slug": "premium-membership"4}
Response:
1{2 "data": {3 "sessionId": "cm9x4k2j00003lb08n5qz7v1r",4 "amount": 25,5 "currency": "USDC",6 "description": "Premium Membership",7 "merchant": {8 "name": "Acme Corp",9 "payoutAddress": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"10 },11 "expiresAt": "2026-03-24T14:00:00.000Z"12 }13}
2. Checkout Page Render
When a user visits usecoal.xyz/pay/:slug, the frontend fetches the session and displays the hosted payment page. This page shows:
- Merchant name and logo
- Product name and description
- Exact amount due
- A wallet connect button
No authentication is required from the user at this point.
3. Wallet Connection
The user connects their EVM wallet (MetaMask, Coinbase Wallet, WalletConnect, or a Privy embedded wallet). Coal's frontend reads the connected address and prepares the ERC-20 transfer parameters:
- Token contract: Configured settlement token on Base (
SETTLEMENT_TOKEN_ADDRESS) - Recipient: the merchant's
payoutAddress - Amount:
session.amountconverted to raw token units using the configured token decimals
4. On-Chain Transfer
The user reviews and signs the settlement-token ERC-20 transfer in their wallet. This is a standard transfer(address to, uint256 amount) call on the configured token contract. The transaction is broadcast to the Base network.
Coal does not hold funds at any point. The transfer goes directly from the user's wallet to the merchant's configured payout address. Coal's role is to verify the transfer happened correctly.
The transaction is confirmed on Base in approximately 1–2 seconds.
5. Transaction Hash Submission
Once the transfer is sent, the frontend captures the transaction hash and POSTs it to Coal:
Request:
1POST /api/pay/confirm2{3 "sessionId": "cm9x4k2j00003lb08n5qz7v1r",4 "txHash": "0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b"5}
Coal performs these checks synchronously:
- The session exists and is in
pendingstatus - The session has not expired
- The
txHashhas not already been used in a confirmed transaction - The
txHashis not already submitted to a different session
If all checks pass, the session transitions to verifying and the pendingTxHash is recorded.
Response:
1{2 "data": {3 "status": "verifying",4 "sessionId": "cm9x4k2j00003lb08n5qz7v1r"5 }6}
Note on idempotency: If you re-submit the same sessionId + txHash combination (e.g., due to a network retry), Coal returns the current session status without creating a duplicate. This makes the endpoint safe to call more than once.
6. Asynchronous On-Chain Verification
Coal runs a background cron job approximately every minute that processes all sessions in verifying status. The job:
- Fetches the transaction receipt from the Base RPC node using
getTransactionReceipt(txHash) - If the receipt is not yet available (transaction not mined), the session is left in
verifyingand retried on the next cron run - If the transaction reverted, the session is marked
failed - Decodes the ERC-20
Transferevent from the receipt logs - Validates the recipient address matches the merchant's
payoutAddress(case-insensitive) - Validates the transfer amount matches the session amount (using raw BigInt arithmetic - no floating point)
- If all validations pass, the session transitions to
confirmedand aTransactionrecord is created
Timing: Under normal conditions, confirmation happens within 60–90 seconds of the on-chain transfer. In practice, most payments confirm in under 30 seconds since the cron runs frequently and Base block times are ~2 seconds.
7. Status Polling
Your frontend (or server) can poll GET /api/pay/status/:sessionId to monitor the session:
1GET /api/pay/status/cm9x4k2j00003lb08n5qz7v1r
1{2 "status": "confirmed",3 "txHash": "0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b",4 "redirectUrl": "https://yoursite.com/success"5}
The redirectUrl field is only returned when status === "confirmed". Use this to redirect the user.
8. Webhook Delivery
When a session is confirmed, Coal fires a checkout.session.completed webhook to the callbackUrl set on the session (or the merchant's default webhookUrl if no per-session URL was provided).
Webhook payload:
1{2 "event": "checkout.session.completed",3 "data": {4 "id": "cm9x4k2j00003lb08n5qz7v1r",5 "amount": "25.000000",6 "currency": "USDC",7 "status": "confirmed",8 "txHash": "0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b",9 "explorerUrl": "https://basescan.org/tx/0x3a4b5c6d..."10 }11}
Webhooks are signed with HMAC-SHA256. The Coal-Signature header has the format t=TIMESTAMP,v1=SIGNATURE. See Webhooks for full verification instructions.
9. User Redirect
After confirmation is detected (either via polling or webhook), the user is redirected to your redirectUrl with the session ID appended as a query parameter:
1https://yoursite.com/success?session_id=cm9x4k2j00003lb08n5qz7v1r
Use the session_id to look up the order on your side if needed.
Session Status State Machine
1┌──────────────────────────────┐2 │ │3 ┌────────────▼──────────┐ │ expiresAt exceeded4 │ pending │──────────────────▶│──────────────┐5 └───────────────────────┘ │ │6 │ │ ▼7 POST /api/pay/confirm │ ┌─────────┐8 │ │ │ expired │9 ▼ │ └─────────┘10 ┌──────────────────────┐ │11 │ verifying │───────────────────┘12 └──────────────────────┘13 │ │14 cron: tx OK cron: tx failed / amount15 recipient OK mismatch / reverted16 │ │17 ▼ ▼18 ┌──────────┐ ┌────────┐19 │confirmed │ │ failed │20 └──────────┘ └────────┘
| Status | Description |
|---|---|
pending | Session created, waiting for the user to send the transaction |
verifying | Transaction hash submitted; background job is checking the chain |
confirmed | Transaction verified on-chain; payment complete |
failed | On-chain check failed (see failure scenarios below) |
expired | Session passed its expiresAt deadline before confirmation |
Timing Reference
| Event | Typical Duration |
|---|---|
| Base block time | ~2 seconds |
| Transaction receipt available | 2–6 seconds after broadcast |
| Cron job interval | ~60 seconds |
| End-to-end confirmation (typical) | 30–90 seconds |
| Session expiry (default) | 24 hours |
Failure Scenarios
Transaction Reverted
The EVM transaction was included in a block but the execution failed (status 0x0 in the receipt). This can happen if the user had insufficient balance or allowance for the configured settlement token at the time of execution.
Result: Session moves to failed.
Recovery: The session cannot be reused. The user must start a new checkout session and attempt the payment again.
Amount Mismatch
The on-chain Transfer event shows a different token amount than what was recorded on the session. Coal compares amounts using raw BigInt arithmetic after normalizing to the configured token decimals - so even a 1-unit discrepancy can cause a failure.
Result: Session moves to failed.
Common cause: The user edited the transfer amount in their wallet, or fees were deducted from the transfer amount.
Recipient Mismatch
The to field in the decoded Transfer event does not match the merchant's configured payoutAddress. The comparison is case-insensitive.
Result: Session moves to failed.
Common cause: Merchant changed their payout address between session creation and payment, or the user sent to a different address.
Transaction Not Mined (Timeout)
The transaction hash was submitted but the transaction has not been mined within the session expiry window. This can happen if the user submitted a transaction with a very low gas price, causing it to be stuck in the mempool.
Result: On each cron run, Coal calls getTransactionReceipt(). If the receipt is unavailable, the session stays in verifying and is retried on the next run. If the session's expiresAt is reached before the transaction mines, the session transitions to expired.
No Transfer Event Found
The transaction was mined and succeeded, but the receipt logs do not contain an ERC-20 Transfer event from the configured settlement token contract address. This indicates the user sent a different token, called a different contract function, or the token address is misconfigured.
Result: Session moves to failed.
Session Expiry
If a session reaches its expiresAt timestamp (24 hours after creation) without being confirmed, it transitions to expired. This is checked both by the status endpoint (on every poll) and by the cron job (on every run).
Result: Session moves to expired. A new session must be created if the user still wants to pay.
Webhook Retry Policy
If Coal cannot reach your webhook endpoint (connection timeout, non-2xx response), it retries using exponential backoff:
| Attempt | Delay |
|---|---|
| 1st | Immediate |
| 2nd | 1 minute |
| 3rd | 5 minutes |
| 4th | 30 minutes |
| 5th | 2 hours |
After 5 failed attempts, the webhook event is marked exhausted and no further retries occur. You can view webhook delivery history in Console → Webhooks.
To avoid missed events, always respond with 200 OK as quickly as possible and process the payload asynchronously (e.g. via a queue).
Next Steps
- Webhooks — verify signatures and handle events
- Quickstart — end-to-end integration with code examples
- Authentication — API key management and security
