coal
coal

Authorize & Capture

Legacy / internal reference only.

Authorize & Capture is a two-phase payment model that separates the authorization (reserving funds) from the capture (collecting them). This is useful for businesses that need to verify an order before charging the customer.

Note: Coal's live public merchant API focuses on direct-settlement checkout sessions. This page is preserved as legacy/internal reference for the authorize/capture workflow and should not be treated as the primary public merchant integration.

When to Use It

Use CaseExample
Pre-ordersReserve funds when order placed, capture on ship date
Hotel holdsAuth on check-in, capture on check-out
Metered billingAuth a limit, capture actual usage
Fraud reviewHold funds while you review the order

For most use cases - simple product sales, digital downloads, subscriptions - the default direct-settlement mode is sufficient. Use authorize_capture only when you need time between reserving and collecting funds in a legacy or internal workflow.

How It Works

Authorization
Your AppLegacy/internal system creates authorization-capable session
User / BrowserCustomer completes authorization
Coalstatus: "authorized", authId set
Merchant Review
Your AppMerchant reviews order
Capture or Void
Your AppCoal
POST /api/console/payments/capture

{ sessionId }

Coalstatus: "confirmed" ✓
Your AppCoal
POST /api/console/payments/void

{ sessionId }

Coalstatus: "voided" — authorization released

Authorizations expire according to the session's authExpiresAt value. Check that timestamp before attempting capture or void.

Status Transitions

Normal Path
Coalpending → authorized → confirmed (captured)
Release Path
Coalauthorized → voided (authorization released)
Failure Paths
Coalpending → failed (authorization declined)
Coalpending → expired (session timed out before auth)

Creating an Authorize & Capture Session

This workflow is documented for legacy/internal systems only. Coal's current public hosted checkout flow is direct-settlement first, and the public POST /api/checkout/init route does not expose authorize_capture as a new merchant integration surface.

If you are maintaining an older internal integration, keep the authorization logic on the server and use the capture/void routes below once an authorization already exists.

Capturing a Payment

Once authorized, capture the payment from your server:

POST/api/console/payments/capture
bash
1curl -X POST https://api.usecoal.xyz/api/console/payments/capture \
2 -H "Authorization: Bearer <Privy JWT>" \
3 -H "Content-Type: application/json" \
4 -d '{ "sessionId": "clxxx" }'
json
1{
2 "data": {
3 "sessionId": "clxxx",
4 "txHash": "0xabc123..."
5 }
6}

Voiding an Authorization

To release the hold without capturing:

POST/api/console/payments/void
bash
1curl -X POST https://api.usecoal.xyz/api/console/payments/void \
2 -H "Authorization: Bearer <Privy JWT>"
json
1{
2 "data": {
3 "sessionId": "clxxx",
4 "status": "voided"
5 }
6}

Webhook Events

EventFired when
checkout.authorizedCustomer successfully authorizes
checkout.confirmedCapture completes
checkout.voidedAuthorization released
checkout.failedAuthorization or capture fails

Node.js Integration Example

typescript
1import { NextRequest, NextResponse } from 'next/server';
2
3// Route: POST /api/orders/:orderId/fulfill
4export async function POST(req: NextRequest, { params }: { params: { orderId: string } }) {
5 const order = await db.orders.findById(params.orderId);
6
7 if (!order || order.status !== 'pending_review') {
8 return NextResponse.json({ error: 'Order not ready for fulfillment' }, { status: 400 });
9 }
10
11 // Capture the authorized payment
12 const res = await fetch(`${process.env.COAL_API_URL}/api/console/payments/capture`, {
13 method: 'POST',
14 headers: {
15 'Authorization': 'Bearer <Privy JWT>',
16 'Content-Type': 'application/json',
17 },
18 body: JSON.stringify({ sessionId: order.coalSessionId }),
19 });
20
21 if (!res.ok) {
22 const err = await res.json();
23 return NextResponse.json({ error: err.error?.message }, { status: 502 });
24 }
25
26 await db.orders.update(params.orderId, { status: 'paid' });
27 await fulfillOrder(order);
28
29 return NextResponse.json({ success: true });
30}