Handle Payment Result
Pay links fire the same webhook events as direct invoice integrations. The orderId you set when creating the pay link is included in every event — use it to match the payment back to your order.
Order status flow
Map Coinsnap webhook events to your order statuses:
| Your order status | When to set it |
|---|---|
pending_payment | Order created, pay link generated |
awaiting_bitcoin_payment | Customer opened the pay link (optional — requires polling) |
paid | Settled webhook received |
completed | Order fulfilled |
For most integrations, you only need two states: pending_payment and paid.
Webhook events
| Event | additionalStatus | Meaning | Action |
|---|---|---|---|
Settled | None | Exact amount paid | Mark as paid, fulfill |
Settled | Overpaid | Customer paid more than the amount | Mark as paid, fulfill; decide on the overpayment |
Settled | PaidAfterExpiration | Payment received after invoice expired | Review manually |
Expired | None | Invoice expired, no payment | No action — pay link is still open |
Expired | Underpaid | Invoice expired with partial payment | Contact customer |
Handling events
- Node.js
- PHP
import express from 'express';
import crypto from 'crypto';
const router = express.Router();
router.post('/coinsnap', express.raw({ type: 'application/json' }), async (req, res) => {
// 1. Verify signature
const sig = req.headers['x-coinsnap-sig'] ?? '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.COINSNAP_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const sigBuf = Buffer.from(sig.padEnd(expected.length));
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
return res.status(401).send('Unauthorized');
}
// 2. Parse event
const event = JSON.parse(req.body);
const orderId = event.metadata?.orderId;
// 3. Handle
switch (event.type) {
case 'Settled': {
// Idempotency guard
const order = await db.orders.findById(orderId);
if (!order || order.status === 'paid') break;
await db.orders.update({ id: orderId }, { status: 'paid', paidAt: new Date() });
await triggerFulfillment(orderId);
break;
}
case 'Expired':
// Pay link remains open — no action needed unless underpaid
if (event.additionalStatus === 'Underpaid') {
await notifyCustomerOfUnderpayment(orderId);
}
break;
}
res.status(200).send('OK');
});
export default router;
<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_COINSNAP_SIG'] ?? '';
$secret = getenv('COINSNAP_WEBHOOK_SECRET');
// 1. Verify signature
if (!hash_equals('sha256=' . hash_hmac('sha256', $payload, $secret), $signature)) {
http_response_code(401);
exit('Unauthorized');
}
// 2. Parse event
$event = json_decode($payload, true);
$order_id = $event['metadata']['orderId'] ?? null;
// 3. Handle
switch ($event['type']) {
case 'Settled':
// Idempotency guard
$order = $db->query('SELECT status FROM orders WHERE id = ?', [$order_id])->fetch();
if (!$order || $order['status'] === 'paid') break;
$db->prepare('UPDATE orders SET status = ?, paid_at = NOW() WHERE id = ?')
->execute(['paid', $order_id]);
triggerFulfillment($order_id);
break;
case 'Expired':
// Pay link remains open — no action needed unless underpaid
if (($event['additionalStatus'] ?? '') === 'Underpaid') {
notifyCustomerOfUnderpayment($order_id);
}
break;
}
http_response_code(200);
echo 'OK';
Signature verification
Always verify the X-Coinsnap-Sig header before processing. See Webhooks → Verification for full examples in Node.js, PHP, and Python, including common mistakes.
Reconciliation
The orderId field in the webhook payload matches the orderId you set when creating the pay link. Use this to look up the order in your system:
{
"type": "Settled",
"invoiceId": "inv_4Kz9mXpQ2rNvBtYwLs8cDf",
"metadata": {
"orderId": "ORD-2026-1045"
},
"additionalStatus": "None"
}
For accounting and ERP integrations, orderId is the field that links a Coinsnap payment back to the original invoice number in your external system.
Polling as a fallback (development only)
const res = await fetch(
`https://app.coinsnap.io/api/v1/stores/${storeId}/payment-requests/${payLinkId}`,
{ headers: { 'x-api-key': apiKey } }
);
const link = await res.json();
// Check link.status for payment state
Do not poll in production. Webhooks are the reliable confirmation mechanism.