Skip to main content
This is the primary way you learn that a payment has been completed. When a transfer involving your OCID finishes, the payment gateway POSTs a signed proof to this endpoint.

Flow

  1. Customer pays via a payment gateway
  2. Gateway completes the transfer to your OCID
  3. Gateway POSTs signed proof to your /transfer/webhook
  4. You verify the proof and mark the order as paid

Verifying the Proof

Always verify the proof before trusting it:
app.post('/transfer/webhook', async (req, res) => {
  const { proof, signature } = req.body;

  // 1. Check the issuer is trusted
  if (!acceptedIssuers.includes(proof.issuer)) {
    return res.status(400).json({
      error: { code: 'ISSUER_NOT_ACCEPTED', message: 'Unknown issuer' }
    });
  }

  // 2. Fetch issuer's public key
  const issuerMetadata = await fetch(`${issuerEndpoint}/metadata.json`);
  const publicKey = issuerMetadata.config.publicKey;

  // 3. Verify signature
  const canonical = JSON.stringify(proof, Object.keys(proof).sort());
  if (!secp256k1_verify(publicKey, signature, SHA256(canonical))) {
    return res.status(400).json({
      error: { code: 'PROOF_SIGNATURE_INVALID', message: 'Invalid signature' }
    });
  }

  // 4. Check recipient is your OCID
  if (proof.to.ocid !== YOUR_OCID) {
    return res.status(400).json({
      error: { code: 'INVALID_PROOF', message: 'Wrong recipient' }
    });
  }

  // 5. Find and update the order
  const order = db.getOrderByReference(proof.to.reference);
  if (order && proof.amount === order.amount) {
    db.markOrderPaid(order.id, proof.txid, proof.timestamp);
  }

  res.json({ status: 'accepted', txid: proof.txid });
});

Proof Fields

FieldDescription
proof.txidUnique transaction ID from the issuer
proof.issuerOCID of the service that executed the transfer
proof.from.ocidSender’s OCID
proof.to.ocidYour merchant OCID
proof.to.referenceYour order ID (if provided during payment)
proof.amountTransfer amount
proof.currencyCurrency code
proof.timestampWhen the transfer completed

Respond to the webhook call

Return accepted if you processed the notification:
{
  "status": "accepted",
  "txid": "gateway_tx_456"
}
Return rejected with a reason if there’s an issue:
{
  "status": "rejected",
  "txid": "gateway_tx_456",
  "message": "Order already paid"
}