Jumploon

Webhooks

Webhooks push events to your server as they happen: an email was delivered, opened, bounced; a contact was created; mail was received. Jumploon offers two kinds. Most apps want webhook endpoints; the event webhook suits apps that prefer events in batches.

Webhook endpointsEvent webhook (v3)
EndpointsAs many as you like, each with its own eventsOne per account
DeliveryOne event per requestBatches of events as a JSON array
Signaturewebhook-id, webhook-timestamp and webhook-signature headersX-Mailserver-Timestamp and X-Mailserver-Signature headers
Set upPOST /webhooks or Webhooks in the dashboardPATCH /v3/user/webhooks/event/settings

Both retry failed deliveries with increasing delays, up to 10 times. A delivery counts as failed unless your server answers with a 2xx status within a few seconds, so respond quickly and do slow work in the background.

Webhook endpoints

Create an endpoint

curl https://api.jumploon.com/webhooks \
  -H "Authorization: Bearer $JUMPLOON_API_KEY" -H "Content-Type: application/json" \
  -d '{ "endpoint": "https://acme.com/hooks/email", "events": ["email.delivered", "email.bounced", "email.clicked"] }'

The response includes the endpoint's signing_secret (it starts with whsec_). Rotate it with POST /webhooks/:id/signing-secret/rotate.

Events

EventWhen
email.sentThe email was accepted and queued
email.scheduledThe email was scheduled for later
email.deliveredThe recipient's server accepted it
email.delivery_delayedA temporary failure; it will be retried
email.bouncedPermanently rejected; the address is now suppressed
email.failedCouldn't be sent, for example after retries ran out
email.suppressedNot sent, because the address is suppressed
email.openedThe recipient opened it (needs open tracking)
email.clickedThe recipient clicked a link (needs click tracking)
email.receivedMail arrived at your inbound host
contact.created, contact.updated, contact.deletedContact changes
domain.created, domain.updated, domain.deletedDomain changes

Delivery events are per recipient. email.complained can be subscribed to, but isn't produced yet, because Jumploon doesn't process mailbox-provider complaint feedback loops yet.

Payload

{
  "type": "email.delivered",
  "created_at": "2026-10-18T09:41:03.000Z",
  "data": {
    "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
    "from": "Acme <hello@acme.com>",
    "to": ["ada@example.org"],
    "subject": "Your receipt",
    "created_at": "2026-10-18T09:41:02.000Z",
    "tags": { "type": "receipt" }
  }
}

Verify the signature

Always verify that a request came from Jumploon before trusting it. Each request carries three headers:

  • webhook-id: the event's ID, the same on every retry. Use it to ignore duplicates.
  • webhook-timestamp: when it was sent, in Unix seconds.
  • webhook-signature: v1, followed by the base64 HMAC-SHA256 of <id>.<timestamp>.<raw body>, keyed with your signing secret (the part after whsec_, base64-decoded).
import crypto from "node:crypto";
 
export async function POST(req: Request) {
  const body = await req.text(); // the raw body, not parsed JSON
  const id = req.headers.get("webhook-id")!;
  const timestamp = req.headers.get("webhook-timestamp")!;
  const key = Buffer.from(process.env.JUMPLOON_WEBHOOK_SECRET!.replace(/^whsec_/, ""), "base64");
  const expected = `v1,${crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest("base64")}`;
 
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  const signature = req.headers.get("webhook-signature")!;
  const valid = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!fresh || !valid) return new Response(null, { status: 401 });
 
  const event = JSON.parse(body); // event.type, event.data …
  return new Response(null, { status: 200 });
}

Any library that follows the Standard Webhooks spec can verify these too.

Recent deliveries and their responses are listed at GET /webhooks/:id/events and in the dashboard.

Event webhook (v3)

Enable it

curl -X PATCH https://api.jumploon.com/v3/user/webhooks/event/settings \
  -H "Authorization: Bearer $JUMPLOON_API_KEY" -H "Content-Type: application/json" \
  -d '{ "enabled": true, "url": "https://acme.com/hooks/events" }'

The response includes the signing secret.

Payload

A JSON array of events. Event types: processed, dropped, deferred, delivered, bounce, open, click and unsubscribe. A message's custom_args appear as top-level fields.

[
  {
    "email": "ada@example.org",
    "timestamp": 1792316463,
    "event": "delivered",
    "sg_event_id": "x9q2m7c4v1b8n3k6",
    "sg_message_id": "…",
    "category": ["receipt"],
    "order_id": "1042"
  }
]

Events can arrive more than once. Deduplicate on sg_event_id.

Verify the signature

Each batch has X-Mailserver-Timestamp and X-Mailserver-Signature headers. The signature is the hex HMAC-SHA256 of <timestamp>.<raw body>:

import crypto from "node:crypto";
 
function verify(rawBody: string, timestamp: string, signature: string, secret: string) {
  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  return expected.length === signature.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Reject requests whose timestamp is more than a few minutes old to prevent replays.