Webhooks
Get notified the moment something happens — no polling. Every delivery is signed and retried.
Set your webhook URL and reveal your signing secret from the Webhooks page. We POST a JSON event to your URL and retry with backoff until you return a 2xx.
Events
| Field | Type | Description | |
|---|---|---|---|
| buy.completed | event | optional | A buy was fulfilled and the asset credited. |
| sell.completed | event | optional | A sell settled into the user's INR wallet. |
| sip.created | event | optional | A recurring plan (SIP) mandate was created. |
| sip.installment | event | optional | A SIP installment executed. |
| test.ping | event | optional | Sent when you use “Send test event” in the dashboard. |
Payload
Every delivery has the same envelope. event_id is stable across retries — deduplicate on it. Sandbox deliveries carry "mode": "sandbox".
{
"event": "buy.completed",
"event_id": "evt_9f2c1a7b3d",
"created_at": "2026-08-08T09:12:44.000Z",
"mode": "live",
"data": {
"user_code": "usr_xxx",
"asset_type": "gold",
"amount_inr": 1000,
"quantity_grams": 0.1234,
"transaction_id": "…",
"status": "completed"
}
}Headers
| Field | Type | Description | |
|---|---|---|---|
| X-OroPocket-Signature | string | optional | sha256=<hex HMAC>. See below — signed over `{timestamp}.{rawBody}`, NOT the body alone. |
| X-OroPocket-Timestamp | string | optional | Unix seconds. Part of the signed payload; reject old timestamps to stop replays. |
| X-OroPocket-Event | string | optional | The event name, e.g. buy.completed. |
| X-OroPocket-Event-Id | string | optional | Same value as event_id in the body — use it to dedupe. |
Any static credential headers you need (e.g. x-access-key) can be attached to every delivery including retries — ask support to configure them. OroPocket-set headers can never be overridden.
Verify the signature
`${timestamp}.${rawBody}` — the timestamp, a literal dot, then the raw request body. Hashing the body on its own will fail every time. Use the raw bytes, before any JSON parsing or re-serialisation.import crypto from "crypto";
// rawBody must be the raw string/Buffer, e.g. express.raw({type:"application/json"})
function verify(rawBody, headers, secret) {
const signature = headers["x-oropocket-signature"] || "";
const timestamp = headers["x-oropocket-timestamp"] || "";
// Reject replays: ignore anything older than 5 minutes.
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || age > 300) return false;
const expected =
"sha256=" +
crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Retries
We make one immediate attempt with a 10 second timeout, then retry failures up to 5 attempts total. Any 2xx acknowledges the delivery; non-2xx responses and timeouts are retried on this backoff:
| Field | Type | Description | |
|---|---|---|---|
| After attempt 1 | 30 seconds | optional | First retry. |
| After attempt 2 | 2 minutes | optional | |
| After attempt 3 | 10 minutes | optional | |
| After attempt 4 | 60 minutes | optional | |
| After attempt 5 | 6 hours | optional | Final attempt; then the delivery is marked failed. |
The retry queue lives in the database, so retries survive restarts. Return a 2xx as soon as you've persisted the event and do slow work asynchronously — otherwise the 10s timeout will cause duplicate deliveries.