ReleasePad Webhooks
Get an HTTP request on your own server the moment a changelog post is created, published or deleted.
Every request carries the full post as JSON and an X-ReleasePad-Event header
that tells you what happened before you parse a single byte of the body.
X-ReleasePad-Event: post.published
Introduction
A webhook is a URL on your server that ReleasePad calls when something happens to a post. Instead of
polling the Management API for changes, you
register an endpoint once and ReleasePad sends an HTTP POST to it with
the full post as JSON. Typical uses: announce a release in Slack or Discord, kick off an email campaign,
sync release notes into a help center or CRM, or trigger an automation in Zapier, Make or n8n.
Webhooks are configured per product. A product can have as many endpoints as you like, and each endpoint picks which of the three events it wants. Every request uses the same envelope, and the post inside it has exactly the same shape as the post object of the Management API, so code that already reads API responses can read webhook payloads unchanged.
Transport
HTTP POST with a JSON body to the URL you register. HTTPS
endpoints only.
Events
post.created, post.published,
post.deleted, plus a test event you trigger from the dashboard.
Availability
Paid plans only. See pricing.
Set up an endpoint
Endpoints are managed by an account admin from the product's settings in the dashboard. There is nothing to configure on the API side.
- Open the product and go to More → Webhooks.
- Click New webhook, paste the endpoint URL and tick the events you want. All three are selected by default; you need at least one.
- Click Add webhook. The endpoint is active right away.
- Click Send test next to it. ReleasePad posts a
testevent immediately and shows you the HTTP status your server answered with, so you can confirm the wiring before any real event fires. - Use Pause to stop deliveries without losing the configuration, and Remove to delete the endpoint.
The URL must start with https:// and include a host. Plain
http:// endpoints are rejected: post bodies are your release notes, and
they never travel unencrypted. To receive events on a machine without a certificate, put it behind a tunnel
such as ngrok or Cloudflare Tunnel, which terminates TLS for you and gives you an https URL to register.
Request & headers
Every delivery is a single POST to your URL. The event name travels in
two places: the X-ReleasePad-Event header and the
event field of the JSON body. They are always identical. The header
exists so you can route the request to the right handler, or drop events you do not care about, before
parsing the body at all.
| Header | Value | Notes |
|---|---|---|
| X-ReleasePad-Event | post.created post.published post.deleted test | One value per request, chosen by the action that fired it. Equals the body's event field. |
| Content-Type | application/json | The body is UTF-8 JSON. |
| User-Agent | ReleasePad-Webhooks/1.0 | Handy for log filters and firewall rules. |
How the header is set, action by action:
| Action in ReleasePad | X-ReleasePad-Event |
|---|---|
| A post is saved for the first time, as a draft | post.created |
| A post is saved for the first time, already published | post.created, then post.published (two requests) |
A draft is published (Publish button, or draft: false through the API or MCP) | post.published |
| A post is deleted, draft or published | post.deleted |
| An admin clicks Send test in the dashboard | test |
POST /releasepad HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ReleasePad-Webhooks/1.0
X-ReleasePad-Event: post.published
Content-Length: 1042
{"id":"0c4337ea-49b3-488f-897a-f130d60ba748","event":"post.published","product_id":20,"created_at":"2026-10-06T22:28:21.548372Z","data":{...}}
There is no request signature and no authentication header. See Security for how to make sure only ReleasePad can call your endpoint.
Payload envelope
The body is the same five fields for every event. Only event and the
contents of data change.
| Field | Type | Description |
|---|---|---|
| id | string | A UUID that is unique to this delivery. Two requests about the same post have different ids. Store it to ignore duplicates. |
| event | string | post.created, post.published, post.deleted or test. Same value as the X-ReleasePad-Event header. |
| product_id | integer · null | The product the post belongs to. null only on a test event for a product without posts. |
| created_at | string | When this delivery was built, ISO 8601 in UTC. Not the post's own timestamps; those live inside data. |
| data | object · null | The post object as it was right after the action. null only on a test event for a product without posts. |
{
"id": "0c4337ea-49b3-488f-897a-f130d60ba748",
"event": "post.published",
"product_id": 20,
"created_at": "2026-10-06T22:28:21.548372Z",
"data": { ... }
}
The post object
data is the post exactly as the Management API returns it. The
body is the stored Markdown, never rendered HTML. Fields you will
usually branch on:
| Field | Type | Description |
|---|---|---|
| id | integer | The post id. Use it with Get a post if you ever need the current state again. |
| title, blurb, body | string | Title, the auto generated plain text summary, and the full Markdown body. |
| draft | boolean | true while the post is a draft. On post.published it is always false. |
| publish_at | string | When the post becomes (or became) visible, ISO 8601 UTC. A value in the future means the post is scheduled. |
| is_private, only_show_to_ids | boolean · string | Private posts and the list of user ids allowed to see them. |
| category | object · null | { id, name, background_color }. |
| view_counter, product_id, created_at, updated_at | various | Same meaning as in the API. |
The complete field list is documented once, in the API reference.
Events
Three events cover the life of a post, plus one you trigger by hand. Each one below lists exactly which actions fire it, which do not, and shows a real delivery.
post.created
A post was created
Fires once for every new post, the moment it is first saved, from any entry point: the web editor, the Management API, the MCP server, or the GitHub integration drafting release notes from your commits.
Fires
- A draft is saved for the first time (
data.draftistrue). - A post is created already published (
data.draftisfalse); apost.publishedrequest follows right after. - The GitHub integration creates a draft from new commits.
Does not fire
- Any later edit to the post.
- The sample posts seeded into a brand new product.
POST /releasepad
Content-Type: application/json
X-ReleasePad-Event: post.created
{
"id": "63a6c2b1-e4fd-4e2d-9f84-81357880e399",
"event": "post.created",
"product_id": 20,
"created_at": "2026-10-06T22:28:21.530481Z",
"data": {
"id": 2199,
"title": "Dark mode is finally here",
"blurb": "Toggle dark mode from any screen using the new switch in Settings → Appearance. ...",
"body": "## Dark mode\n\nToggle dark mode from any screen using the new switch in **Settings → Appearance**. ...",
"draft": true,
"is_private": false,
"only_show_to_ids": null,
"publish_at": "2026-10-06T22:28:21.444847Z",
"view_counter": 0,
"category": { "id": 64, "name": "New Feature", "background_color": "#dbeafe" },
"product_id": 20,
"created_at": "2026-10-06T22:28:21.455648Z",
"updated_at": "2026-10-06T22:28:21.455648Z"
}
}
post.published
A post went from draft to published
Fires every time a post's draft flag goes from true to false. It is
the event most integrations want: it marks the moment someone decided the release note is ready.
Scheduled posts. The event fires when the post is saved as published, whatever
its publish_at date. A post scheduled for next Tuesday fires post.published
today, with next Tuesday in data.publish_at. If your integration should act when the post
actually becomes visible, compare data.publish_at with the current time and delay your own
work accordingly.
Fires
Does not fire
- Editing the title, body, category or date of a post that is already published.
- Unpublishing (
{"draft": true}). There is nopost.unpublishedevent. - A scheduled post reaching its
publish_attime. The event already fired when it was scheduled.
POST /releasepad
Content-Type: application/json
X-ReleasePad-Event: post.published
{
"id": "0c4337ea-49b3-488f-897a-f130d60ba748",
"event": "post.published",
"product_id": 20,
"created_at": "2026-10-06T22:28:21.548372Z",
"data": {
"id": 2199,
"title": "Dark mode is finally here",
"blurb": "Toggle dark mode from any screen using the new switch in Settings → Appearance. ...",
"body": "## Dark mode\n\nToggle dark mode from any screen using the new switch in **Settings → Appearance**. ...",
"draft": false,
"is_private": false,
"only_show_to_ids": null,
"publish_at": "2026-10-06T22:28:21.444847Z",
"view_counter": 0,
"category": { "id": 64, "name": "New Feature", "background_color": "#dbeafe" },
"product_id": 20,
"created_at": "2026-10-06T22:28:21.455648Z",
"updated_at": "2026-10-06T22:28:21.536605Z"
}
}
The same post scheduled for later looks identical except for the date:
"publish_at": "2026-10-14T09:00:00.000000Z". Nothing else in the payload tells you it is
scheduled, so always read that field.
post.deleted
A post was deleted
Fires when a post is deleted from the editor, the
API or MCP. Deletes in ReleasePad
are soft: the post disappears from the dashboard, the widget, the changelog page and every API response,
so data is your last chance to read it. It carries the post exactly as it was right before
the delete, including its draft flag, so you can tell whether you are removing something
that was public or just cleaning up a draft.
Fires
- Deleting a published post (
data.draftisfalse). - Deleting a draft (
data.draftistrue). - Deleting a scheduled post that never became visible (
data.draftisfalse,data.publish_atis still in the future).
Does not fire
- Unpublishing a post. It is still there, just hidden.
- Deleting a category. Its posts are kept.
- Archiving a whole product. Its posts are not deleted one by one.
POST /releasepad
Content-Type: application/json
X-ReleasePad-Event: post.deleted
{
"id": "55c9b829-e822-4b2b-bce5-daaf1fe6d683",
"event": "post.deleted",
"product_id": 20,
"created_at": "2026-10-06T22:28:21.590919Z",
"data": {
"id": 2199,
"title": "Dark mode is finally here",
"blurb": "Toggle dark mode from any screen using the new switch in Settings → Appearance. ...",
"body": "## Dark mode\n\nToggle dark mode from any screen using the new switch in **Settings → Appearance**. ...",
"draft": false,
"is_private": false,
"only_show_to_ids": null,
"publish_at": "2026-10-06T22:28:21.444847Z",
"view_counter": 0,
"category": { "id": 64, "name": "New Feature", "background_color": "#dbeafe" },
"product_id": 20,
"created_at": "2026-10-06T22:28:21.455648Z",
"updated_at": "2026-10-06T22:28:21.585766Z"
}
}
test
A manual test from the dashboard
Sent only when an admin clicks Send test on the Webhooks page. It is delivered immediately and synchronously, and the dashboard shows the HTTP status your endpoint returned (or the connection error), so it doubles as a connectivity check. It ignores the endpoint's event selection and its paused state: a paused endpoint can still be tested.
data is the product's most recently created post, so your handler sees a realistic body. For
a product with no posts, data and product_id are null. Production
handlers should ignore it or just log it, and never act on it as if it were a real post event.
POST /releasepad
Content-Type: application/json
X-ReleasePad-Event: test
{
"id": "9d1f0b2a-3c4e-4f5a-8b6c-7d8e9f0a1b2c",
"event": "test",
"product_id": 20,
"created_at": "2026-10-06T22:27:35.630000Z",
"data": {
"id": 2193,
"title": "Dark mode is finally here",
"draft": false,
"publish_at": "2026-10-01T09:00:00.000000Z",
"category": { "id": 64, "name": "New Feature", "background_color": "#dbeafe" },
"product_id": 20,
"...": "the rest of the post object"
}
}
What fires when
The same rules, as a lookup table. "Two requests" means two separate deliveries, each with its own
id, sent one right after the other.
| You do this | ReleasePad sends | data.draft |
|---|---|---|
| Save a new draft | post.created | true |
Create a post already published (API draft: false, or Publish on a brand new post) | post.created, post.published (two requests) | false, false |
Create a post already published with a future publish_at | post.created, post.published (two requests) | false, false |
| Publish an existing draft | post.published | false |
| Edit a draft | nothing | · |
| Edit a published post (title, body, category, date) | nothing | · |
Unpublish a post (draft: true) | nothing | · |
| Publish it again | post.published | false |
A scheduled post reaches its publish_at time | nothing | · |
| Delete a published post | post.deleted | false |
| Delete a draft | post.deleted | true |
| Click Send test | test | latest post's value |
Delivery rules
- One attempt, no retries. Each event is sent once to each subscribed endpoint, in the background, a few milliseconds after the action. If your server is down or answers with an error, that delivery is lost; ReleasePad logs the failure but does not retry. If you need to recover, fetch the post again with the Management API.
- Any 2xx counts as delivered. Anything else, including redirects, is treated as a failure. Redirects are not followed.
- Timeouts. 5 seconds to connect and 10 seconds to receive a response. Answer first, work later.
- One request per endpoint. A product with three endpoints subscribed to
post.publishedgets three independent requests, each with its ownid. - Ordering is not guaranteed. When a post is created already published, the
post.createdandpost.publishedrequests are sent at the same time and may reach you in either order. Usedata.draftanddata.updated_at, not arrival order, to decide what to do. - Paused endpoints receive nothing except manual tests. Deliveries resume as soon as the endpoint is enabled again; events that happened while it was paused are not replayed.
- Plan changes. Webhooks are a paid feature. If an account goes back to trial, its endpoints stay configured but receive nothing until the account is paid again.
Handling webhooks
A receiver that follows four habits will never lose an event it could have kept.
Respond with 200 immediately
Store the payload or put it on a queue, answer, then do the real work. A handler that posts to Slack before responding risks the 10 second timeout.
Route on the header
Read X-ReleasePad-Event first and ignore
anything you do not recognise, including test. New events may be added later.
Dedupe on id
Keep the delivery ids you have processed and skip repeats. ReleasePad does not retry today, but a receiver that is safe to call twice is a receiver that will survive the day it does.
Key on data.id
Use the post id as the key in your own system so a later
post.published or post.deleted can find what an earlier
post.created made.
// Express receiver: announce published posts in Slack
app.post("/releasepad", express.json(), async (req, res) => {
const event = req.get("X-ReleasePad-Event");
res.sendStatus(200); // answer first
if (event !== "post.published") return; // created, deleted, test: ignore
const post = req.body.data;
if (new Date(post.publish_at) > new Date()) return; // scheduled: wait for it
await slack.chat.postMessage({
channel: "#releases",
text: `*${post.title}*\n${post.blurb}\nhttps://pro.releasepad.io/en/acme-app`
});
});
Security
Every delivery goes over HTTPS; http endpoints cannot be registered. Deliveries are not signed and carry no authentication header, so the URL itself is the credential. Treat it like one.
- Keep a valid certificate on the endpoint. ReleasePad verifies TLS like a browser would; an expired or self signed certificate makes the delivery fail.
- Put a secret in the URL and check it on every request, for example
https://hooks.example.com/releasepad/7f3c9a0b2e4dor a?token=query parameter. Reject requests without it. Rotate it by creating a new endpoint and removing the old one. - Validate the body. Only trust
dataafter checking thatproduct_idis one of your products. - Payloads contain whatever is in the post, including private posts
(
is_private: true). Do not forward private posts to public channels.
Signed deliveries (an HMAC header with a per endpoint secret) are on the roadmap. The URL secret pattern above will keep working once they ship.
Webhooks FAQ
Use post.published. It fires exactly when a draft becomes a published post, which is the moment the release note is ready. If you schedule posts, read data.publish_at and delay your announcement until that time, because the event fires when the post is saved, not when it becomes visible.
No. Each event is sent once to each endpoint. A timeout or a response outside the 2xx range is logged on our side and not retried. Keep your endpoint fast and available, and use the Management API to fetch a post again if you ever miss one.
Read the X-ReleasePad-Event header. Its value is post.created, post.published, post.deleted or test, and it always matches the event field inside the JSON body. Routing on the header lets you discard events you do not handle without parsing the body.
Not yet. Use an HTTPS URL that contains a secret path segment or query parameter and reject any request that does not carry it. Signed deliveries with an HMAC header are planned, and the URL secret will keep working when they arrive.
Yes. Create a Catch Hook or Webhook trigger in the tool, paste the URL it gives you as the endpoint in ReleasePad, and click Send test so the tool can learn the payload shape. Then filter on the event field to react to published posts only.
Expose your local server with a tunnel such as ngrok or Cloudflare Tunnel, which gives you an https URL, register that URL as the endpoint, and click Send test. The dashboard shows the HTTP status your server returned, and every create, publish or delete you make in the editor arrives on your machine within a second. Plain http URLs are not accepted.
Built and maintained by the ReleasePad team · Last updated
Need another event?
Tell us what you are wiring up. Signed deliveries, retries and a post.updated event are on the list; your use case decides the order.
Contact the team