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');
const valid =
typeof signature === 'string' &&
signature.length === expected.length &&
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 valid = secrets.some((secret) => {
const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
return (
signature.length === expected.length &&
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
Issue: Duplicate Webhooks
Possible Cause: Your endpoint is slow to respond, causing timeouts and retries.
Solution:
- Acknowledge webhooks immediately (return 200 OK)
- Process events asynchronously in background jobs
- Implement idempotency checks
FAQ
Q: Can I use the same webhook URL for multiple events?
Yes. Your endpoint will receive all event types at the same URL. Inspect the payload structure to determine the event type.
Q: What if my webhook endpoint is down during an event?
Yugo will retry delivery multiple times. If your endpoint is down longer, you can still query the API to get the current resource status.
Q: Can I change my webhook URL?
Yes. Update the webhook_url field when creating new Payins/Payouts. The webhook URL is set per resource, not account-wide.
Q: How do I test webhooks without creating real payments?
The API currently runs against the live endpoint https://api.yugo.finance/. A dedicated sandbox is coming soon.