EkireDocs

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.

EventFires whendata fields
instance.createdA server finishes provisioningid, name, ipAddress, planId
instance.destroyedA server is destroyedid, name
instance.startedA server is powered onid, name
instance.stoppedA server is powered offid, name
instance.rebootedA server is rebootedid, name
instance.resizedA server is resized to a different planid, name, planId
payment.succeededA payment to your wallet succeedsamount, currency, balanceAfter
wallet.low_balanceYour wallet balance runs lowbalance
account.suspendedYour account is suspendedreason
account.reactivatedYour account is reactivated(none)
ticket.createdA support ticket is openedid, number, subject
ticket.repliedA support ticket receives a replyid, 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.

FieldTypeDescription
idstring (UUID)Unique delivery ID. Matches the X-Ekire-Delivery header — use it as an idempotency key.
eventstringThe event type, e.g. instance.created.
createdAtstring (ISO 8601)When the delivery was created, in UTC.
dataobjectThe 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:

HeaderValue
Content-Typeapplication/json
User-AgentEkire-Webhooks/1
X-Ekire-EventThe event type, e.g. instance.created
X-Ekire-DeliveryThe delivery ID (matches id in the body)
X-Ekire-TimestampUnix time in seconds when the delivery was created — the same instant as the envelope's createdAt. It stays constant across retries.
X-Ekire-SignatureThe 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:

  1. Read X-Ekire-Timestamp and X-Ekire-Signature from the request headers.
  2. Compute HMAC-SHA256("{timestamp}.{raw_body}", your_secret) and hex-encode it.
  3. Compare sha256=<your digest> against the header value using a constant-time comparison.
  4. 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-Delivery ID (which equals the envelope's id) as an idempotency key.
  • No redirects. We don't follow 3xx responses; 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.