Introduction
The NicePublish API lets you do from code what you can do in the dashboard: list the Facebook Pages connected to your account, organise them into clients, send clients a hosted link to connect their own Pages, upload media, and create, schedule, edit and delete posts.
- Base URL:
https://nicepublish.com/api/v1 - Requests and responses are JSON (
Content-Type: application/json), except media uploads which usemultipart/form-data. - Timestamps are ISO 8601 strings in UTC, e.g.
2026-10-06T09:00:00+00:00. - Successful responses wrap the result in a
datakey. - The API is available on the Growth and Scale plans (and during the free trial). Other plans receive
402 plan_required.
Authentication
Create an API key in Dashboard → Developers. Keys start with np_live_ and are shown only once, so store them securely (for example in an environment variable). Send the key as a bearer token on every request:
curl https://nicepublish.com/api/v1/me \
-H "Authorization: Bearer np_live_your_api_key" \
-H "Accept: application/json"
<?php
// composer require guzzlehttp/guzzle
$api = new GuzzleHttp\Client([
'base_uri' => 'https://nicepublish.com/api/v1/',
'headers' => [
'Authorization' => 'Bearer ' . getenv('NICEPUBLISH_API_KEY'),
'Accept' => 'application/json',
],
]);
$me = json_decode((string) $api->get('me')->getBody(), true)['data'];
// Node.js 18+ (built-in fetch)
const res = await fetch('https://nicepublish.com/api/v1/me', {
headers: {
Authorization: `Bearer ${process.env.NICEPUBLISH_API_KEY}`,
Accept: 'application/json',
},
});
const { data: me } = await res.json();
Keys act on behalf of your whole NicePublish account. Never put them in browser or mobile app code. You can revoke a key at any time from the same page; requests with a missing, invalid or revoked key receive 401 unauthenticated.
Rate limits
Each API key can make 120 requests per minute. Responses include the standard X-RateLimit-Limit and X-RateLimit-Remaining headers. When you exceed the limit you receive 429 rate_limited with a Retry-After header (in seconds) — wait that long before retrying.
Separately from the API rate limit, every plan has a daily posting limit per Page, and identical content to the same Page within 24 hours is rejected. These protect your Pages from spam-like activity. See Create posts.
Errors
Errors use conventional HTTP status codes and always have the same JSON shape. fields is present for validation errors and maps each invalid field to a list of messages.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{
"error": {
"code": "validation_failed",
"message": "Facebook posts can have either a link preview or media, not both.",
"fields": {
"link": ["Facebook posts can have either a link preview or media, not both. Put the link in the text or the first comment instead."]
}
}
}
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthenticated | Missing, invalid or revoked API key. |
| 402 | plan_required | Your plan doesn't include API access, or your trial/plan has ended. |
| 403 | account_suspended | The account has been suspended. Contact support. |
| 404 | not_found | The resource doesn't exist or doesn't belong to your account. |
| 422 | validation_failed | The request was understood but a field is invalid or a rule (limits, duplicates) was not met. See fields. |
| 429 | rate_limited | Too many requests. Retry after the Retry-After header. |
Unexpected server errors return 5xx. They are rare; retry with exponential backoff, and use an idempotency key when retrying POST /posts.
Idempotency
Network problems happen. To retry POST /posts safely without creating duplicate posts, send an Idempotency-Key header with a unique value (a UUID works well) that you generate once per logical post:
curl -X POST https://nicepublish.com/api/v1/posts \
-H "Authorization: Bearer np_live_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4f9c2b1e-8a7d-4e55-b2c0-1d9e6f3a7b21" \
-d '{"page_ids":[12],"message":"Hello from our app"}'
- Same key and same body → you receive the posts created by the original request. Nothing new is created or published.
- Same key with a different body →
422 validation_failed(fieldidempotency_key). - Keys are scoped to your account.
Independently of idempotency keys, NicePublish claims every post atomically before publishing it, so a single post is never sent to Facebook twice by our workers.
Pagination
GET /posts is paginated. Use page and per_page (up to 100) query parameters; the response includes a meta object:
{
"data": [ { "id": 981, "status": "published", "...": "..." } ],
"meta": {
"current_page": 1,
"last_page": 4,
"per_page": 25,
"total": 92
}
}
GET /pages and GET /clients return all matching records in data; use their filters to narrow results.
Objects
The Page object
A Facebook Page connected to your account.
-
idinteger - NicePublish Page ID. Use this in
page_idswhen creating posts. -
facebook_page_idstring - The Page's ID on Facebook.
-
namestring - Page name.
-
usernamestring|null - The Page's Facebook username, if it has one.
-
categorystring|null - Page category as reported by Facebook.
-
picture_urlstring|null - Profile picture URL. Facebook CDN URLs can expire; refresh by fetching the Page again.
-
followers_countinteger|null - Follower count at the last refresh.
-
statusstring activeorneeds_reauth. A Page needs reconnecting when its access was removed, expired or changed in Facebook. You can't publish to it until it is reconnected.-
client_idinteger|null - The client this Page belongs to, if any.
-
capabilitiesobject - What the granted permissions allow:
publish,first_comment(posting comments as the Page) andinsights(reading analytics). Each is a boolean. -
connected_atstring - ISO 8601 time the Page was connected.
{
"id": 12,
"facebook_page_id": "104857392011223",
"name": "Green Leaf Café",
"username": "greenleafcafe",
"category": "Coffee shop",
"picture_url": "https://scontent.xx.fbcdn.net/...",
"followers_count": 4210,
"status": "active",
"client_id": 3,
"capabilities": {
"publish": true,
"first_comment": true,
"insights": true
},
"connected_at": "2026-09-14T10:22:31+00:00"
}
The Client object
A client groups Pages that belong to one of your customers. Clients can connect their own Pages through a connect link without a NicePublish account.
-
idinteger - Client ID.
-
namestring - Display name.
-
external_idstring|null - Your own identifier for this customer (e.g. the user or account ID in your system). Useful for lookups.
-
pages_countinteger - Number of Pages connected for this client.
-
created_atstring - ISO 8601 creation time.
{
"id": 3,
"name": "Green Leaf Café",
"external_id": "cust_8812",
"pages_count": 1,
"created_at": "2026-09-14T10:15:02+00:00"
}
The Post object
One post on one Page. Creating a post for several Pages returns one Post per Page, sharing a batch_id.
-
idinteger - Post ID.
-
batch_idstring - Shared by all posts created in the same request (one per Page).
-
page_idinteger - NicePublish Page ID.
-
facebook_page_idstring - The Page's ID on Facebook.
-
statusstring - See post statuses.
-
typestring text,link,photo,albumorvideo— derived from the content.-
messagestring|null - Post text.
-
linkstring|null - Link shown as a preview card.
-
mediaarray - Attached media items:
{"type": "image"|"video", "url": "…", "media_id": 5}(media_idonly for uploaded files). -
first_commentstring|null - Comment posted as the Page right after publishing.
-
first_comment_idstring|null - Facebook ID of the first comment once posted.
-
first_comment_errorstring|null - Why the first comment could not be posted, if it failed. The post itself is still published.
-
scheduled_atstring|null - ISO 8601 time the post is (or was) scheduled for.
-
published_atstring|null - ISO 8601 time Facebook accepted the post.
-
facebook_post_idstring|null - The post's ID on Facebook once published.
-
permalinkstring|null - Public URL of the post on Facebook.
-
errorstring|null - Human-readable reason when
statusisfailed. -
metricsobject|null - Latest engagement:
reactions,comments,shares, plus insight metrics such aspost_media_view,post_total_media_view_unique,post_clicksandpost_video_viewswhen available. Metric names follow Meta's and can change when Meta updates its API.nulluntil the first sync. -
metrics_synced_atstring|null - When metrics were last refreshed.
-
sourcestring web(created in the dashboard) orapi.-
created_atstring - ISO 8601 creation time.
Post statuses
- draft
- Saved but not scheduled. Created in the dashboard; it won't be published until scheduled.
- scheduled
- Waiting for its scheduled time (or about to be published now).
- publishing
- Being sent to Facebook right now.
- published
- Live on Facebook. facebook_post_id and permalink are set.
- failed
- Facebook rejected the post or it could not be sent. See error. You can edit and reschedule it.
- canceled
- Canceled before publishing.
- unknown
- Facebook timed out, so we can't yet tell whether the post went live. We check the Page to verify and update the status. An unknown post is never re-sent automatically — that is how we avoid double posts.
{
"id": 981,
"batch_id": "6f1c2a9e-3b7d-4c1e-9a55-0e2f7d8b1c44",
"page_id": 12,
"facebook_page_id": "104857392011223",
"status": "published",
"type": "photo",
"message": "Our autumn menu starts Monday.",
"link": null,
"media": [
{ "type": "image", "url": "https://nicepublish.com/m/Xk2...a9.jpg", "media_id": 5 }
],
"first_comment": "Full menu: https://example.com/menu",
"first_comment_id": "122101234567_88231",
"first_comment_error": null,
"scheduled_at": "2026-10-06T09:00:00+00:00",
"published_at": "2026-10-06T09:00:04+00:00",
"facebook_post_id": "104857392011223_122101234567",
"permalink": "https://www.facebook.com/104857392011223/posts/122101234567",
"error": null,
"metrics": {
"reactions": 42,
"comments": 7,
"shares": 3,
"post_media_view": 1830,
"post_total_media_view_unique": 1210,
"post_clicks": 64
},
"metrics_synced_at": "2026-10-06T15:00:12+00:00",
"source": "api",
"created_at": "2026-10-03T08:41:19+00:00"
}
The Media object
A file you uploaded with POST /media. Reference it in a post with {"media_id": 5}.
-
idinteger - Media ID.
-
typestring imageorvideo.-
urlstring - Public URL used to hand the file to Facebook.
-
mimestring - MIME type, e.g.
image/jpeg. -
sizeinteger - Size in bytes.
-
created_atstring - ISO 8601 upload time.
{
"id": 5,
"type": "image",
"url": "https://nicepublish.com/m/Xk2...a9.jpg",
"mime": "image/jpeg",
"size": 482113,
"created_at": "2026-10-03T08:40:55+00:00"
}
Uploaded files are deleted automatically 30 days after they are no longer needed by any scheduled post.
Endpoints
Account
Retrieve your account
GET /me
Returns the account the key belongs to, its current plan, plan limits and current usage. Handy as a connectivity check.
{
"data": {
"id": 7,
"name": "Jane Doe",
"email": "[email protected]",
"timezone": "Europe/London",
"plan": {
"slug": "growth",
"name": "Growth"
},
"on_trial": false,
"trial_ends_at": null,
"limits": {
"max_pages": 15,
"max_clients": 10,
"posts_per_page_per_day": 50
},
"usage": {
"pages": 4,
"clients": 2
}
}
}
Pages
Pages are connected through Facebook Login — in the dashboard or via a connect link. The API can list, inspect and disconnect them.
List Pages
GET /pages
Query parameters
-
client_idinteger optional - Only Pages belonging to this client.
-
statusstring optional activeorneeds_reauth.
curl "https://nicepublish.com/api/v1/pages?client_id=3&status=active" \
-H "Authorization: Bearer np_live_your_api_key"
Returns {"data": [Page, …]}.
Retrieve a Page
GET /pages/{id}
Returns {"data": Page}.
Disconnect a Page
DELETE /pages/{id}
Disconnects the Page: its access token is deleted immediately and its scheduled posts are canceled. Posts already published on Facebook are not removed from Facebook. Sends a page.disconnected webhook. Returns {"deleted": true}.
Page insights
GET /pages/{id}/insights
Daily Page-level metrics stored by NicePublish. Requires the Page's insights capability.
Query parameters
-
sincedate optional - First day,
YYYY-MM-DD. -
untildate optional - Last day,
YYYY-MM-DD.
curl "https://nicepublish.com/api/v1/pages/12/insights?since=2026-10-01&until=2026-10-07" \
-H "Authorization: Bearer np_live_your_api_key"
{
"data": {
"page_id": 12,
"followers_count": 4203,
"since": "2026-10-01",
"until": "2026-10-07",
"synced_at": "2026-10-07T09:00:12+00:00",
"metrics": {
"page_media_view": [
{ "date": "2026-10-01", "value": 123 },
{ "date": "2026-10-02", "value": 148 }
],
"page_follows": [
{ "date": "2026-10-01", "value": 4198 },
{ "date": "2026-10-02", "value": 4203 }
]
}
}
}
Metrics typically include page_media_view (views), page_total_media_view_unique (reach), page_post_engagements, page_follows, page_daily_follows_unique, page_daily_unfollows_unique and page_views_total. Meta occasionally renames or retires metrics; a metric Facebook no longer provides is simply omitted.
Clients
The number of clients you can create depends on your plan (limits.max_clients in GET /me).
List clients
GET /clients
Query parameters
-
external_idstring optional - Find the client with this external ID.
Returns {"data": [Client, …]}.
Create a client
POST /clients
Body
-
namestring required - Display name, e.g. your customer's business name.
-
external_idstring optional - Your own ID for this customer.
curl -X POST https://nicepublish.com/api/v1/clients \
-H "Authorization: Bearer np_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name":"Green Leaf Café","external_id":"cust_8812"}'
Returns 201 with {"data": Client}.
Retrieve a client
GET /clients/{id}
Returns {"data": Client}.
Update a client
PATCH /clients/{id}
Send any of name and external_id. Returns {"data": Client}.
Delete a client
DELETE /clients/{id}
Deletes the client and disconnects all of its Pages (tokens deleted, scheduled posts canceled). A page.disconnected webhook is sent for each Page. Returns {"deleted": true}.
Connect links
A connect link is a secure, hosted, one-time page where your customer connects their own Facebook Pages to a client in your account. They don't need a NicePublish account, and you never handle their Facebook credentials.
Create a connect link
POST /clients/{id}/connect-links
Body
-
redirect_urlstring optional - Where to send the customer's browser when they finish or cancel. Must be an absolute URL. Without it, they see a NicePublish confirmation page.
-
statestring optional - Any value you want echoed back on the redirect, e.g. your user ID or a CSRF token.
-
expires_in_hoursinteger optional - Link lifetime, 1–168 hours. Default 72.
curl -X POST https://nicepublish.com/api/v1/clients/3/connect-links \
-H "Authorization: Bearer np_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"redirect_url": "https://app.example.com/integrations/facebook/done",
"state": "user_551",
"expires_in_hours": 72
}'
HTTP/1.1 201 Created
{
"data": {
"url": "https://nicepublish.com/connect/k3J9...Qw",
"expires_at": "2026-10-06T08:41:19+00:00"
}
}
The flow
- Send your customer to
data.url(a link, button or redirect). - They see who is asking for access, sign in with Facebook, review the permissions and choose which of their Pages to connect.
- Their browser returns to your
redirect_urlwith query parameters:
https://app.example.com/integrations/facebook/done?status=connected&client_id=3&page_ids=12,13&state=user_551
https://app.example.com/integrations/facebook/done?status=canceled&client_id=3&state=user_551
status—connectedorcanceled.client_id— the client the Pages were added to.page_ids— comma-separated NicePublish Page IDs (only when connected).state— the value you supplied, if any. Check it matches what you expect.
A page.connected webhook is also sent for each Page, so you don't have to rely on the browser redirect. Treat the redirect as a hint and confirm via the webhook or GET /pages?client_id=….
Media
Upload a file
POST /media
Upload a photo or video as multipart/form-data with a single file field, then reference it in a post with {"media_id": …}. Alternatively, pass a public https URL directly in the post's media array.
- Images: JPG, PNG, GIF or WebP, up to 10 MB.
- Videos: MP4 or MOV, up to 100 MB.
curl -X POST https://nicepublish.com/api/v1/media \
-H "Authorization: Bearer np_live_your_api_key" \
-F "file=@/path/to/photo.jpg"
Returns 201 with {"data": Media}.
Posts
List posts
GET /posts
Query parameters
-
statusstring optional - Filter by status.
-
page_idinteger optional - Only posts for this Page.
-
client_idinteger optional - Only posts for Pages of this client.
-
per_pageinteger optional - Results per page, up to 100.
-
pageinteger optional - Page number, starting at 1.
Returns a paginated list, newest first: {"data": [Post, …], "meta": {…}}.
Create posts
POST /posts
Creates the same post on one or more Pages — one Post per Page. Send an Idempotency-Key header so retries are safe.
Body
-
page_idsinteger[] required - NicePublish Page IDs (not Facebook IDs). Each Page must be
active. -
messagestring optional - Post text, up to 10,000 characters.
-
linkstring optional - A URL to show as a link preview.
-
mediaobject[] optional - Up to 10 items, each either
{"media_id": 5}(an uploaded file) or{"url": "https://…"}(a public HTTPS URL). For URLs you may add"type": "image"or"video"; otherwise it's inferred from the file extension. -
first_commentstring optional - Posted as the Page right after the post is published. Every selected Page needs the
first_commentcapability. -
scheduled_atstring optional - ISO 8601 time, up to 6 months ahead. Omit to publish now. A time without an offset is interpreted in your account's timezone.
Rules
- A post needs at least one of
message,linkormedia. linkandmediacan't be combined — Facebook shows either a link preview or media. Put the link in the text or the first comment instead.- At most one video per post, and no other media alongside a video.
- At most 10 photos (Facebook shows several photos as an album).
- Each Page has a daily posting limit set by your plan (
limits.posts_per_page_per_day). - Identical content to the same Page within 24 hours is rejected.
Rule violations return 422 validation_failed and nothing is created.
curl -X POST https://nicepublish.com/api/v1/posts \
-H "Authorization: Bearer np_live_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4f9c2b1e-8a7d-4e55-b2c0-1d9e6f3a7b21" \
-d '{
"page_ids": [12, 13],
"message": "Our autumn menu starts Monday.",
"media": [{ "media_id": 5 }, { "url": "https://cdn.example.com/soup.jpg" }],
"first_comment": "Full menu: https://example.com/menu",
"scheduled_at": "2026-10-06T09:00:00+01:00"
}'
<?php
$response = $api->post('posts', [
'headers' => ['Idempotency-Key' => $uuid], // generate once per logical post and reuse on retry
'json' => [
'page_ids' => [12, 13],
'message' => 'Our autumn menu starts Monday.',
'media' => [['media_id' => 5], ['url' => 'https://cdn.example.com/soup.jpg']],
'first_comment' => 'Full menu: https://example.com/menu',
'scheduled_at' => '2026-10-06T09:00:00+01:00',
],
]);
$posts = json_decode((string) $response->getBody(), true)['data']; // one post per Page
import { randomUUID } from 'node:crypto';
const idempotencyKey = randomUUID(); // store it and reuse it if you retry
const res = await fetch('https://nicepublish.com/api/v1/posts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NICEPUBLISH_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
page_ids: [12, 13],
message: 'Our autumn menu starts Monday.',
media: [{ media_id: 5 }, { url: 'https://cdn.example.com/soup.jpg' }],
first_comment: 'Full menu: https://example.com/menu',
scheduled_at: '2026-10-06T09:00:00+01:00',
}),
});
const { data: posts } = await res.json(); // one post per Page
HTTP/1.1 201 Created
{
"data": [
{ "id": 981, "page_id": 12, "status": "scheduled", "type": "album", "...": "..." },
{ "id": 982, "page_id": 13, "status": "scheduled", "type": "album", "...": "..." }
]
}
Publishing now: when you omit scheduled_at, the API attempts publication immediately and returns each post's resulting status — published or failed. If publication is still in progress when the response is sent, the status is publishing or scheduled, and a post.published or post.failed webhook follows.
Retrieve a post
GET /posts/{id}
Returns {"data": Post}.
Update a post
PATCH /posts/{id}
- draft, scheduled or failed — any field from Create posts except
page_ids. The same rules apply. - published — only
message, which edits the post's text on Facebook. - Other statuses can't be edited.
curl -X PATCH https://nicepublish.com/api/v1/posts/981 \
-H "Authorization: Bearer np_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"message":"Our autumn menu starts Monday — now with oat milk!"}'
Returns {"data": Post}.
Cancel or delete a post
DELETE /posts/{id}
If the post is scheduled, it is canceled and won't be published. If it is published, it is deleted from Facebook. Returns {"deleted": true}. Posts with status publishing or unknown return 409 — try again shortly.
Refresh metrics
POST /posts/{id}/sync
Fetches the latest engagement and insight metrics for a published post from Facebook. Metrics are also refreshed automatically in the background. Returns {"data": Post}.
Webhooks
Webhooks
Webhooks tell your server when something happens, so you don't need to poll. Set your endpoint URL in Dashboard → Developers; the signing secret is shown there too. You can send a test delivery from the same page.
- Deliveries are
POSTrequests with a JSON body, sent fromUser-Agent: NicePublish-Webhooks/1.0. - Respond with any
2xxstatus within 10 seconds. Do slow work asynchronously. - Use the event
idto ignore duplicates — in rare cases an event can be delivered more than once. - Webhooks are sent while your plan includes API access.
Events & payloads
| Event | Sent when | data |
|---|---|---|
page.connected | A Page is connected (in the dashboard or via a connect link). | page |
page.disconnected | A Page is disconnected, or removed with its client. | page |
page.needs_reconnect | Facebook access for a Page was removed or expired. Its status becomes needs_reauth. | page |
post.published | A post went live on Facebook. | post |
post.failed | A post could not be published. See post.error. | post |
Every delivery has the same envelope. data.post is a Post; data.page is a Page.
POST /webhooks/nicepublish HTTP/1.1
Content-Type: application/json
User-Agent: NicePublish-Webhooks/1.0
X-NicePublish-Event: post.published
X-NicePublish-Signature: t=1791277204,v1=5b0f1c6a9d...e3
{
"id": "evt_k2m9x0q4h7r1s8t3v6w5y2z0",
"event": "post.published",
"created_at": "2026-10-06T09:00:04+00:00",
"data": {
"post": { "id": 981, "page_id": 12, "status": "published", "facebook_post_id": "104857392011223_122101234567", "...": "..." }
}
}
Verifying signatures
Every delivery includes an X-NicePublish-Signature header so you can confirm it came from NicePublish and wasn't modified:
X-NicePublish-Signature: t=<unix timestamp>,v1=<hex signature>
- Split the header on
,and readtandv1. - Build the signed string:
<t>.<raw request body>— the timestamp, a dot, and the exact bytes you received. - Compute an HMAC-SHA256 of that string using your signing secret, hex-encoded.
- Compare it with
v1using a constant-time comparison. - Reject the request if
tis more than 5 minutes from your current time, to prevent replays.
<?php
$payload = file_get_contents('php://input'); // the raw body, before any JSON decoding
$header = $_SERVER['HTTP_X_NICEPUBLISH_SIGNATURE'] ?? '';
$secret = getenv('NICEPUBLISH_WEBHOOK_SECRET');
$parts = [];
foreach (explode(',', $header) as $pair) {
[$key, $value] = array_pad(explode('=', $pair, 2), 2, '');
$parts[trim($key)] = trim($value);
}
$timestamp = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
if (abs(time() - $timestamp) > 300 || ! hash_equals($expected, $parts['v1'] ?? '')) {
http_response_code(400);
exit('Invalid signature');
}
$event = json_decode($payload, true);
switch ($event['event']) {
case 'post.published':
// $event['data']['post'] ...
break;
case 'page.needs_reconnect':
// $event['data']['page'] ...
break;
}
http_response_code(200);
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Use the raw body: re-serialising parsed JSON will change the bytes and break the signature.
app.post('/webhooks/nicepublish', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-NicePublish-Signature') || '';
const parts = Object.fromEntries(
header.split(',').map((pair) => {
const i = pair.indexOf('=');
return [pair.slice(0, i).trim(), pair.slice(i + 1).trim()];
})
);
const timestamp = Number(parts.t);
const expected = crypto
.createHmac('sha256', process.env.NICEPUBLISH_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body.toString('utf8')}`)
.digest();
const received = Buffer.from(parts.v1 || '', 'hex');
const fresh = Math.abs(Date.now() / 1000 - timestamp) <= 300;
const valid = received.length === expected.length && crypto.timingSafeEqual(received, expected);
if (!fresh || !valid) return res.status(400).send('Invalid signature');
const event = JSON.parse(req.body.toString('utf8'));
// switch (event.event) { case 'post.published': ... }
res.sendStatus(200);
});
The X-NicePublish-Event header carries the event name, so you can route requests before parsing the body. Keep your signing secret private, just like an API key.
Retries
If your endpoint doesn't respond with a 2xx status (including timeouts and connection errors), we retry with increasing delays — about 1 minute, 5 minutes, 30 minutes and 2 hours after the previous attempt — up to 5 attempts in total. After the last failed attempt the delivery is marked as failed and is not retried again.
Because deliveries can be retried, make your handler idempotent: processing the same event id twice should have no extra effect.
Need help?
Questions about the API or an integration? Email [email protected] and include the request time and, if you have it, the post or event ID.