Build with the API
Webhooks
Signed events delivered to your own endpoint, with retries, and how to verify them.
A webhook is Gridline calling you. Instead of asking "is the report ready yet?" over and over, you give Gridline an address and it sends you a message the moment something happens. Webhooks are signed, so you can be sure a message really came from us.
In the dashboard
Admins can manage endpoints without code under Developers → Webhooks: add an endpoint (its signing secret is shown once), Send a test, pause or resume it, replace its secret, remove it, and read every delivery with its status, response code and attempts. A failed delivery has a Send again button.
Set one up
/outgoing-webhooksAdmins only, with a session. API keys cannot manage webhooks.namestringrequired- A label for you, up to 100 characters.
urlstringrequired- Where to send events. Use HTTPS. Addresses that point to private networks are refused.
eventsstring[]required- Which events you want. See the list below.
curl https://gridline-data-analysis-app.duckdns.org/api/outgoing-webhooks \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Slack bridge",
"url": "https://example.com/hooks/gridline",
"events": ["report.ready", "rules.failed"]
}'How many endpoints you can have depends on your plan: Free 1, Basic 5, Premium no limit.
Events
| Event | Sent when | `data` contains |
|---|---|---|
file.uploaded | A file or a new version was saved. | fileId, datasetId, version |
file.sensitive_data_found | A report found personal or secret data in a file the whole company can see. | fileId, datasetId, version, columns (names and kinds, never values) |
dataset.changed | A new version was compared row by row with the one before it, and rows differ. | fileId, datasetId, version, previousVersion, added, removed, changed, unchanged |
dataset.schema_changed | A new version removed or retyped a column its predecessor had. | datasetId, fileId, version, previousVersion, columnsAdded, columnsRemoved, typeChanges |
report.ready | A report finished successfully. | fileId, reportStatus |
report.failed | A report failed for good. | fileId, reportStatus |
rules.failed | A file failed an error rule. | fileId, failedRuleCount, qualityScore |
quota.threshold | The company reached 80% or 100% of its file quota. | threshold, plan, periodKey, filesUsed, filesLimit |
invoice.finalized | An invoice was issued. | invoiceId, totalCents, periodStart, periodEnd |
There is also ping, which you trigger yourself to test an endpoint (POST /outgoing-webhooks/{id}/ping). Events carry ids and numbers, never cell values. When you need details, fetch them with an API key.
What you receive
A POST with a JSON body:
{
"id": "6f1e0c52-6a5e-4a52-8a7d-3b6b0b8d2f10",
"type": "report.ready",
"createdAt": "2026-10-02T09:14:07.014Z",
"companyId": "…",
"schemaVersion": 1,
"data": { "fileId": "5b0c1f1e-…", "reportStatus": "ready" }
}and these headers:
webhook-idoptional- The event's id. It is the same on every retry of the same event, so you can use it to ignore duplicates.
webhook-timestampoptional- When it was sent, in seconds since 1970.
webhook-signatureoptionalv1=followed by the signature (see below).user-agentoptionalGridline-Webhooks/1.0.
Check the signature
Anyone can send a request to your address, so verify every one. The signature is an HMAC-SHA256 of the timestamp, a dot, and the exact raw body, keyed with your whsec_… secret.
import { createHmac, timingSafeEqual } from "node:crypto";
export function isGenuine(secret, headers, rawBody) {
const timestamp = headers["webhook-timestamp"];
const received = headers["webhook-signature"];
const expected =
"v1=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(received ?? "");
if (a.length !== b.length || !timingSafeEqual(a, b)) return false;
// Reject old messages, so a captured one cannot be replayed later.
return Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
}Replying
Answer with any 2xx status as soon as you have received the event, and do the real work afterwards. Gridline treats anything else (or no answer) as a failure.
Retries
- A failed delivery is retried up to 5 times in all, with a growing delay between attempts.
- Delivery is at least once: an event can arrive twice. Use
webhook-idto skip ones you have already handled. - An answer of `410 Gone` means "stop sending these": the endpoint is disabled at once.
- After 20 failed deliveries in a row, the endpoint is disabled automatically. Fix the receiver, then turn it back on with
PATCH /outgoing-webhooks/{id}and{ "active": true }. - Events are sent after the change is saved. If an upload is rolled back, no event leaves.
Manage endpoints
GET /outgoing-webhooks: list your endpoints.PATCH /outgoing-webhooks/{id}: change thename,url,eventsoractive.DELETE /outgoing-webhooks/{id}: remove one.POST /outgoing-webhooks/{id}/rotate-secret: make a new secret (shown once). The old one stops signing at once, so deliveries in flight use the new one. Update your receiver first, or accept a few failed checks.GET /outgoing-webhooks/{id}/deliveries: what was sent, withstatus,attempts, the answer's status and the last error. Kept for 30 days.POST /outgoing-webhooks/deliveries/{deliveryId}/redeliver: send one again.