coal
COAL DOCS

Webhooks

Webhooks allow your application to receive real-time notifications about events in your Coal account. Instead of polling our API, Coal pushes data to your server as soon as it happens.

[!IMPORTANT] Webhooks are the most reliable way to handle post-payment logic, such as fulfilling orders or updating user balances.

The Webhook Event

Coal currently supports the checkout.session.completed event, which is fired when a payment is successfully confirmed on the blockchain.

Payload Structure

When an event occurs, Coal sends a POST request to your configured callbackUrl. The body of the request will contain the event object.

json
1{
2 "event": "checkout.session.completed",
3 "data": {
4 "id": "cs_cl9...",
5 "amount": "50.00",
6 "currency": "MNEE",
7 "status": "confirmed",
8 "customer_details": {
9 "email": "customer@example.com"
10 }
11 }
12}

Security & Verification

To ensure that webhook requests are genuinely from Coal and haven't been tampered with, every request includes a Coal-Signature header.

The header format is: t=TIMESTAMP,v1=SIGNATURE

Verifying Signatures

You should compute the HMAC-SHA256 of the payload and compare it to the signature provided in the header.

  1. Extract the timestamp and signature from the header.
  2. Prepare the signed_payload string: concatenate the timestamp, a period (.), and the raw JSON body.
  3. Compute the HMAC using your API Secret Key.
  4. Compare your computed signature with the one in the header.

Example Code (Next.js / Node.js)

Here is a robust example of how to handle verify Coal webhooks in a Next.js App Router API route.

typescript
1// app/api/webhook/route.ts
2import { NextResponse } from 'next/server';
3import crypto from 'crypto';
4
5export async function POST(request: Request) {
6 const rawBody = await request.text();
7 const signatureHeader = request.headers.get('Coal-Signature');
8
9 if (!signatureHeader) {
10 return NextResponse.json({ error: "Missing signature" }, { status: 400 });
11 }
12
13 // 1. Extract pieces
14 const [t, v1] = signatureHeader.split(',').map(part => part.split('=')[1]);
15
16 // 2. Prepare payload
17 const signedPayload = `${t}.${rawBody}`;
18
19 // 3. Compute HMAC
20 const hmac = crypto.createHmac('sha256', process.env.COAL_SECRET_KEY!);
21 const digest = hmac.update(signedPayload).digest('hex');
22
23 // 4. Compare (Timing Safe)
24 if (!crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(v1))) {
25 return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
26 }
27
28 // Process Event
29 const event = JSON.parse(rawBody);
30
31 if (event.event === 'checkout.session.completed') {
32 const session = event.data;
33 console.log(`💰 Payment confirmed: ${session.amount} ${session.currency}`);
34 // TODO: Fulfill order
35 }
36
37 return NextResponse.json({ received: true });
38}

[!TIP] Always return a 200 OK response quickly to acknowledge receipt. If you have complex processing, consider using a background queue.