Skip to contentDevelopersPrezly website
Integrations

React to changes with webhooks

Register a newsroom event receiver, verify its shared secret, and reconcile story or category changes safely.

Webhooks notify your integration when a newsroom's stories or categories change. Use them to refresh a custom website, invalidate a cache, or queue a synchronization job. A notification identifies a changed resource; read the current resource before deciding what to publish on your own site.

Choose the credential and events

Webhook management requires an integration API key with account-level webhook permissions. Personal API tokens are unsupported, and newsroom integration credentials are explicitly denied webhook access. Ask the Prezly Customer Success Team to configure suitable access; adding sites:write to a personal token does not enable these operations. See Authentication, then load the integration key into PREZLY_API_TOKEN on your server.

View permission is required for reads; management permission is required for writes. Update and delete also read the registration first. Responses include the shared secret, so keep management requests and responses out of public browser code and application logs.

Choose only the events your receiver handles:

ResourceEventsIdentifier to retain
Storystory.created, story.updated, story.deletedstory.uuid
Categorycategory.created, category.updated, category.deletedNumeric category.id, together with its owning newsroom

There is no separate story.published event in this contract. Publication and scheduled releases can produce story.updated; an update alone does not establish that the story is public. Read its current status and visibility using Get Story and apply the rules in Publishing stories.

Story creation and update notifications are suppressed when the resulting story is confidential. Changing a public story to confidential can therefore produce no story.updated notification. story.deleted notifications are still sent for confidential stories.

If you publish a copy of Prezly content, webhooks alone cannot keep its visibility in sync. Periodically reconcile stored stories with authorized API reads, recheck publication status and visibility, and withdraw copies that are no longer public. Invalidate a cached public copy if you can no longer confirm it is public; investigate access failures separately.

Prepare the receiver before activating delivery

Expose an HTTPS POST endpoint controlled by your integration. Generate a strong, unique shared secret and store it separately from your API key. The configured secret is sent as the top-level JSON secret property; it is not an HMAC signature or an Authorization header. Do not put your Prezly API credential in that field.

The body contains action and either a story or category reference. This abbreviated example omits other story reference fields:

{
  "action": "story.updated",
  "secret": "YOUR_WEBHOOK_SECRET",
  "story": {
    "id": 12345,
    "uuid": "11111111-1111-4111-8111-111111111111"
  }
}

Save this Node.js 22+ helper as read-webhook.mjs. Set PREZLY_WEBHOOK_SECRET to the configured secret in your receiver's environment. Call the helper with the request body's UTF-8 text after enforcing a body-size limit in your HTTP framework:

import { timingSafeEqual } from 'node:crypto';

const expected = process.env.PREZLY_WEBHOOK_SECRET;
if (!expected) throw new Error('Set PREZLY_WEBHOOK_SECRET before accepting webhooks.');
const expectedBytes = Buffer.from(expected, 'utf8');
const actions = new Set([
  'story.created', 'story.updated', 'story.deleted',
  'category.created', 'category.updated', 'category.deleted',
]);

export function readWebhook(rawBody) {
  const payload = JSON.parse(rawBody);
  if (!payload || typeof payload !== 'object' || typeof payload.secret !== 'string') {
    throw new Error('Unauthenticated webhook.');
  }
  const supplied = Buffer.from(payload.secret, 'utf8');
  if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
    throw new Error('Unauthenticated webhook.');
  }
  if (!actions.has(payload.action)) return null;
  const kind = payload.action.split('.')[0];
  const resource = payload[kind];
  const id = kind === 'story' ? resource?.uuid : resource?.id;
  const validId = kind === 'story'
    ? typeof id === 'string' && /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(id)
    : Number.isSafeInteger(id) && id > 0;
  if (!validId) throw new Error('Missing or invalid resource identifier.');
  return { action: payload.action, kind, id };
}

This helper authenticates and normalizes a notification. Your HTTP handler must map malformed/unauthenticated input to an error response, and durably queue the returned work before acknowledging with a successful response. An authenticated unknown action returns null; acknowledge it without processing. Bind the receiver to the newsroom you configured rather than accepting an arbitrary destination from incoming JSON. Keep the secret and full payload out of logs.

Delivery is asynchronous. The current sender POSTs JSON with a default 10-second timeout. Acknowledge promptly after accepting work; fetch resources and rebuild pages in a worker. There is no delivery ID, timestamp, or signature in this payload contract, so the secret does not provide replay protection. Make processing safe to repeat, tolerate ordering differences, and periodically reconcile with API reads. Do not rely on a particular retry schedule, exactly-once delivery, or permanent retention of missed events.

Register an inactive webhook

Save this as webhook-create.json, replacing the example URL and secret with your receiver's values. Keep the file out of source control and shared artifacts:

{
  "name": "Custom newsroom synchronization",
  "url": "https://newsroom.example.test/integrations/prezly",
  "events": ["story.created", "story.updated", "story.deleted", "category.created", "category.updated", "category.deleted"],
  "secret": "YOUR_WEBHOOK_SECRET",
  "is_active": false
}

Replace YOUR_NEWSROOM_UUID:

curl --fail-with-body --silent --show-error --request POST 'https://api.prezly.com/v2/newsrooms/YOUR_NEWSROOM_UUID/webhooks' \
  --header "Authorization: Bearer $PREZLY_API_TOKEN" \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --data-binary @webhook-create.json

The response is 201 Created with webhook. Store its UUID id with the owning newsroom UUID. Creation is not an upsert: if its response is lost, list registrations and reconcile the name, URL, and event selection before creating another.

The API also supports request_options for delivery configuration. Use the normal TLS verification defaults; resolve certificate problems at the receiver. Registration responses can include a null secret when none was configured. This guide always supplies one and explicitly starts inactive.

Create webhook →

Inspect, activate, and process changes

List registrations, including inactive ones, before enabling a duplicate receiver:

curl --fail-with-body --silent --show-error 'https://api.prezly.com/v2/newsrooms/YOUR_NEWSROOM_UUID/webhooks' \
  --header "Authorization: Bearer $PREZLY_API_TOKEN" \
  --header 'Accept: application/json'

The response contains an unpaginated webhooks array, newest registrations first. Read one registration by its stored UUID:

curl --fail-with-body --silent --show-error 'https://api.prezly.com/v2/newsrooms/YOUR_NEWSROOM_UUID/webhooks/YOUR_WEBHOOK_ID' \
  --header "Authorization: Bearer $PREZLY_API_TOKEN" \
  --header 'Accept: application/json'

Check the webhook response, test your receiver with controlled fixtures, and then activate:

curl --fail-with-body --silent --show-error --request PATCH 'https://api.prezly.com/v2/newsrooms/YOUR_NEWSROOM_UUID/webhooks/YOUR_WEBHOOK_ID' \
  --header "Authorization: Bearer $PREZLY_API_TOKEN" \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --data '{"is_active":true}'

PATCH returns 200 OK with webhook. Omitted properties remain unchanged; supplying events replaces the whole selection. Use is_active: false to pause before changing the URL or rotating the secret. Coordinate the receiver's secret update and allow for work already queued; pausing a registration is not a recall of in-flight delivery.

For created/updated resources, fetch the current story or category, then update your local record by its stable identity. Category events use the owning newsroom UUID in the read URL. For deleted events, remove or invalidate the corresponding local record even if its API read now returns 404. A 404 on a current-state read can also mean access changed; investigate before treating every failure as deletion. A delayed notification must not overwrite a newer local state with an old payload snapshot.

Update webhook → · List webhooks → · Get webhook →

Remove a retired registration

After identifying the registration to retire:

curl --fail-with-body --silent --show-error --request DELETE 'https://api.prezly.com/v2/newsrooms/YOUR_NEWSROOM_UUID/webhooks/YOUR_WEBHOOK_ID' \
  --header "Authorization: Bearer $PREZLY_API_TOKEN" \
  --header 'Accept: application/json'

A successful deletion returns 204 No Content; do not parse an empty body as JSON. It removes the registration, not your integration's already accepted work. After an uncertain update or deletion, read/list the same newsroom's registrations before retrying. For HTTP failures and deliberate retries, see Handling errors.

Delete webhook →

On this page