What connecting a webhook does#

A CMS connection is the last hop of the PilotDeck pipeline: once you approve a draft, PilotDeck delivers it for you. With Webhook, the destination is a URL you own. PilotDeck sends the article as a signed JSON POST, and your side decides how to store and render it. The example below uses a PilotDeck site named pilotdeck-testwebsite.

Works with any stack

Headless sites, automations, serverless functions and custom back ends can all receive the same payload. No platform API is involved.

Signed deliveries

Every request carries an HMAC-SHA256 signature of the raw body, so your endpoint can prove the request came from PilotDeck.

SEO included

The payload carries the meta title, meta description, FAQ and ready-made JSON-LD, so you can place them in your page head.

Using Framer? Framer has no direct connector. In PilotDeck the Framer tile reads "Through the Webhook connector" and opens this same form. Follow this guide, and point the endpoint at a service that writes into your Framer project. PilotDeck only sends the POST, so what the receiver does with it is up to you.

Before you start#

You needDetails
An endpoint you controlAn HTTPS URL that accepts POST requests with a JSON body, such as https://example.com/hooks/pilotdeck. It must answer with a 2xx status.
Code to receive the postA handler that verifies the signature, then creates or updates the article in your system, keyed by post.slug or post.id.
A PilotDeck loginPermission to change integrations for the site. Without it the Connect button is unavailable.
The signing secret is shown once. PilotDeck creates the secret when you connect and shows it a single time. Store it in your receiver's secret store straight away. Never paste it into chat, tickets or screenshots. Every secret in this guide is a placeholder.

Overview#

  1. 1Build or choose an HTTPS endpoint that accepts a JSON POSTYou
  2. 2Open Settings > Integrations in PilotDeck and pick WebhookYou
  3. 3Enter the Webhook URL and choose a publish modeYou
  4. 4Connect. PilotDeck sends a signed ping and creates the signing secretPilotDeck
  5. 5Save the signing secret in your receiverYou
  6. 6Send a test draft and check your endpoint received itVerify
1

Prepare your endpoint#

You

Create a route that accepts POST with a JSON body and returns a 2xx status. Keep the raw request body available, because the signature is computed over the exact bytes PilotDeck sends.

While you are connecting, handle the ping event first (see The ping event). It is sent before you have seen the signing secret, so your endpoint should answer it with 200 and leave signature checks for post.publish deliveries.

Optionally, answer a post.publish request with a JSON body containing id (or remoteId) and url (or remoteUrl). PilotDeck stores them as the remote ID and link of the delivered article. An empty or non-JSON body is fine.

2

Open Integrations in PilotDeck#

You

In PilotDeck, make sure the correct site is selected in the top-left site switcher (here, pilotdeck-testwebsite). Go to Settings > Integrations and find the CMS Connection card. With nothing connected it reads "No CMS connected".

Click Webhook (Signed JSON POST to any endpoint). A "How to connect Webhook" panel opens next to the form. It lists the setup steps, the sample payload under What we POST to your endpoint, and the Request headers.

Settings > Integrations > CMS Connection with Webhook selected: the setup notes on the left, the Webhook URL and Publish mode on the right.
3

Enter the URL and choose a mode#

You
  1. Type your endpoint into Webhook URL (required), for example https://example.com/hooks/pilotdeck. Include https:// and the full domain.
  2. Pick a Publish mode. See Publish mode for what it means for a webhook.
  3. There is no Category field for Webhook. The category is not sent.
4

Test, then connect#

PilotDeck

Click Test connection first. PilotDeck sends a signed ping request to your URL and saves nothing. You should see "Looks good, credentials work." If the endpoint does not answer with a 2xx status, you see an error instead.

A passing test. The URL shown here is a placeholder; yours is the address of your own endpoint.

Then click Connect. PilotDeck sends one more ping and only saves the connection if your endpoint accepts it. If it does not, the connection is not created and you see "That endpoint did not accept the test delivery" with the HTTP status your endpoint returned. On success a Save this signing secret, it won't be shown again box appears with the secret, and the card shows an ACTIVE badge.

Connected. The lavender box shows the signing secret once; it is masked in this screenshot.
Copy the secret now. The secret is not shown again after you dismiss the box. If you lose it, disconnect and connect again to get a new one, then update your receiver.

The secret is a string that starts with the prefix whsec_ followed by random hex characters. Store it as <your-signing-secret> in your receiver's environment.

Connecting a CMS disables any other active CMS connection on the same site, because a site has one active CMS at a time. PilotDeck asks you to confirm which connection will be disabled.
5

Verify with a test post#

Verify

On the connected card click Send test draft. PilotDeck sends a bundled sample article to your endpoint as a post.publish event. The card shows "Test draft created." when your endpoint answers with a 2xx status, and a link if your response included url or remoteUrl. Check that your system stored the article.

"Test draft created." appears under the buttons.
What a test receiver shows: a POST with the x-pilotdeck-event, x-pilotdeck-delivery and x-pilotdeck-signature headers and the JSON body. Receiver ID and server address are masked.
Send test live sends the same sample article. For a webhook it makes no difference to the request, because the endpoint receives the same event either way (see Publish mode). Test items can be deleted on your side afterwards.
Done. Approved content for this site now goes to your endpoint as a signed JSON POST.

What PilotDeck sends#

Each article is one POST with Content-Type: application/json and the user agent Pilotdeck-Connector/1.0. The body is a JSON envelope. Values below are placeholders. Optional author fields are left out when empty.

post.publish body (placeholder values)
{
  "event": "post.publish",
  "deliveryId": "<unique-delivery-id>",
  "post": {
    "id": "<post-id>",
    "title": "Example article title",
    "slug": "example-article-title",
    "html": "<h2>Example heading</h2><p>Article body as HTML.</p>",
    "metaTitle": "Example article title | Example",
    "metaDescription": "A short description for search results.",
    "tags": [],
    "categories": [],
    "faq": [
      { "question": "Example question?", "answer": "Example answer." }
    ],
    "conclusion": "",
    "author": {
      "name": "Example Author",
      "title": "Example role",
      "bio": "Short bio.",
      "url": "https://example.com/authors/example",
      "image": "https://example.com/authors/example.png",
      "slug": "example"
    },
    "featuredImage": "https://example.com/images/hero.png",
    "ogImage": "https://example.com/images/hero.png",
    "images": [],
    "status": "draft",
    "publishedAt": null,
    "updatedAt": "2026-01-01T00:00:00.000Z",
    "canonical": null
  },
  "jsonLd": [
    { "@context": "https://schema.org", "@type": "FAQPage" },
    { "@context": "https://schema.org", "@type": "BlogPosting" }
  ],
  "sentAt": "2026-01-01T00:00:00.000Z"
}

The jsonLd entries are shortened above. In a real delivery they are complete schema.org objects: an FAQPage when the article has FAQ entries, and a BlogPosting that includes the author when there is one.

FieldMeaning
eventpost.publish for an article, ping for the connection test.
deliveryIdA unique ID per delivery. Use it to make your handler idempotent.
post.htmlThe article body as HTML.
post.slug, post.idStable keys to create or update the article on your side.
post.metaTitle, post.metaDescription, post.canonicalMap these into your page head. canonical is null unless a value exists.
post.faqStructured question and answer pairs. Render them as a visible FAQ section.
post.authorThe full author object, present when the site has an author. Your receiver renders it.
post.featuredImage, post.ogImageImage URLs. PilotDeck does not upload media, so rehost them if you need to.
jsonLdReady-made structured data. Embed each entry in its own <script type="application/ld+json"> tag.
sentAtWhen the request was created, as an ISO timestamp.
In live deliveries tags, categories, faq and conclusion can be empty, because they only carry content when the draft has it. Build your receiver to accept empty values for any optional field.

Request headers and signature#

HeaderValue
X-Pilotdeck-EventThe event type: post.publish, or ping for the connection test.
X-Pilotdeck-DeliveryA unique delivery ID, the same value as deliveryId in the body.
X-Pilotdeck-Signaturesha256= followed by the lowercase hex HMAC-SHA256 of the raw request body, keyed with your signing secret.

To verify a request, compute the HMAC-SHA256 of the exact raw body using your signing secret, prefix it with sha256=, and compare it to the header with a constant-time comparison. Verify the raw bytes before parsing the JSON, because re-serialising the body can change it.

Node.js verification example
const crypto = require("crypto");

function isValidSignature(rawBody, header, secret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(header || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// rawBody: the unparsed request body
// header: the X-Pilotdeck-Signature value
// secret: process.env.PILOTDECK_SIGNING_SECRET (<your-signing-secret>)
Reject a post.publish request with a missing or wrong signature using 401. Do not store its content.

The ping event#

Test connection and Connect both send a ping. It has the same envelope as an article, with event set to ping, post set to null and jsonLd as an empty list. It is signed like any other request.

PilotDeck creates the signing secret for each attempt, and you only see the final one after Connect succeeds. So the ping signature cannot be checked against a secret you already hold. Answer ping with a 2xx status without rejecting it on signature, then enforce signatures on post.publish once you have saved the secret.

Timeouts and failures#

SituationWhat PilotDeck does
Your endpoint answers 2xxThe delivery counts as successful. The card stays ACTIVE.
Your endpoint is slowPilotDeck waits up to 20 seconds for each request, then treats it as a network failure. A whole deploy job is capped at 2 minutes.
Your endpoint answers 400 or 422Treated as a validation failure. The delivery is marked failed.
Your endpoint answers 401Treated as an authentication failure. The connection is marked needs_reauth.
Your endpoint answers 403, 404 or 413Treated as permission denied, not found or payload too large. The connection is marked error.
Your endpoint answers 429 or 5xx, or cannot be reachedTreated as a retryable failure (rate limited, server error or network). The connection is marked error.
The URL is not validThe form tells you to include https:// and the full domain.

Each delivery is a single attempt. The webhook adapter does not retry on its own, so make your endpoint fast, acknowledge quickly, and do heavy work after answering. Because a delivery can be sent more than once if a deploy is run again, treat deliveryId and post.slug as keys and make your handler idempotent.

Publish mode#

ModeWhat it is
Draft only (default)The CMS setting that tells a connector to create items as drafts instead of publishing them.
Publish liveThe CMS setting that tells a connector to publish items straight away.

The webhook connector does not read this setting. It cannot create a draft or schedule anything on your side, because it only delivers JSON. A delivery looks the same in either mode, and what happens next is decided by your receiver. If you want a review step, hold the article in your own system until someone approves it.

The Publish mode here is separate from PilotDeck's site publish mode (clone, CMS or copy) and from draft handling (review or auto). A site on the CMS publish mode with no connected CMS is not ready to publish. Check that the card shows ACTIVE. Switching modes on a connected card asks you to confirm.

Disconnecting#

On the connected card click Disconnect and confirm. Approved content stops being delivered right away. To reconnect, repeat steps 2 to 4. You receive a new signing secret, so update your receiver with it. Remove the old secret from your receiver when you disconnect.

Troubleshooting#

"Test connection" fails

Check that the URL starts with https://, is reachable from the public internet and answers POST with a 2xx status. A 401 or 403 from your endpoint on the ping usually means it is rejecting the request on signature. Let ping through, as described in The ping event.

Connect says the endpoint did not accept the test delivery

Your endpoint returned a non-2xx status. The message includes the HTTP status. Fix the handler, then click Connect again.

The signature never matches

Compute the HMAC over the raw body bytes, not over re-serialised JSON. Use the full sha256= prefix when comparing, and make sure you saved the secret from the box shown after Connect.

I lost the signing secret

It is shown only once. Disconnect, connect again to get a new secret, then update your receiver.

The same article arrives twice

Deliveries are single attempts, but a deploy can be run again. Use deliveryId or post.slug as an idempotency key and update instead of creating a duplicate.

How do I use this with Framer?

Pick the Framer tile, which opens this Webhook form. Point Webhook URL at a service you run that receives the POST and writes the article into your Framer project.

There is no Category field

That is expected. Webhook deliveries do not carry a category setting, so categories arrives empty. Your receiver decides where the article goes.