Webhook events
If you haven't set up an endpoint yet, start with the Webhooks guide — it covers
creating an endpoint, copying your signing secret, and testing with a ping. Everything below is what
your receiver reads: the event catalog, the delivery envelope, and the signature check.
Event catalog
You subscribe an endpoint to one or more of these events. When a subscribed event fires on your account,
we POST a signed envelope to every active endpoint that listens for it. The event type appears both in
the X-Ekire-Event header and in the envelope's event field, and the event-specific fields live under
data.
| Event | Fires when | data fields |
|---|---|---|
instance.created | A server finishes provisioning | id, name, ipAddress, planId |
instance.destroyed | A server is destroyed | id, name |
instance.started | A server is powered on | id, name |
instance.stopped | A server is powered off | id, name |
instance.rebooted | A server is rebooted | id, name |
instance.resized | A server is resized to a different plan | id, name, planId |
payment.succeeded | A payment to your wallet succeeds | amount, currency, balanceAfter |
wallet.low_balance | Your wallet balance runs low | balance |
account.suspended | Your account is suspended | reason |
account.reactivated | Your account is reactivated | (none) |
ticket.created | A support ticket is opened | id, number, subject |
ticket.replied | A support ticket receives a reply | id, number, subject |
The ping event
The Test button on an endpoint sends a synthetic ping delivery whose data is a short message,
e.g. { "message": "This is a test event from Ekire." }. It's a way to confirm your endpoint receives
and verifies us — you don't subscribe to it, and it never fires on its own.
Monetary amounts (amount, balance, balanceAfter) are integers in minor units — cents of the
base currency, which currency names. 2000 is $20.00. planId is one of nano, small, medium.
reason on account.suspended describes why — for example "zero_balance".
Payload
Every delivery is a single HTTP POST with a JSON envelope. The envelope is the same for every event;
only the data object changes.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique delivery ID. Matches the X-Ekire-Delivery header — use it as an idempotency key. |
event | string | The event type, e.g. instance.created. |
createdAt | string (ISO 8601) | When the delivery was created, in UTC. |
data | object | The event-specific fields from the catalog above. |
A full instance.created delivery body looks like this:
{
"id": "8f2b1c9e-4d3a-4f21-9b7e-2c1a0d5e6f70",
"event": "instance.created",
"createdAt": "2026-07-19T10:00:00.000Z",
"data": {
"id": "b1d4e8a2-7c33-49f0-8a1b-9e2f4c6d0a55",
"name": "web-01",
"ipAddress": "203.0.113.42",
"planId": "nano"
}
}And a payment.succeeded delivery, to show a billing payload:
{
"id": "3a9c7e10-6b52-4d88-a1f4-0c7b2e9d4f13",
"event": "payment.succeeded",
"createdAt": "2026-07-19T10:05:00.000Z",
"data": {
"amount": 2000,
"currency": "USD",
"balanceAfter": 4250
}
}Verifying signatures
Every delivery is signed with HMAC-SHA256 using your endpoint's signing secret (the whsec_… value
shown once when you created the endpoint). Verifying the signature proves the request came from Ekire and
wasn't altered in transit.
Alongside the JSON body, each delivery carries these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Ekire-Webhooks/1 |
X-Ekire-Event | The event type, e.g. instance.created |
X-Ekire-Delivery | The delivery ID (matches id in the body) |
X-Ekire-Timestamp | Unix time in seconds when the delivery was created — the same instant as the envelope's createdAt. It stays constant across retries. |
X-Ekire-Signature | The signature, as sha256=<hex digest> |
The signed value is the timestamp and the raw request body joined with a . — that is,
"{timestamp}.{body}". Binding the timestamp into the signature means a captured body can't be replayed
under a new time. The digest is a lowercase hex string, sent as sha256=<digest>.
To verify a delivery:
- Read
X-Ekire-TimestampandX-Ekire-Signaturefrom the request headers. - Compute
HMAC-SHA256("{timestamp}.{raw_body}", your_secret)and hex-encode it. - Compare
sha256=<your digest>against the header value using a constant-time comparison. - Optionally reject deliveries whose timestamp is too old, to blunt replay attempts. Because the timestamp is fixed at creation and retries span roughly five minutes, your window must be wider than that or you'll reject legitimate final retries.
Use the raw request body exactly as received — parsing and re-serializing the JSON can change the bytes and break the match.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody is the exact bytes received; secret is your whsec_… value.
export function verifyEkireSignature(rawBody, headers, secret) {
const timestamp = headers["x-ekire-timestamp"];
const received = headers["x-ekire-signature"]; // "sha256=<hex>"
const digest = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const expected = `sha256=${digest}`;
// Constant-time compare — bail out if lengths differ, then compare bytes.
if (received.length !== expected.length) return false;
const ok = timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) return false;
// Optional replay defense: 10 minutes, wide enough to cover the retry span.
const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
return skew <= 600;
}Verify before you trust
Always verify the signature before acting on a payload. An unverified request could come from anyone. Keep your signing secret in a secrets manager — never in client-side code or source control — and if it may be exposed, delete the endpoint and create a new one to rotate it.
Delivery and retries
Your endpoint should return a 2xx status quickly to acknowledge a delivery. Anything else — a non-2xx status, a redirect, a timeout, or a connection error — counts as a failed attempt.
- Timeout. We wait up to 10 seconds for a response, so acknowledge fast and do heavy work afterward.
- Retries. A failed delivery is retried with exponential backoff — up to 6 attempts total,
spanning roughly 10 seconds to a few minutes. Because a delivery can arrive more than once, treat the
X-Ekire-DeliveryID (which equals the envelope'sid) as an idempotency key. - No redirects. We don't follow
3xxresponses; a redirect is treated as a failure. Point the endpoint at its final URL. - Auto-pause. After a sustained run of consecutive failures, an endpoint is automatically paused so we stop hammering a dead URL. Fix it and re-enable it in the console — that clears the failure streak.