Overview — Webhooks
When an event you subscribe to happens, Abyssale sends an HTTP POST with a JSON payload to a URL you control — so you learn about a finished render without polling for it.
Start by creating a webhook in the dashboard, then pick your payloads from the Events reference.
What your endpoint must do
| Property | Detail |
|---|---|
| Method | HTTP POST (HTTPS only) |
| Content type | application/json |
| Endpoint | A public URL on your server |
| Acknowledgement | Return 200 or 201 within 15 seconds |
| Retries | 6 attempts in total — the first delivery, then up to 5 retries, spanning about 3 hours |
| Backoff | 15 s, 3 min, 10 min, 30 min, 2 h — the five gaps between those six attempts |
| Identifying header | X-Referer: api.abyssale.com |
| Signature | X-Abyssale-Signature: t=…,v1=… once the workspace has a signing secret — how to verify |
| Delivery id | X-Abyssale-Delivery-Id — 64 lowercase hex characters, the same on every attempt of one delivery |
| Static IP support | Contact support for IP whitelisting ranges |
Anything other than 200/201 counts as a failure and is retried. The 15 seconds is a hard timeout, so acknowledge first and do your work afterwards.
One delivery uses a slightly different ladder
NEW_EXPORT sent to an export's own callback_url is retried the same 6 times over about 3 hours, but the first gap is 60 seconds rather than 15 — so the ladder is 60 s, 3 min, 10 min, 30 min, 2 h.
It also has no subscription behind it, so none of the deactivation rules below can apply: a delivery that exhausts its retries is simply dropped, and nothing is switched off. Dashboard subscriptions and a callback_url on a generation request both use the 15 s ladder above.
Answer 410 to stop the retries
410 Gone is the response to give when an endpoint is retired: it is never retried, and it deactivates the subscription.
Three 400 bodies are treated the same way, because they come from integration platforms that cannot accept the delivery at all — the body must match exactly:
Queue is full.There is no scenario listening for this webhook.Organization is in read-only mode.
Every other failure keeps its place in the backoff schedule above.
Three failures also retire a subscription once the retries run out
A delivery that is still failing on the sixth and final attempt with any of these deactivates the subscription, exactly as a 410 would:
404 Not Found- a timeout — no response within 15 seconds
- a connection error — DNS failure, refused connection, TLS failure
So a receiver that is unreachable for the full ~3 hours, or that consistently answers slower than 15 seconds, is retired silently. Nothing notifies you, and deliveries stop.
A persistent 5xx is treated differently: it exhausts its retries and the delivery is dropped, but the subscription survives. If your receiver needs a maintenance window, failing with a 503 is therefore much safer than letting connections time out.
Only a subscription can be deactivated this way. A per-job callback_url is not a subscription, so there is nothing to retire — a failed delivery is simply lost.
Verify the signature, not the X-Referer header
X-Referer is a hint, not authentication — anything that finds your URL can send it. Check the X-Abyssale-Signature header instead: see Signature verification.
Signing is opt-in, so until you call a /signing-secret endpoint once your deliveries carry no signature and the payload alone does not prove the request came from Abyssale. Until then, use an unguessable callback path and verify anything that matters against the API (GET /generation-request/{id}, GET /banners/{id}) before acting on it.
Build your receiver to be idempotent. A retry re-sends the same payload, and a webhook is not ordered against your own polling — you may see a job finish through the status endpoint first. Treat a payload as "this is ready", keyed on its id, rather than as a one-shot event you cannot afford to miss. Every attempt of one delivery carries the same X-Abyssale-Delivery-Id, which is what to deduplicate on.
Every payload names its event
Every webhook payload carries a top-level event_type field ("NEW_BANNER", "NEW_BANNER_BATCH", "NEW_EXPORT", "TEMPLATE_STATUS", …) — route on it instead of inferring the event from the payload structure. Job-specific callback_url deliveries carry it too.
Build your receiver
A receiver that honours every rule above, in the order that matters: bind the raw body, verify, drop repeats, acknowledge, then work.
This receiver rejects unsigned deliveries
Signing is opt-in, so until the workspace has a secret your deliveries carry no signature and this handler answers 401 to every one of them. A 401 is an ordinary failure: it is retried for about three hours and then dropped. Your subscription survives — but you lose every delivery in the meantime. Enable signing first, or skip the verification branch until you do.
import express from 'express';
import { verifyWebhookSignature } from '@abyssale/sdk/webhooks';
const app = express();
const seen = new Set(); // use Redis or your database — a Set dies with the process
// The signature covers the bytes as sent, so bind the RAW body. A parsed body
// re-serialized is a different string and will never verify.
app.post('/abyssale/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const valid = verifyWebhookSignature({
body: req.body, // Buffer, exactly as received
header: req.get('X-Abyssale-Signature'),
secret: process.env.ABYSSALE_SIGNING_SECRET,
});
if (!valid) return res.sendStatus(401);
const deliveryId = req.get('X-Abyssale-Delivery-Id');
if (seen.has(deliveryId)) return res.sendStatus(200); // a retry, already handled
seen.add(deliveryId);
res.sendStatus(200); // acknowledge inside the 15 s window, then do the work
const payload = JSON.parse(req.body);
switch (payload.event_type) {
case 'NEW_BANNER': /* one visual is ready */ break;
case 'NEW_BANNER_BATCH': /* a multi-format job finished */ break;
case 'NEW_EXPORT': /* a ZIP export is ready */ break;
case 'TEMPLATE_STATUS': /* a design changed status */ break;
default: break; // unknown event — ignore it, do not fail
}
});
app.listen(3000);Both helpers ship in a dependency-free entry point that does not need an API key — @abyssale/sdk/webhooks and abyssale.webhooks — so a receiver-only service can import them without configuring a client. Verification returns false rather than throwing, which is why the check reads as a plain boolean.
Retiring the endpoint? Return 410 Gone instead of 200 and the subscription is deactivated, as above. Anything else — including a 404 from a path you have removed — keeps being retried for about three hours.
What people use them for
| Event | Typical use |
|---|---|
NEW_BANNER | Take the download URL and push the asset to your own CDN or S3 bucket |
NEW_BANNER_BATCH | Know the exact moment an async batch finishes |
NEW_EXPORT | Download the ZIP and notify your team |
TEMPLATE_STATUS | Tell internal tools when a design is approved or rejected |
