Webhooks
Overview
Webhooks provide real-time notifications about events in your Yugo account. Instead of polling the API for status changes, Yugo will send HTTP POST requests to your configured webhook URL when events occur.
Events
Yugo sends webhook notifications for the following events:
- Payin Status Changed
- Payout Status Changed
Webhook Request Format
Headers
Each webhook request includes the following headers:
POST /your-webhook-endpoint
Host: your-domain.com
Content-Type: application/json
X-Webhook-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Authentication: Yugo signs every webhook request with your webhook secret and sends the signature in the X-Webhook-Signature header. Compute the same signature on your side and compare to verify the request came from Yugo and was not modified in transit.
Payload
The request body contains the complete resource object (Payin or Payout) in JSON format.
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"reference": "ORDER-12345",
"status": "AUTHORIZED",
// ...
}
Verifying the Signature
The signature is the HMAC-SHA256 of the raw request body, keyed with your webhook secret, encoded as a lowercase hex string.
X-Webhook-Signature = hex( HMAC_SHA256( webhook_secret, raw_request_body ) )
To verify:
- Read the raw request body as bytes, before any JSON parsing or re-serialization — even a whitespace difference will produce a different signature.
- Compute
HMAC-SHA256(webhook_secret, raw_body)and hex-encode the result. - Compare against the value in
X-Webhook-Signatureusing a constant-time comparison (e.g.crypto.timingSafeEqualin Node.js,hmac.compare_digestin Python). - If they do not match, reject the request with
401 Unauthorized.
Example (Express.js)
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use the raw body parser so the bytes are preserved exactly as sent.
app.post(
'/webhooks/yugo',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-webhook-signature'];
const expected = crypto
.createHmac('sha256', process.env.YUGO_WEBHOOK_SECRET)
.update(req.body) // Buffer of the raw body
.digest('hex');
// Check the shape before decoding: Buffer.from(str, 'hex') silently stops at the
// first non-hex character, and timingSafeEqual throws on unequal byte lengths — a
// malformed header would 500 instead of 401.
const valid =
typeof signature === 'string' &&
/^[0-9a-f]{64}$/.test(signature) &&
crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex'),
);
if (!valid) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
// ... process event, then acknowledge quickly
res.status(200).send('OK');
},
);
Webhook Secrets
You configure and view your webhook secret in the Yugo Merchant Portal. Treat it like a password: store it as a secret, never expose it in client-side code or logs.
Secret Rotation
Yugo supports rotating the webhook secret without downtime via a primary / secondary scheme:
- The primary secret is used to sign all outgoing webhooks.
- When you rotate, the previous primary is retained as the secondary secret.
- During the rotation window, your endpoint should accept a signature that matches either the primary or the secondary secret. This lets in-flight webhooks signed with the old secret still validate while you finish rolling out the new one.
- Once you have updated your verification logic everywhere, you can drop the secondary.
A typical verification loop during rotation:
const secrets = [process.env.YUGO_WEBHOOK_SECRET_PRIMARY, process.env.YUGO_WEBHOOK_SECRET_SECONDARY]
.filter(Boolean);
const wellFormed = typeof signature === 'string' && /^[0-9a-f]{64}$/.test(signature);
const valid = wellFormed && secrets.some((secret) => {
const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
});
Implementing a Webhook Endpoint
Basic Requirements
Your webhook endpoint must:
- Accept POST requests - Yugo sends all webhooks as HTTP POST
- Return 200 OK - Respond with status code 200-299 to acknowledge receipt
- Respond quickly - Return a response within 1 second
- Be publicly accessible - The URL must be reachable from the internet
- Use HTTPS - Webhook URLs must use HTTPS in production
- Be idempotent - Handle duplicate webhook deliveries gracefully
- Verify the signature - Recompute the HMAC over the raw body and compare against
X-Webhook-Signaturebefore trusting the payload - Be whitelisted - Add your endpoint URL in the Yugo Merchant Portal
Retry Behavior
When Yugo Retries
Yugo automatically retries webhook delivery when:
- Your endpoint returns a 4xx or 5xx status code
- Your endpoint doesn't respond within 5 seconds (timeout)
- There's a network error preventing delivery
Retry Schedule
Yugo uses exponential backoff for up to 48 hours.
You can query the resource via the API to get the current status.
Common Issues
Issue: Webhooks Not Received
Possible Causes:
- Webhook URL is not publicly accessible
- Firewall blocking Yugo's IP addresses
- SSL certificate issues (expired, self-signed)
- Endpoint returning non-2xx status code
Solution:
- Test your webhook URL with
curlfrom an external server - Check firewall and security group settings
- Verify SSL certificate is valid
- Check application logs for errors