nicepublish

Developers

NicePublish API reference

A small, predictable REST API for publishing to Facebook Pages from your own software — plus signed webhooks so you know what happened.

https://nicepublish.com/api/v1 JSON over HTTPS v1

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 use multipart/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 data key.
  • The API is available on the Growth and Scale plans (and during the free trial). Other plans receive 402 plan_required.
The API publishes only to Pages that have been connected to your account through Facebook Login, by you or by your client via a connect link. Only publish content you own or have permission to share, and follow our Acceptable Use Policy.

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.

json
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."]
    }
  }
}
StatusCodeMeaning
401unauthenticatedMissing, invalid or revoked API key.
402plan_requiredYour plan doesn't include API access, or your trial/plan has ended.
403account_suspendedThe account has been suspended. Contact support.
404not_foundThe resource doesn't exist or doesn't belong to your account.
422validation_failedThe request was understood but a field is invalid or a rule (limits, duplicates) was not met. See fields.
429rate_limitedToo 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
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 (field idempotency_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:

json
{
  "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.

id integer
NicePublish Page ID. Use this in page_ids when creating posts.
facebook_page_id string
The Page's ID on Facebook.
name string
Page name.
username string|null
The Page's Facebook username, if it has one.
category string|null
Page category as reported by Facebook.
picture_url string|null
Profile picture URL. Facebook CDN URLs can expire; refresh by fetching the Page again.
followers_count integer|null
Follower count at the last refresh.
status string
active or needs_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_id integer|null
The client this Page belongs to, if any.
capabilities object
What the granted permissions allow: publish, first_comment (posting comments as the Page) and insights (reading analytics). Each is a boolean.
connected_at string
ISO 8601 time the Page was connected.
Example json
{
  "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.

id integer
Client ID.
name string
Display name.
external_id string|null
Your own identifier for this customer (e.g. the user or account ID in your system). Useful for lookups.
pages_count integer
Number of Pages connected for this client.
created_at string
ISO 8601 creation time.
Example json
{
  "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.

id integer
Post ID.
batch_id string
Shared by all posts created in the same request (one per Page).
page_id integer
NicePublish Page ID.
facebook_page_id string
The Page's ID on Facebook.
status string
See post statuses.
type string
text, link, photo, album or video — derived from the content.
message string|null
Post text.
link string|null
Link shown as a preview card.
media array
Attached media items: {"type": "image"|"video", "url": "…", "media_id": 5} (media_id only for uploaded files).
first_comment string|null
Comment posted as the Page right after publishing.
first_comment_id string|null
Facebook ID of the first comment once posted.
first_comment_error string|null
Why the first comment could not be posted, if it failed. The post itself is still published.
scheduled_at string|null
ISO 8601 time the post is (or was) scheduled for.
published_at string|null
ISO 8601 time Facebook accepted the post.
facebook_post_id string|null
The post's ID on Facebook once published.
permalink string|null
Public URL of the post on Facebook.
error string|null
Human-readable reason when status is failed.
metrics object|null
Latest engagement: reactions, comments, shares, plus insight metrics such as post_media_view, post_total_media_view_unique, post_clicks and post_video_views when available. Metric names follow Meta's and can change when Meta updates its API. null until the first sync.
metrics_synced_at string|null
When metrics were last refreshed.
source string
web (created in the dashboard) or api.
created_at string
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.
Example json
{
  "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}.

id integer
Media ID.
type string
image or video.
url string
Public URL used to hand the file to Facebook.
mime string
MIME type, e.g. image/jpeg.
size integer
Size in bytes.
created_at string
ISO 8601 upload time.
Example json
{
  "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.

Response json
{
  "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_id integer optional
Only Pages belonging to this client.
status string optional
active or needs_reauth.
curl
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

since date optional
First day, YYYY-MM-DD.
until date optional
Last day, YYYY-MM-DD.
curl
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_id string optional
Find the client with this external ID.

Returns {"data": [Client, …]}.

Create a client

POST /clients

Body

name string required
Display name, e.g. your customer's business name.
external_id string optional
Your own ID for this customer.
curl
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}.

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
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

status string optional
Filter by status.
page_id integer optional
Only posts for this Page.
client_id integer optional
Only posts for Pages of this client.
per_page integer optional
Results per page, up to 100.
page integer 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_ids integer[] required
NicePublish Page IDs (not Facebook IDs). Each Page must be active.
message string optional
Post text, up to 10,000 characters.
link string optional
A URL to show as a link preview.
media object[] 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_comment string optional
Posted as the Page right after the post is published. Every selected Page needs the first_comment capability.
scheduled_at string 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, link or media.
  • link and media can'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
Response json
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
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 POST requests with a JSON body, sent from User-Agent: NicePublish-Webhooks/1.0.
  • Respond with any 2xx status within 10 seconds. Do slow work asynchronously.
  • Use the event id to ignore duplicates — in rare cases an event can be delivered more than once.
  • Webhooks are sent while your plan includes API access.

Events & payloads

EventSent whendata
page.connectedA Page is connected (in the dashboard or via a connect link).page
page.disconnectedA Page is disconnected, or removed with its client.page
page.needs_reconnectFacebook access for a Page was removed or expired. Its status becomes needs_reauth.page
post.publishedA post went live on Facebook.post
post.failedA 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.

Example delivery http
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>
  1. Split the header on , and read t and v1.
  2. Build the signed string: <t>.<raw request body> — the timestamp, a dot, and the exact bytes you received.
  3. Compute an HMAC-SHA256 of that string using your signing secret, hex-encoded.
  4. Compare it with v1 using a constant-time comparison.
  5. Reject the request if t is 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.