Guide to Ordinal Webhooks: Features and Best Practices
Ordinal webhooks notify your systems when something happens in your Ordinal workspace: a post is published or fails to publish, someone requests an approval, a comment lands on a draft, a social profile needs reconnecting. If you're mirroring published content into a CRM or data warehouse, routing review requests into Slack, or alerting the social team before scheduled posts start failing, webhooks are how those systems find out in real time instead of polling the API.
This guide covers how Ordinal webhooks work, how to create them in the dashboard or through the API, how to authenticate deliveries that Ordinal does not sign, the gaps in what the platform documents about delivery, and the best practices for production.
What are Ordinal webhooks?
Ordinal is a social media content planning, approval, and scheduling platform for marketing teams. Teams use it to draft LinkedIn and X posts, group them into campaigns, run approval workflows, and leave comments on drafts before anything goes live. (It is unrelated to Bitcoin Ordinals.)
Ordinal webhooks are HTTP callbacks that notify your application when events occur in a workspace. When a subscribed event fires, Ordinal POSTs a JSON body to your endpoint. Every delivery uses the same envelope: a type field naming the event, a data object carrying the event-specific payload, and a createdAt ISO 8601 timestamp. The data object holds a single key whose name depends on the event family: profile, post, comment, approval, or invite.
Ordinal webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard (Settings > Integrations > Webhooks) or REST API (POST https://app.tryordinal.com/api/v1/webhooks) |
| Authentication | No signature, signing secret, or timestamp header; optional static custom headers set via the webhook's headers field |
| Envelope | { "type": ..., "data": ..., "createdAt": ... }, with one key under data per event family |
| Event types | 20 topics; each webhook subscribes to at least one via the topics array |
| Acknowledgement | Any 2xx status code |
| Event ID | None; no top-level event id and no delivery-id header |
| Management API | GET, POST, PATCH, and DELETE on /webhooks, authenticated with a workspace API key as a Bearer token |
Common events
Ordinal event names are the type values. Note the mixed separators: some names use underscores inside an otherwise dot-separated string.
| Event | Fires when |
|---|---|
post.created | A new post is created |
post.scheduled | A post is scheduled for publishing |
post.rescheduled | A post's scheduled time is changed |
post.published | A post is successfully published to a channel |
post.publish_failed | A post fails to publish (the reason is in data.post.error) |
post.content.edited | A post's content is edited (debounced per post, fires about 5 minutes after edits) |
post.archived | A post is moved to trash |
post.comment.created | A post-level comment is added |
post.inline_ | An inline comment is added, including each reply in a thread |
post.approval.requested | Someone requests approval for a post |
post.approval.approved | An approver grants approval for a post |
campaign.approval.requested | Someone requests approval for a campaign |
social_ | A connected profile needs reconnecting (for example, an expired token) |
invite.accepted | A user accepts a workspace invite |
Branch on the type field at the top level of the body, then read the matching key under data: data.post for post events, data.comment for comments, data.approval for both post and campaign approvals.
See Ordinal webhook payloads in action. Inspect and replay sample Ordinal webhook payloads in the Hookdeck Console — no account or setup required.
Setting up Ordinal webhooks
In the dashboard, go to Settings > Integrations > Webhooks, set a name and your endpoint URL, choose the event topics, and save. Ordinal does not document whether the dashboard form lets you set custom headers, and custom headers are the only way to authenticate deliveries. If the form has no field for them, add them through the API afterwards.
Via the API, POST to the webhooks endpoint with a name, URL, the topics you want, and a headers object carrying a secret you generated yourself (for example with openssl rand -hex 32):
curl -X POST https://app.tryordinal.com/api/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "CRM Sync",
"url": "https://your-app.com/webhooks/ordinal",
"topics": ["post.published", "post.publish_failed"],
"headers": { "X-Webhook-Secret": "YOUR_GENERATED_SECRET" }
}'
The API key is a workspace key you send to Ordinal; it never arrives on a delivery and is not a webhook secret. The header name in headers is your choice, not an Ordinal convention. The create response returns only id, name, url, topics, and createdAt, so call GET /webhooks/{id} to confirm the headers were saved. To add headers to a webhook created in the dashboard, or to rotate the secret, send PATCH /webhooks/{id} with the full headers object.
Ordinal has no test or ping event. To exercise an integration, perform the real action in the app: create a post, request an approval, or add a comment.
For local development, use the Hookdeck CLI: hookdeck listen 3000 ordinal --path /webhooks/ordinal gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required. Register the generated URL as your webhook endpoint.
Securing Ordinal webhooks
Ordinal does not sign webhook deliveries. There is no signature header, signing secret, timestamp, or HMAC, so there is nothing to verify cryptographically. Any code that computes an HMAC for Ordinal will reject every genuine delivery.
Authentication comes from the static header you set in the webhook's headers field. Ordinal adds that header to every delivery, and your handler compares it against the stored secret. Compare in constant time rather than with ===, reject the request if the secret is not configured instead of accepting everything, and check the header's type and length before comparing, since crypto.timingSafeEqual throws on buffers of different lengths. Because nothing is signed over the body, parsing JSON before the check is fine:
const crypto = require("crypto");
// The header name you chose in the webhook's `headers` field; Node lowercases it
const SECRET_HEADER = "x-webhook-secret";
function verifyOrdinalSecret(headers, expected) {
if (!expected) return false; // fail closed on misconfiguration
const provided = headers[SECRET_HEADER];
if (typeof provided !== "string") return false;
const a = Buffer.from(provided, "utf8");
const b = Buffer.from(expected, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhooks/ordinal", express.json(), (req, res) => {
const secret = process.env.ORDINAL_WEBHOOK_SECRET;
if (!secret) {
return res.status(500).send("Webhook secret not configured");
}
if (!verifyOrdinalSecret(req.headers, secret)) {
return res.status(401).send("Unauthorized");
}
const event = req.body;
switch (event.type) {
case "post.published":
// data.post.postUrl is the live link on the channel (may be null)
break;
case "post.publish_failed":
// data.post.error carries the reason
break;
case "post.approval.requested":
// data.approval, not data.post
break;
}
res.status(200).send("OK");
});
Note what this check proves: the caller knows your secret. It does not prove the body is unmodified or that the request is not a replay, because the value is identical on every delivery.
Make Ordinal webhooks production-ready. Hookdeck Event Gateway checks your secret header at the edge, deduplicates, and durably queues every event with replay for anything that fails.
Ordinal webhook limitations and pain points
No signature, only a static header
The Problem: Ordinal deliveries carry no signature. Unless you configure a custom header, a delivery carries no credential at all, and anyone who learns your endpoint URL can POST a fabricated post.published or invite.accepted. Even with a header configured, a single captured request exposes the secret for every future delivery.
Why It Happens: Ordinal issues no signing secret. The only authentication mechanism it documents is the optional headers field, which attaches the same static values to every request.
Workarounds:
- Set a long random secret in
headerson every webhook, and reject requests without it. - Serve the endpoint over HTTPS only, and never log the header value.
- Rotate the secret with
PATCH /webhooks/{id}, accepting both old and new values in your handler during the switch.
How Hookdeck Can Help: Hookdeck's Ordinal source type supports optional API Key verification (a header name and value) and Basic Auth. Configure the API Key check with the same header name and value you set in Ordinal's headers, and Hookdeck rejects requests without it before they reach your handler. Basic Auth depends on Ordinal forwarding a custom Authorization header, which its docs don't confirm, so the custom header is the safer choice. Hookdeck cannot perform HMAC verification for Ordinal, because no body signature exists. See sources for setup.
Delivery behavior is undocumented
The Problem: Ordinal documents that your endpoint should return a 2xx to acknowledge receipt, and nothing more. The retry policy, number of attempts, request timeout, and ordering guarantees are not specified, so you cannot plan around them.
Why It Happens: The webhook docs cover the envelope, event types, and management API, but not delivery semantics.
Workarounds:
- Return 200 immediately and defer processing, so slow work never risks a failed delivery.
- Assume a failed delivery may not be retried, and alert on handler errors quickly.
- Reconcile against the Ordinal API periodically for state that must not drift, such as published posts.
How Hookdeck Can Help: Hookdeck ingests and durably queues events ahead of your endpoint, with automatic retries on a schedule you control, Issues that alert you when deliveries fail, and replay for any event. Your own outages stop depending on retry behavior Ordinal doesn't document.
No event ID to deduplicate on
The Problem: The envelope has no top-level event id, and Ordinal documents no delivery-id header. If the same event arrives twice, nothing in the request marks it as a repeat.
Why It Happens: The envelope is exactly type, data, and createdAt. The ids inside data identify the post, comment, or approval, not the event.
Workarounds:
- Derive a key from
type, the resource id insidedata, andcreatedAt, and store it on processing. - Make writes idempotent (upserts keyed on the post id) so a repeat is harmless even if the key check misses.
How Hookdeck Can Help: Deduplication can drop repeated deliveries based on the fields you choose, such as type, createdAt, and the resource id, before they reach your handler.
Payload shape varies by event family
The Problem: The key under data changes with the event. Reading data.post on post.comment.created or post.approval.requested returns undefined, because those events use data.comment and data.approval. Field names also differ between related events: post.published has a singular channel and publishedBy, while post.created has a channels array and post.publish_failed has createdBy.
Why It Happens: Ordinal groups events into families (profile, post, comment, approval, invite), and each family has its own payload object.
Workarounds:
- Map each
typeto itsdatakey in one place and read through that mapping. - Use the exact event strings:
post.publish_failed, notpost.publish.failed. - Subscribe each webhook only to the topics you handle.
How Hookdeck Can Help: Register a single Hookdeck source URL for all your topics, then use filters to route events by type to different destinations. Transformations can reshape each family into a consistent structure before delivery.
Best practices
Set a secret header on every webhook
A webhook without headers arrives with no credential. Generate a random secret, set it when you create the webhook (or PATCH it onto a dashboard-created one), confirm it with GET /webhooks/{id}, and store it in an environment variable.
Compare in constant time and fail closed
Use crypto.timingSafeEqual with a length check first, return 401 on a missing or wrong header, and reject everything if the secret is unset. A handler that accepts all requests when the environment variable is missing turns a bad deploy into an open endpoint.
Acknowledge fast, process asynchronously
Ordinal doesn't document its timeout or retry policy, so treat every slow response as a potential lost event. Return 200 as soon as the header checks out and hand the event to a queue or background job. See why to process webhooks asynchronously.
Make handlers idempotent
Without an event id, build your own key from type, the resource id, and createdAt, and make processing safe to repeat so a duplicate post.published never creates two CRM records. See our guide to webhook idempotency.
Treat edits as current state
post.content.edited is debounced per post and fires about 5 minutes after edits, with the latest content for all channels. Overwrite your copy with what it carries rather than trying to apply it as a diff, and don't expect one event per edit.
Conclusion
Ordinal webhooks cover the lifecycle of social content in a workspace (posts, approvals, comments, connected profiles, and invites) in a consistent type, data, and createdAt envelope. The key decision happens at creation time: Ordinal signs nothing, so the static header you put in headers is your endpoint's only protection against forged requests.
Ordinal documents no retry policy or timeout and sends no event id, so a production integration should acknowledge fast, deduplicate on a derived key, and not depend on redelivery. Hookdeck Event Gateway puts header verification, deduplication, and a durable queue with replay in front of your endpoint, so your handlers only ever process authenticated events.
Get started with Hookdeck for free and handle Ordinal webhooks reliably in minutes.