Webhook Integration
Emerald delivers a signed HTTP POST to your registered URL whenever a deposit
or withdrawal is attributed to your partnerId. Delivery is HMAC-signed so you
can verify authenticity, and retried on failure.
Registering a Webhook
Your endpoint must be https and resolve to a public host (loopback,
private, and link-local addresses are rejected). Emerald issues you a signing
secret (whsec_…) out of band, alongside your API key.
curl -X POST https://api.emeraldvaults.io/api/v1/partner/webhooks \
-H "x-api-key: sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-server.com/webhooks/emerald"}'
Response:
{
"ok": true,
"data": { "webhookUrl": "https://your-server.com/webhooks/emerald" }
}
Calling this endpoint again overwrites the existing webhook URL.
Events
Two event types are delivered:
| Event | Trigger |
|---|---|
deposit.referral | A deposit attributed to your partnerId was indexed |
withdrawal.referral | A withdrawal request attributed to your partnerId was indexed |
Payload
Each delivery is a JSON envelope. Numeric on-chain amounts are native-unit decimal strings.
{
"id": "f3c1a2b4-...-uuid",
"version": 1,
"type": "deposit.referral",
"createdAt": "2026-06-11T12:00:00.000Z",
"partnerId": 7,
"data": {
"vaultAddress": "0xdc55...",
"userAddress": "0xabc...",
"assets": "1000000000",
"shares": "999000000000000000000",
"txHash": "0x...",
"blockNumber": "24840900",
"logIndex": 3,
"timestamp": "2026-06-11T11:59:48.000Z"
}
}
For withdrawal.referral, data carries shares, assetsEstimate, and
requestIndex instead of assets / shares.
Headers
| Header | Meaning |
|---|---|
X-Indigo-Event-Id | UUID — also payload.id. Retries reuse it; use it to deduplicate. |
X-Indigo-Event-Type | deposit.referral / withdrawal.referral |
X-Indigo-Timestamp | Unix seconds. Reject deliveries older than ~5 min (replay defense). |
X-Indigo-Signature | sha256=<hex> — HMAC-SHA256 of `${timestamp}.${rawBody}` keyed with your signing secret. |
Verifying the signature
Sign over the raw request body bytes, not a re-serialized object.
import crypto from "node:crypto";
function verify(req, secret) {
const ts = req.headers["x-indigo-timestamp"];
const sig = req.headers["x-indigo-signature"]; // "sha256=…"
// 1) freshness — reject stale timestamps
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
// 2) recompute over the raw body
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(`${ts}.${req.rawBody}`).digest("hex");
// 3) constant-time compare
return (
sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
);
}
Delivery & retries
- Respond
2xxwithin 10 seconds to acknowledge. - A non-2xx response or timeout triggers up to 3 retries with backoff
(1s / 5s / 15s). The same
X-Indigo-Event-Idis reused on every attempt. - Redirects are not followed — point us at your final endpoint.
Best Practices
- Deduplicate on
X-Indigo-Event-Id— an event may be delivered more than once. - Verify every payload with the signature check above before trusting it.
- Process asynchronously — enqueue the payload and return
2xximmediately. - Rotate your signing secret with Emerald if it is ever exposed.