Settlement providers POST signed proofs to this endpoint when transfers complete. Match the proof to pending payments and forward your own signed proof to merchants.
When You Receive Webhooks
You receive webhooks when:
- A settlement provider you accept completes a transfer to your OCID
- The transfer was for a pending payment at your gateway
- You need to credit a merchant and notify them
Implementation
app.post('/transfer/webhook', verifyAuth, async (req, res) => {
const { proof, signature } = req.body;
const senderOcid = parseInt(req.headers['x-oc-id']);
// 1. Verify sender is an accepted settlement provider
if (!ACCEPTED_SETTLEMENT_PROVIDERS.includes(senderOcid)) {
return res.status(400).json({
error: { code: 'ISSUER_NOT_ACCEPTED', message: 'Unknown settlement provider' }
});
}
// 2. Verify sender is the proof issuer
if (proof.issuer !== senderOcid) {
return res.status(400).json({
error: { code: 'INVALID_PROOF', message: 'Issuer mismatch' }
});
}
// 3. Verify proof signature
const publicKey = await getPublicKey(senderOcid);
const canonical = JSON.stringify(proof, Object.keys(proof).sort());
if (!secp256k1_verify(publicKey, signature, canonical)) {
return res.status(400).json({
error: { code: 'PROOF_SIGNATURE_INVALID', message: 'Invalid signature' }
});
}
// 4. Verify we are the recipient
if (proof.to.ocid !== YOUR_OCID) {
return res.status(400).json({
error: { code: 'INVALID_PROOF', message: 'Wrong recipient' }
});
}
// 5. Find the pending payment
const payment = await db.getPaymentByReference(proof.to.reference);
if (!payment) {
// Log for investigation but accept
console.log(`Unknown payment reference: ${proof.to.reference}`);
return res.json({ status: 'accepted', txid: proof.txid });
}
// 6. Verify amount matches
if (proof.amount !== payment.amount || proof.currency !== payment.currency) {
return res.status(400).json({
error: { code: 'AMOUNT_MISMATCH', message: 'Amount does not match' }
});
}
// 7. Mark payment as complete
await db.completePayment(payment.id, proof.txid, proof.timestamp);
// 8. Credit merchant account
await db.creditMerchant(payment.merchantOcid, payment.amount, payment.currency);
// 9. Create and send proof to merchant
const merchantProof = {
txid: payment.txid,
issuer: YOUR_OCID,
from: proof.from,
to: { ocid: payment.merchantOcid, reference: payment.orderId },
amount: payment.amount,
currency: payment.currency,
timestamp: Math.floor(Date.now() / 1000),
memo: `Payment for ${payment.orderId}`
};
await notifyMerchant(payment.merchantOcid, merchantProof);
res.json({ status: 'accepted', txid: proof.txid });
});
Forwarding to Merchants
After receiving a valid proof, create your own proof for the merchant:
async function notifyMerchant(merchantOcid, proof) {
const merchant = await db.getMerchant(merchantOcid);
const endpoint = merchant.webhookUrl || await resolveEndpoint(merchantOcid);
const signature = signProof(proof);
await fetch(`${endpoint}/transfer/webhook`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...createAuthHeaders(YOUR_OCID, YOUR_PRIVATE_KEY)
},
body: JSON.stringify({ proof, signature })
});
}
Always verify the proof signature before crediting accounts or forwarding to merchants.