Webhooks

A webhook tells your own server when items change. When an item is created, updated, restored or deleted, JSONPad sends the change to a URL you choose as a signed POST request, and your server does whatever your app needs: sends an email, posts to Slack, charges a card, updates a search index, or keeps a copy of the data.

JSONPad doesn't run your code, and doesn't hold your other secrets. Webhooks are where your server takes over, so anything that needs a private API key, like a payment provider, stays on your server. Your app keeps talking to JSONPad directly, and your server only hears about the changes it cares about.

Creating a webhook

Open Webhooks in the dashboard and click Create. Each webhook has:

  • a URL: your server's endpoint. It must use https, and be reachable from the internet. Addresses on private networks are refused.
  • the events to send (see below)
  • the lists to send them for: particular lists, or all of your lists, including ones you create later
  • a signing secret, created with the webhook, which your server uses to check that a request really came from JSONPad

Click Send ping on the webhook's page to send a test event straight away and see how your server responded.

Webhooks are part of your account's configuration, so they are managed by you (in the dashboard, or in user auth mode), never with an API token. Each plan allows a number of webhooks: 1 on Free, 5 on Indie, 20 on Pro and 50 on Scale. Deliveries are not counted as requests.

Events

  • item.createdAn item was created.
  • item.updatedAn item, or part of its data, was changed. The payload includes the item before the change as previous, so you can tell what changed.
  • item.restoredAn item was restored to an earlier version. The payload includes previous, unless the item had been deleted.
  • item.deletedAn item was deleted. The payload includes the item as it was.
  • pingA test event from the dashboard's Send ping button. It's always sent, and has no item.

Events are sent however the change was made: with an API token, by an identity, or in the dashboard.

The payload

Each delivery is a JSON body like this one, for an item.updated event:

{
id: "b0a4f0f4-6c83-4d0d-9a57-5bb7bf36a0e2"
type: "item.updated"
createdAt: "2026-09-19T14:03:13.000Z"
data: {
list: {
id: "3e1f8a4d-9d0f-4b87-8f0e-1c2b3a4d5e6f"
name: "Orders"
pathName: "orders"
}
item: {
id: "0b9f4a52-7f3e-4b8a-9d1c-2e3f4a5b6c7d"
createdAt: "2026-09-19T13:58:02.000Z"
updatedAt: "2026-09-19T14:03:13.000Z"
identity: null
data: {
orderNumber: 1042
email: "alice@example.com"
status: "paid"
}
version: "1.0.1"
readonly: false
activated: true
description: ""
tags: [
]
size: 66
locked: false
}
previous: {
id: "0b9f4a52-7f3e-4b8a-9d1c-2e3f4a5b6c7d"
...: "..."
data: {
orderNumber: 1042
email: "alice@example.com"
status: "pending"
}
version: "1.0.0"
}
actor: {
authMode: "token"
tokenId: "7c6d5e4f-3a2b-4c1d-9e8f-7a6b5c4d3e2f"
identityId: null
}
}
}
  • idstringThe event's id. Every webhook sent the same change gets the same id, and so does a redelivery, so use it to handle each event once.
  • typestringThe event type (see above).
  • data.listobjectThe list the item is in.
  • data.itemobjectThe item, as the dashboard shows it. Values hidden by guard indexes are included, because the webhook is your own server. If the item's data is larger than 256KB, data is null and dataOmitted is true: fetch the item from the API instead.
  • data.previousobjectFor item.updated and item.restored, the item before the change.
  • data.actorobjectWho made the change: authMode is token or user (the dashboard), with the id of the token and of the identity, if any.

Verifying signatures

Every delivery has these headers:

  • x-jsonpad-signatureheadert=<timestamp>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <timestamp>.<body> using the webhook's signing secret. Always check it, and that the timestamp is recent, before trusting the request: anyone can send a request to your URL.
  • x-jsonpad-eventheaderThe same as type.
  • x-jsonpad-deliveryheaderThe delivery's id, as shown in the dashboard's delivery log. It stays the same when a delivery is retried.
Node (Express)
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950import crypto from 'crypto'; import express from 'express'; const app = express(); // Keep the raw body: the signature is computed over the exact bytes sent app.post( '/webhooks/jsonpad', express.raw({ type: 'application/json' }), async (req, res) => { const body = req.body.toString('utf8'); const signature = req.header('x-jsonpad-signature') ?? ''; const [, timestamp, v1] = signature.match(/^t=(\d+),v1=([0-9a-f]+)$/) ?? []; const expected = crypto .createHmac('sha256', process.env.JSONPAD_WEBHOOK_SECRET!) .update(`${timestamp}.${body}`) .digest('hex'); const valid = !!v1 && v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) && Math.abs(Date.now() / 1000 - Number(timestamp)) < 5 * 60; if (!valid) { return res.status(401).end(); } const event = JSON.parse(body); // A redelivery has the same id, so handle each event once if (await alreadyHandled(event.id)) { return res.status(204).end(); } if (event.type === 'item.updated') { const { item, previous } = event.data; if (item.data.status === 'paid' && previous.data.status !== 'paid') { await sendReceipt(item.data.email, item.data.orderNumber); } } // Respond quickly with a 2xx, or JSONPad will try again later res.status(204).end(); } ); app.listen(3000);

Deliveries are signed when they're sent, with the webhook's current secret. If you regenerate the secret, deliveries still waiting to be sent are signed with the new one.

Responding, and retries

Respond with any 2xx status within 10 seconds. Do the slow work afterwards, or in a queue of your own.

Anything else (another status, a timeout, or your server being unreachable) is tried again: after 30 seconds, 2 minutes, 10 minutes, 1 hour, 3 hours, 6 hours and 12 hours, which is 8 attempts over about a day. After the last attempt the delivery has failed for good, and you can redeliver it from the dashboard once your server is fixed.

Deliveries are at least once, and not necessarily in order. A retry can arrive after a later event, and a delivery can very occasionally arrive twice. Use the event id to ignore duplicates, and the item's updatedAt or version to ignore stale changes.

Only one delivery is sent to a webhook at a time, so a slow server slows down its own deliveries, not anyone else's.

Failing webhooks

If 20 deliveries in a row fail for good, JSONPad disables the webhook, records a webhook-disabled event in your event log, and shows why on the webhook's page. Deliveries already waiting are kept, and sent once you activate the webhook again.

A webhook with 1,000 deliveries waiting isn't given any more until some of them are sent, so a server that's been down for a long time can miss events. Check the delivery log if your server has been unavailable.

The delivery log

Each webhook's page lists its deliveries from the last 30 days: the event, its status, how many attempts were made, and your server's response (the status and the start of the body). Click a delivery to see its payload, and to redeliver it: the same payload, with the same event id, is queued to be sent again.

Managing webhooks with the API

These endpoints are only available in user auth mode (they're what the dashboard uses), not with an API token.

  • GET /webhooksList your webhooks (paginated).
  • POST /webhooksCreate a webhook: url and events are required; description, listIds (empty for all lists) and activated are optional.
  • GET /webhooks/:webhookIdFetch a webhook.
  • PUT /webhooks/:webhookIdUpdate a webhook. Activating a disabled webhook resets its failure count.
  • DELETE /webhooks/:webhookIdDelete a webhook and its deliveries.
  • POST /webhooks/:webhookId/secretReplace the signing secret.
  • POST /webhooks/:webhookId/pingSend a ping and return the result.
  • GET /webhooks/:webhookId/deliveriesList deliveries (paginated, newest first), optionally filtered by status (pending, succeeded or failed), event or itemId.
  • GET /webhooks/:webhookId/deliveries/:deliveryIdFetch a delivery, including its payload.
  • POST /webhooks/:webhookId/deliveries/:deliveryId/redeliverQueue a delivery to be sent again.