Pager API · v2

API Documentation

Pull conversations, messages and clients into your own systems, get notified about new messages and comments via webhooks, send messages and files to clients, reply to comments, and manage conversations, managers' access and Pager's reference data.

Getting started

Introduction

The Pager API gives your systems access to your organization's data. You can read conversations, messages and clients, send messages and files to clients, reply to Instagram comments, change a conversation's status, responsible manager and group, fill in client cards, manage managers' access to channels, and manage the reference data managers use in the dashboard: statuses, client groups, saved reply folders and saved replies. Pager can also notify your server about new messages and comments on its own via webhooks.

Base URL
https://api.pager.co.ua/v2
  • Requests and responses are UTF-8 JSON; dates are ISO 8601 in UTC.
  • Every list comes in the same envelope, { data, hasMore, nextCursor }, so you write the pagination loop once — see Pagination.
  • Unknown fields in the body or query aren't ignored: they return 400 unknown_parameter, so a typo in a field name never slips by silently.
  • A machine-readable API description is available as an OpenAPI spec: for Postman, client generation and AI assistants.
  • Every response has an X-Request-Id header. Include it when you contact support.
  • Every request is signed with an API key — see Authentication.

Getting started

Authentication

Send your organization's API key in the Authorization header as Bearer <key>. A key belongs to one organization: the API only reads and changes that organization's data.

The API accepts requests from any origin, but a key grants access to your whole organization's data. Keep it on your server — in environment variables or a secrets manager — and never ship it in browser or mobile app code.

Keys look like pgr_live_ followed by 40 characters. A missing, invalid, revoked or expired key always gets the same response: 401 invalid_api_key.

Start your integration with GET /me: it confirms the key works and returns your organization, plan and rate limits.

curl "https://api.pager.co.ua/v2/me" \
  -H "Authorization: Bearer $PAGER_API_KEY"

Getting started

Pagination

Conversations, messages, clients and conversation history come in pages. Each page is an envelope: { data, hasMore, nextCursor }.

  • limit is the page size, from 1 to 100, 50 by default.
  • If hasMore is true, pass nextCursor as the cursor parameter to get the next page. Keep all other parameters the same.
  • Records go from newest to oldest. Pages never duplicate or skip records, even when several records share the same timestamp.
  • The cursor is opaque: don't parse or build it yourself. A malformed cursor returns 400 invalid_cursor.
  • Reference data (statuses, groups, folders, saved replies) and a conversation's orders are returned in full: hasMore is always false.
# First page
curl "https://api.pager.co.ua/v2/conversations?limit=100" \
  -H "Authorization: Bearer $PAGER_API_KEY"

# Next page: nextCursor from the previous response
curl "https://api.pager.co.ua/v2/conversations?limit=100&cursor=WyIyMDI2LTA5LTIzVDE2OjAyOjExLjg0MFoiLCIyYjljNGQ2ZS04ZjBhLTRjMmUtOWI0ZC02ZjhhMGMyZTRiNmQiXQ" \
  -H "Authorization: Bearer $PAGER_API_KEY"

Syncing

For regular syncs, use updatedSince instead of a full walk. The conversation list is sorted by lastMessageAt: a conversation that gets a new message jumps to the top, so a page-by-page walk can miss it. Remember the largest updatedAt you've received and pass it as updatedSince next time. The bound is inclusive, so the last records come again — upsert by id rather than inserting.

Getting started

Filters and expand

Lists accept filters in the query string. Different filters combine with AND: a record must match each of them.

  • Several values for one filter: ?statusId=a,b or ?statusId=a&statusId=b. Any of the values matches.
  • null means an empty field, same as in the dashboard: ?responsibleUserId=null — conversations with no responsible manager, ?statusId=a,null — with status a or no status. Works for statusId, responsibleUserId and clientGroupId.
  • q is a case-insensitive search over the same fields as the dashboard search. Each resource lists its fields in the parameter description.
  • An ID from another organization in a filter isn't an error — the list is just empty.
  • An unknown parameter returns 400 unknown_parameter.

expand

Without expand, a conversation only carries the IDs of related objects (clientId, statusId and so on), so lists stay lean. The expand parameter adds the objects themselves: client, channel, status, clientGroup, responsibleUser, comma-separated. An unknown value returns 400 with param: "expand". Channel tokens and keys are never included.

curl "https://api.pager.co.ua/v2/conversations?statusId=5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e,null&responsibleUserId=null&expand=client,status" \
  -H "Authorization: Bearer $PAGER_API_KEY"

Getting started

Errors

Errors come with a matching HTTP status and an error object. Handle them by type, the broad category; code narrows down the cause, param points to the request field, and requestId matches the X-Request-Id header.

Response · 400
{
  "error": {
    "type": "invalid_request",
    "code": "invalid_parameter",
    "message": "Color must be #rrggbb",
    "param": "color",
    "requestId": "req_4f1c9a2e7b3d4c6e9a0f1d2e3f4a5b6c"
  }
}
HTTPtypeWhen it happens
400invalid_requestA field failed validation (invalid_parameter), an unknown field was sent (unknown_parameter), the body isn't valid JSON (invalid_body), or an ID in the body doesn't belong to your organization. Webhooks also use invalid_webhook_url and webhook_limit_reached — see Limits.
401authentication_errorThe key is missing, invalid, revoked or expired (invalid_api_key).
402quota_exceededThe plan has expired or the message balance is exhausted (message_limit_reached). Returned when sending a message.
403permission_errorThe key isn't allowed to perform this action.
404not_foundThe object isn't in your organization (status_not_found and so on), or the route doesn't exist (route_not_found).
409conflictThe request conflicts with the current state: the externalId is taken by another client (external_id_taken), the idempotency key was already used (idempotency_key_reused, idempotency_request_in_progress), the comment already has a private reply (private_reply_exists), or a delivery of a disabled webhook can't be retried (webhook_disabled).
422channel_errorThe message couldn't be sent to the channel: the messenger refused it (channel_rejected), the channel doesn't support sending (channel_unsupported), or it isn't configured — see Send a message.
429rate_limit_exceededRate limit exceeded — see Rate limits.
500api_errorInternal Pager error. Retry later, and include requestId when contacting support.

Another organization's object returns the same 404 as a non-existent one — the API never confirms that it exists.

The permission_error type is reserved for future use. Rely on type and code, not the message text: it may change.

Getting started

Rate limits

Limits are tracked per key using a leaky bucket. Each request adds its cost to the bucket, and the bucket drains at a steady rate. While the cost fits, the request goes through; when it doesn't, the API returns 429 rate_limit_exceeded.

BucketCapacityDrains
general6010 per secondAll requests.
outbound301 per secondRequests that go out to messengers: each sent message, comment reply and comment deletion takes an extra 1. So up to 30 such requests in a burst, then one per second.

Request cost: 1 to retrieve, create, update or delete an object, 2 for a page of a list, 5 for an organization-wide message search or a conversation count. Each operation shows its cost next to it. In practice, you can burst up to 60 single-cost requests, then sustain 10 per second.

GET /me returns the key's current limits in rateLimits.

HeaderWhen it happens
RateLimit-LimitBucket capacity.
RateLimit-RemainingHow many units still fit in the bucket.
RateLimit-ResetSeconds until the bucket is fully drained.
RateLimit-PolicyThe policy as 60;w=6: capacity and how many seconds a full bucket takes to drain.
Retry-AfterOn 429 responses only: seconds to wait before retrying.
Response · 429
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 6
RateLimit-Policy: 60;w=6
Retry-After: 1
When you get a 429, wait the number of seconds in Retry-After and retry. Retrying without a pause will just hit the limit again.

Getting started

Idempotency

Requests with side effects accept an Idempotency-Key header. If the connection dropped and you don't know whether the request went through, retry it with the same key — Pager won't perform the action twice and returns the stored response instead.

curl -X POST "https://api.pager.co.ua/v2/conversations/2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d/messages" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Idempotency-Key: a3f1c9e2-5b7d-4e8a-9c0b-1d2e3f4a5b6c" \
  -H "Content-Type: application/json" \
  -d '{"text":"Дякуємо за замовлення! Номер ТТН: 20450000012345"}'
  • For now, sending a message, replying to a comment and creating a webhook subscription accept the header; other requests ignore it. The header is optional: without it the request runs as usual.
  • A key is any string up to 255 characters, unique for each new action. A UUID is the easiest choice. An empty or too long key returns 400 invalid_idempotency_key.
  • A key is scoped to your organization and lives for 24 hours. A retry with the same key and the same body gets the stored response with an Idempotent-Replayed: true header.
  • Only successful (2xx) responses are stored. After an error the key is released, so you can send a corrected request with the same key.
  • The same key with a different body or path returns 409 idempotency_key_reused. While the first request is still running — 409 idempotency_request_in_progress: wait and retry.

Getting started

Sending files

A file is sent to a client in three steps: get an upload link, upload the file to Pager's storage, and send a message with the ready-made attachment. The file goes to storage directly, bypassing the API, so its size doesn't affect rate limits.

  1. 1

    POST /files with the conversationId, name, MIME type and size of the file. The response contains uploadUrl, the headers to use and a ready-made attachment object.

  2. 2

    Within 5 minutes, upload the file with a PUT request to uploadUrl using the headers from headers. No Authorization here: the signature in the link grants access.

  3. 3

    Send a message with attachments: [attachment] — the object from step one, unchanged.

# 1. Upload link
curl -X POST "https://api.pager.co.ua/v2/files" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d", "name": "invoice-184502.pdf", "mime": "application/pdf", "size": 248193}'

# 2. The file goes straight to storage, no Authorization
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @invoice-184502.pdf

# 3. Message with the attachment
curl -X POST "https://api.pager.co.ua/v2/conversations/2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d/messages" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"attachments": [ATTACHMENT]}'

One message carries either text or one file: most messengers drop text sent next to a file. To send a file with a caption, send two messages.

Up to 20 MB. Images, video, audio, PDF, ZIP, Office documents, TXT and CSV are allowed — see Files. If the file wasn't uploaded, the API returns 400 attachment_not_uploaded; if the uploaded file is larger than allowed — 400 attachment_too_large.

Getting started

OpenAPI

The full API description is available in OpenAPI 3.1 format. The spec is generated from the same schemas the API uses to validate requests, so it always matches the current version. Import it into Postman or Insomnia, or generate a client for your language from it.

OpenAPI
https://api.pager.co.ua/v2/openapi.json
https://api.pager.co.ua/v2/docs
  • GET /v2/openapi.json — the spec: every endpoint, parameter, object and error schema, the cost of each request, plus the webhook event formats in the webhooks section.
  • GET /v2/docs — interactive docs built from the spec: browse the schemas and send requests with your key right from the browser.
  • Both URLs are public: no API key is needed, and requests to them don't count towards rate limits. The response is cached for 5 minutes.
# Download the spec
curl -o pager-openapi.json https://api.pager.co.ua/v2/openapi.json

API keys

Creating a key

An organization admin creates the key in the Pager dashboard. An organization can have only one active key.

  1. 1

    Open Settings → API.

  2. 2

    Click Create key and give it a name, e.g. “CRM integration”. The name is just for you, to remember where the key is used.

  3. 3

    Copy the key and store it somewhere safe. The full key is shown only once: Pager stores only its hash, so a lost key can't be recovered — only reissued.

Only admins can create, reissue and revoke the key. Other members see just the key's name, its mask and when it was last used.

API keys

Reissuing a key

Reissue the key if it may have leaked, or if someone who had access to it has left the team.

  1. 1

    In Settings → API, click Reissue.

  2. 2

    Change the name if needed and confirm.

  3. 3

    Save the new key and replace it in all your integrations.

The old key stops working the moment the new one is created — there's no grace period. Requests with the old key will get 401, so update your integrations right after reissuing.

If two admins reissue the key at the same time, one of the requests is rejected. Refresh the page and check which key is active now.

API keys

Revoking a key

Revoke the key when the integration is no longer needed. The organization then has no active key until you create a new one.

  1. 1

    In Settings → API, click the trash button next to the key.

  2. 2

    Confirm the revocation.

Revocation can't be undone. All requests with this key immediately get 401 invalid_api_key.

Webhooks

Overview

Webhooks notify your system about events in Pager as soon as they happen: Pager sends a POST request with the event to your URL. No need to poll the API for new messages.

  • A subscription is a URL plus the list of events to send to it. An organization can have up to 10 subscriptions, for example one for your CRM and another for analytics.
  • Every request is signed with the subscription's secret, so your server can make sure the event really came from Pager — see Verifying signatures.
  • If your server is down, Pager keeps retrying for about 10 hours — see Delivery and retries.
  • Subscriptions can be managed in the dashboard or via the API — see the Webhook subscriptions resource. A subscription created in the dashboard is visible via the API, and vice versa.

Webhooks

Setup

First, prepare a public HTTPS endpoint on your server that accepts POST requests with JSON. Then an organization admin creates the subscription in the dashboard.

  1. 1

    Open Settings → API → Webhooks and click Add webhook.

  2. 2

    Enter the endpoint URL and select the events to subscribe to. The description is optional: it only helps you remember where the events go.

  3. 3

    Copy the signing secret (whsec_…) and store it on your server, for example in the PAGER_WEBHOOK_SECRET environment variable. The secret is shown only once: it can't be recovered, only rotated.

  4. 4

    In the subscription menu, open Deliveries & test and click Send test. Pager immediately sends a webhook.test event and shows what your server responded.

Via the API, a subscription is created with POST /webhooks: the secret comes in the secret field of the response. Check the endpoint with POST /webhooks/{id}/test.

A new subscription receives only events that happen after it was created. It doesn't get the message history: to load existing data, walk the API lists — see Pagination.

In the dashboard, only organization admins can manage webhooks. Via the API, any system holding the organization's API key can.

Webhooks

Events

Each event arrives in a separate request. The request body is a JSON envelope of the same shape for every event type, with the event data in the data field.

EventWhen it happens
message.receivedA client sent a message to any of the organization's channels.
message.sentThe organization sent a message to a client: a manager in the dashboard, your system via the API, a broadcast or an automation.
comment.receivedA client left a comment under an Instagram post — a new one or a reply in a thread. Replies on behalf of the page are not events.
webhook.testA test event from the Send test button or POST /webhooks/{id}/test. You can't subscribe to it: it's only sent on request. data.message is a string.

Event envelope

FieldWhen it happens
idEvent ID, evt_…. It stays the same across retries and matches the Pager-Event-Id header, which makes it the key for dropping duplicates.
typeEvent type — see the table above.
createdAtWhen the event happened, ISO 8601 in UTC.
organizationIdThe organization the event happened in.
dataEvent data. For message events: message, conversation, client and channel; for comment.received, comment instead of message.
Request body · message.received
{
  "id": "evt_6c1f0e9d8b7a45c3a2e1f0d9c8b7a6e5",
  "type": "message.received",
  "createdAt": "2026-09-24T08:31:40.582Z",
  "organizationId": "org_2rRfXLYNflNpMgzPBX1MU18z39C",
  "data": {
    "message": {
      "id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a",
      "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "direction": "incoming",
      "text": "А доставка у Львів скільки йде?",
      "attachments": [],
      "authorId": null,
      "replyToMessageId": null,
      "externalId": "aWdfZAG1faXRlbTo…",
      "reaction": null,
      "isRead": false,
      "isEdited": false,
      "isDelivered": null,
      "errorMessage": null,
      "ad": null,
      "storyReplyUrl": null,
      "createdAt": "2026-09-24T08:31:40.117Z",
      "updatedAt": "2026-09-24T08:31:40.117Z"
    },
    "conversation": {
      "id": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
      "clientId": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
      "statusId": "5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e",
      "responsibleUserId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
      "clientGroupId": "1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e",
      "state": "unread",
      "lastMessageDirection": "incoming",
      "lastMessageAt": "2026-09-24T08:31:40.117Z",
      "snippet": "А доставка у Львів скільки йде?",
      "createdAt": "2026-08-14T09:21:37.512Z",
      "updatedAt": "2026-09-24T08:31:40.117Z"
    },
    "client": {
      "id": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
      "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
      "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "externalId": null,
      "name": "Олена Коваль",
      "username": "olena.koval",
      "imageUrl": "https://files.pager.co.ua/…/avatar.jpg",
      "phone": null,
      "infoName": "Олена",
      "infoLastName": "Коваль",
      "infoPhone": "+380671234567",
      "infoEmail": "olena@example.com",
      "infoAddress": "Київ, НП №52",
      "infoNote": "Цікавиться оптовими цінами",
      "createdAt": "2026-08-14T09:21:37.512Z",
      "updatedAt": "2026-09-23T15:02:11.840Z"
    },
    "channel": {
      "id": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
      "name": "Instagram магазину",
      "channelSource": "Instagram",
      "username": "kvitka.shop",
      "imageUrl": "https://files.pager.co.ua/…/channel.jpg"
    }
  }
}
Request body · comment.received
{
  "id": "evt_3e2d1c0b9a8f47e6d5c4b3a2f1e0d9c8",
  "type": "comment.received",
  "createdAt": "2026-09-24T10:02:16.204Z",
  "organizationId": "org_2rRfXLYNflNpMgzPBX1MU18z39C",
  "data": {
    "comment": {
      "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
      "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "parentId": null,
      "direction": "incoming",
      "text": "Скільки коштує доставка у Львів?",
      "username": "olena.koval",
      "authorId": null,
      "media": {
        "id": "18045678901234567",
        "url": "https://www.instagram.com/p/C9xYz1AbCdE/"
      },
      "hasPrivateReply": false,
      "createdAt": "2026-09-24T10:02:15.000Z",
      "updatedAt": "2026-09-24T10:02:15.000Z"
    },
    "conversation": {
      "id": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
      "clientId": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
      "statusId": "5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e",
      "responsibleUserId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
      "clientGroupId": "1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e",
      "state": "unread",
      "lastMessageDirection": "incoming",
      "lastMessageAt": "2026-09-24T08:31:40.117Z",
      "snippet": "А доставка у Львів скільки йде?",
      "createdAt": "2026-08-14T09:21:37.512Z",
      "updatedAt": "2026-09-24T08:31:40.117Z"
    },
    "client": {
      "id": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
      "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
      "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "externalId": null,
      "name": "Олена Коваль",
      "username": "olena.koval",
      "imageUrl": "https://files.pager.co.ua/…/avatar.jpg",
      "phone": null,
      "infoName": "Олена",
      "infoLastName": "Коваль",
      "infoPhone": "+380671234567",
      "infoEmail": "olena@example.com",
      "infoAddress": "Київ, НП №52",
      "infoNote": "Цікавиться оптовими цінами",
      "createdAt": "2026-08-14T09:21:37.512Z",
      "updatedAt": "2026-09-23T15:02:11.840Z"
    },
    "channel": {
      "id": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
      "name": "Instagram магазину",
      "channelSource": "Instagram",
      "username": "kvitka.shop",
      "imageUrl": "https://files.pager.co.ua/…/channel.jpg"
    }
  }
}

In message.* events the objects use the same formats as the REST API: message, conversation without expand, and client. channel is the channel: id, name, channelSource, username, imageUrl. The objects are a snapshot at the time of the event.

In comment.received, data.comment is a comment without replies. For a client's reply in a thread, parentId points to the thread root. Reply with POST /conversations/{id}/comments.

Links in message.attachments[].url are temporary: they're signed at the moment of each delivery attempt, and expiresAt shows when they expire. If you need the file, download it right away.

Delivery order is not guaranteed: events are sent in parallel and retries shift them in time. Use message.createdAt to order messages.

Expect new fields to appear in the objects: don't reject an event because of an unknown field.

Webhooks

Verifying signatures

Pager signs every request with the subscription's secret. Verify the signature before trusting an event: a webhook URL is not a secret, and anyone can send a request to it.

Request from Pager
POST /pager/webhook HTTP/1.1
Host: crm.example.com
Content-Type: application/json
User-Agent: Pager-Webhooks/1.0
Pager-Event-Id: evt_6c1f0e9d8b7a45c3a2e1f0d9c8b7a6e5
Pager-Event-Type: message.received
Pager-Delivery: dlv_4b7e1f0a-9c2d-4e8b-a6f3-5d1c0b9e8a72
Pager-Attempt: 1
Pager-Signature: t=1758702700,v1=5f0c8a1e2b7d4c9f3a6e1b8d0c7f2a5e9b4d1c8f7a2e6b3d0c9f5a8e1b4d7c2a
HeaderWhen it happens
Pager-Signaturet=<unix time>,v1=<signature>, where v1 is the hex HMAC-SHA256 of the string <t>.<request body>, keyed with the subscription's secret. t is the time of that particular attempt, so a retry has a new one.
Pager-Event-IdThe event id. Same across all retries.
Pager-Event-TypeEvent type, same as type in the body.
Pager-DeliveryDelivery ID, dlv_… — the same id as in the delivery log.
Pager-AttemptAttempt number, starting at 1.
  1. 1

    Take the t and v1 values from the Pager-Signature header.

  2. 2

    Compute the HMAC-SHA256 of the string <t>.<body> with your secret. Use the raw body, byte for byte as it arrived: after parsing and re-serializing the JSON the signature won't match.

  3. 3

    Compare the result with v1 using a constant-time comparison (crypto.timingSafeEqual, hmac.compare_digest).

  4. 4

    Reject requests whose t differs from the current time by more than 5 minutes, so an intercepted request can't be replayed later.

Webhook handler
import crypto from "node:crypto"
import express from "express"

const app = express()
const seen = new Set()

// The signature covers the raw body: don't parse JSON before verifying
app.post("/pager/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("Pager-Signature") ?? ""
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")))

  // 1. Expected signature: HMAC-SHA256 of "t.body"
  const expected = crypto
    .createHmac("sha256", process.env.PAGER_WEBHOOK_SECRET)
    .update(`${t}.${req.body}`)
    .digest("hex")

  // 2. Constant-time comparison
  const valid =
    typeof v1 === "string" &&
    v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))

  // 3. No older than 5 minutes
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 5 * 60

  if (!valid || !fresh) return res.status(400).end()

  const event = JSON.parse(req.body)

  // 4. Retries carry the same event.id: process each event once
  if (!seen.has(event.id)) {
    seen.add(event.id)
    queue.push(event)
  }

  // 5. Respond 2xx right away and process the event in the background
  res.status(200).send("ok")
})
After you rotate the secret, the old one stops working immediately — there's no grace period. Deliveries already in the queue are signed with the new one too. Update the secret on your server right after rotating, or signature checks will fail.

Webhooks

Delivery and retries

A delivery succeeds if your server responds with any 2xx code within 10 seconds. Pager doesn't interpret the response body, it only stores its beginning in the log.

  • Anything else is a failure: another status code, 3xx (redirects aren't followed), a timeout or a connection error. Pager retries on the schedule below.
  • Respond as fast as you can: put the event in a queue, return 200, and process it afterwards. Long processing inside the request leads to timeouts and unnecessary retries.
  • Events are delivered at least once: the same event may arrive twice, for example if your response got lost in the network. Drop duplicates by the event id (Pager-Event-Id).
  • After the last failed attempt the delivery becomes dead and is no longer retried automatically. You can retry it manually in the dashboard or via the API.

Retry schedule

AttemptWhen
1right after the event
2+10 s
3+30 s
4+1 min
5+5 min
6+15 min
7+1 h
8+3 h
9+6 h

Each delay counts from the previous attempt and varies by ±20%, so subscriptions that failed together don't come back in a single wave. In total, up to 9 attempts over about 10 hours.

Automatic disabling

If no delivery to the subscription's URL succeeds for 7 days in a row, Pager disables the subscription: enabled becomes false and disabledReason becomes delivery_failures. Pager doesn't send an email about it: the state is visible in the dashboard and via GET /webhooks/{id}.

While a failure streak lasts, failingSince shows when it started and consecutiveFailures counts failed attempts in a row. The first successful delivery resets both. Test events don't count towards the streak.

To turn the subscription back on, click Enable in the dashboard or send enabled: true to PATCH /webhooks/{id}. The failure streak is reset, and deliveries still in the queue continue.

Every delivery is recorded in the log: status, attempt number, response code and first 2 KB of the response, duration. The log is kept for 30 days; in the dashboard, open it from the subscription menu under Deliveries & test.

Webhooks

Limits

The limits protect both your server and Pager: from excess load and from requests into internal networks.

  • Up to 10 subscriptions per organization. Creating one more returns 400 webhook_limit_reached.
  • https:// only. URLs up to 2000 characters, with no username or password in the address.
  • The URL must point to a public address. Local and private addresses (localhost, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 and so on) are rejected with 400 invalid_webhook_url. The address is checked when the subscription is saved and again before every delivery, so the DNS record can't be swapped after the subscription is created.
  • Redirects aren't followed: a 3xx response counts as a failure. Use the final URL.
  • The timeout is 10 seconds for the whole request, including reading the response.
  • Only the first 2 KB of the response body go into the log.
  • Subscription description — up to 500 characters.
  • Available events: message.received, message.sent and comment.received.

Resources

Organization

Details about your organization and the key that signed the request: plan, remaining messages, key mask and rate limits.

Response fields

  • organizationobject

    The organization's id, name and timezone. The time zone defaults to Europe/Kyiv.

  • planobject | null

    Current plan: name, maxUsers, maxChannels and endDate — the date it's paid through. null if there's no plan.

  • messagesobject | null

    Remaining messages: included from the plan, extra purchased on top; resetAt is when the plan's allowance renews.

  • apiKeyobject

    The request's key: name, mask (prefix + last4), scopes, createdAt and expiresAt. The full key is never returned.

  • rateLimitsobject

    Capacity and drain rate of each bucket — see Rate limits.

Retrieve organization

GET/v2/mecost 1

The best first request for an integration: a 200 means the key works.

curl "https://api.pager.co.ua/v2/me" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "organization": {
    "id": "org_2rRfXLYNflNpMgzPBX1MU18z39C",
    "name": "Магазин «Квітка»",
    "timezone": "Europe/Kyiv"
  },
  "plan": {
    "id": "b1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
    "name": "Business",
    "maxUsers": 10,
    "maxChannels": 5,
    "endDate": "2026-10-24T00:00:00.000Z"
  },
  "messages": {
    "included": 18240,
    "extra": 1500,
    "resetAt": "2026-10-01T00:00:00.000Z"
  },
  "apiKey": {
    "name": "Інтеграція з CRM",
    "prefix": "pgr_live_",
    "last4": "1JgQ",
    "scopes": [
      "*"
    ],
    "createdAt": "2026-09-12T09:03:44.000Z",
    "expiresAt": null
  },
  "rateLimits": {
    "general": {
      "capacity": 60,
      "ratePerSecond": 10
    },
    "outbound": {
      "capacity": 30,
      "ratePerSecond": 1
    }
  }
}

Resources

Conversations

A conversation is the thread with one client in one channel. The list is sorted by lastMessageAt, newest first. Besides reading, you can change a conversation's status, responsible manager, group and read state, and send messages into it.

  • Conversations with the SPAM status aren't hidden, unlike in the dashboard. If you don't need them, pass the statuses you want in statusId (plus null for conversations with no status).
  • A conversation with a new message moves to the top of the list. For syncing, use updatedSince — see Pagination.

The conversation object

  • idstring

    Conversation ID.

  • channelIdstring

    The channel the conversation is in.

  • clientIdstring

    The conversation's client — see Clients.

  • statusIdstring | null

    Status, or null if not set.

  • responsibleUserIdstring | null

    Responsible manager, or null.

  • clientGroupIdstring | null

    Client group, or null.

  • stateenum

    unread — there are unread incoming messages, read — all read.

  • lastMessageDirectionenum | null

    Who wrote last: incoming — the client, outgoing — your organization.

  • lastMessageAtISO 8601

    Time of the last message.

  • snippetstring

    Text of the last message, for previews.

  • createdAtISO 8601

    When the conversation was created.

  • updatedAtISO 8601

    When the conversation was last changed.

  • clientobject · expand

    The client object. Only with expand=client.

  • channelobject · expand

    Channel: id, name, channelSource, username, imageUrl. Only with expand=channel.

  • statusobject · expand

    The status object. Only with expand=status.

  • clientGroupobject · expand

    The client group object. Only with expand=clientGroup.

  • responsibleUserobject · expand

    Manager: id, firstName, lastName, imageUrl. Only with expand=responsibleUser.

The conversation object
{
  "id": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
  "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
  "clientId": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
  "statusId": "5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e",
  "responsibleUserId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
  "clientGroupId": "1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e",
  "state": "unread",
  "lastMessageDirection": "incoming",
  "lastMessageAt": "2026-09-24T08:31:40.117Z",
  "snippet": "А доставка у Львів скільки йде?",
  "createdAt": "2026-08-14T09:21:37.512Z",
  "updatedAt": "2026-09-24T08:31:40.117Z"
}

List conversations

GET/v2/conversationscost 2

Returns conversations page by page. The example below fetches unread conversations with no responsible manager, together with their client.

Parameters

  • channelIdstring[]queryoptional

    Only conversations in these channels.

  • statusIdstring[]queryoptional

    Only conversations with these statuses. null — conversations with no status.

  • responsibleUserIdstring[]queryoptional

    Only conversations of these managers. null — conversations with no responsible manager.

  • clientGroupIdstring[]queryoptional

    Only conversations in these groups. null — conversations with no group.

  • stateenumqueryoptional

    read or unread.

  • directionenumqueryoptional

    Who wrote last: incoming — the client (awaiting a reply), outgoing — your organization.

  • updatedSinceISO 8601queryoptional

    Only conversations changed since this moment. ISO 8601 with a time zone.

  • qstringqueryoptional

    Searches the conversation ID, last message text, and the client's name, username, phone, email and note, plus Zoho ID.

  • limitintegerqueryoptional

    Page size, from 1 to 100. Defaults to 50.

  • cursorstringqueryoptional

    nextCursor from the previous page.

  • expandstringqueryoptional

    Related objects, comma-separated: client, channel, status, clientGroup, responsibleUser.

curl "https://api.pager.co.ua/v2/conversations?state=unread&responsibleUserId=null&limit=20&expand=client" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
      "clientId": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
      "statusId": "5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e",
      "responsibleUserId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
      "clientGroupId": "1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e",
      "state": "unread",
      "lastMessageDirection": "incoming",
      "lastMessageAt": "2026-09-24T08:31:40.117Z",
      "snippet": "А доставка у Львів скільки йде?",
      "createdAt": "2026-08-14T09:21:37.512Z",
      "updatedAt": "2026-09-24T08:31:40.117Z",
      "client": {
        "id": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
        "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
        "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
        "externalId": null,
        "name": "Олена Коваль",
        "username": "olena.koval",
        "imageUrl": "https://files.pager.co.ua/…/avatar.jpg",
        "phone": null,
        "infoName": "Олена",
        "infoLastName": "Коваль",
        "infoPhone": "+380671234567",
        "infoEmail": "olena@example.com",
        "infoAddress": "Київ, НП №52",
        "infoNote": "Цікавиться оптовими цінами",
        "createdAt": "2026-08-14T09:21:37.512Z",
        "updatedAt": "2026-09-23T15:02:11.840Z"
      }
    }
  ],
  "hasMore": true,
  "nextCursor": "WyIyMDI2LTA5LTIzVDE2OjAyOjExLjg0MFoiLCIyYjljNGQ2ZS04ZjBhLTRjMmUtOWI0ZC02ZjhhMGMyZTRiNmQiXQ"
}

Count conversations

GET/v2/conversations/countcost 5

Takes the same filters as the list and returns just { count } — for an unread counter on a dashboard, say.

Parameters

  • channelIdstring[]queryoptional

    Only conversations in these channels.

  • statusIdstring[]queryoptional

    Only conversations with these statuses. null — conversations with no status.

  • responsibleUserIdstring[]queryoptional

    Only conversations of these managers. null — conversations with no responsible manager.

  • clientGroupIdstring[]queryoptional

    Only conversations in these groups. null — conversations with no group.

  • stateenumqueryoptional

    read or unread.

  • directionenumqueryoptional

    Who wrote last: incoming — the client (awaiting a reply), outgoing — your organization.

  • updatedSinceISO 8601queryoptional

    Only conversations changed since this moment. ISO 8601 with a time zone.

  • qstringqueryoptional

    Searches the conversation ID, last message text, and the client's name, username, phone, email and note, plus Zoho ID.

curl "https://api.pager.co.ua/v2/conversations/count?state=unread&responsibleUserId=null" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "count": 17
}

Retrieve a conversation

GET/v2/conversations/{id}cost 1

Returns one conversation. Accepts expand.

Parameters

  • idstringpathrequired

    Conversation ID.

  • expandstringqueryoptional

    Related objects, comma-separated: client, channel, status, clientGroup, responsibleUser.

curl "https://api.pager.co.ua/v2/conversations/2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d?expand=client,status,responsibleUser" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "id": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
  "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
  "clientId": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
  "statusId": "5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e",
  "responsibleUserId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
  "clientGroupId": "1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e",
  "state": "unread",
  "lastMessageDirection": "incoming",
  "lastMessageAt": "2026-09-24T08:31:40.117Z",
  "snippet": "А доставка у Львів скільки йде?",
  "createdAt": "2026-08-14T09:21:37.512Z",
  "updatedAt": "2026-09-24T08:31:40.117Z",
  "client": {
    "id": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
    "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
    "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
    "externalId": null,
    "name": "Олена Коваль",
    "username": "olena.koval",
    "imageUrl": "https://files.pager.co.ua/…/avatar.jpg",
    "phone": null,
    "infoName": "Олена",
    "infoLastName": "Коваль",
    "infoPhone": "+380671234567",
    "infoEmail": "olena@example.com",
    "infoAddress": "Київ, НП №52",
    "infoNote": "Цікавиться оптовими цінами",
    "createdAt": "2026-08-14T09:21:37.512Z",
    "updatedAt": "2026-09-23T15:02:11.840Z"
  },
  "status": {
    "id": "5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e",
    "name": "В роботі",
    "sortIndex": 0,
    "systemStatus": "IN_PROGRESS"
  },
  "responsibleUser": {
    "id": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
    "firstName": "Ірина",
    "lastName": "Шевчук",
    "imageUrl": null
  }
}

Update a conversation

PATCH/v2/conversations/{id}cost 1

Changes a conversation's status, responsible manager, group or read state. Only the fields you send change; null clears a field. Accepts expand, like retrieving a conversation.

Parameters

  • idstringpathrequired

    Conversation ID.

  • expandstringqueryoptional

    Related objects, comma-separated: client, channel, status, clientGroup, responsibleUser.

  • statusIdstring | nullbodyoptional

    New status, or null to clear it.

  • responsibleUserIdstring | nullbodyoptional

    New responsible manager — a member of your organization — or null to unassign. Manager IDs are in conversations' responsibleUserId and in expand=responsibleUser.

  • clientGroupIdstring | nullbodyoptional

    New group, or null to clear it.

  • stateenumbodyoptional

    read — mark as read, unread — mark as unread.

  • Status, group and manager IDs must belong to your organization, otherwise the API returns 400 with the matching param.
  • Status and responsible manager changes go into the conversation history with userId: null.
  • A body with no fields returns 400.
curl -X PATCH "https://api.pager.co.ua/v2/conversations/2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d?expand=status" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"statusId":"7c9e1a3b-5d7f-4a2c-8e6b-0d2f4a6c8e1b","state":"read"}'
Response · 200
{
  "id": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
  "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
  "clientId": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
  "statusId": "7c9e1a3b-5d7f-4a2c-8e6b-0d2f4a6c8e1b",
  "responsibleUserId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
  "clientGroupId": "1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e",
  "state": "read",
  "lastMessageDirection": "incoming",
  "lastMessageAt": "2026-09-24T08:31:40.117Z",
  "snippet": "А доставка у Львів скільки йде?",
  "createdAt": "2026-08-14T09:21:37.512Z",
  "updatedAt": "2026-09-24T09:02:17.408Z",
  "status": {
    "id": "7c9e1a3b-5d7f-4a2c-8e6b-0d2f4a6c8e1b",
    "name": "Оплачено",
    "sortIndex": 3,
    "systemStatus": "COMPLETED"
  }
}

Conversation history

GET/v2/conversations/{id}/historycost 2

Status and responsible manager changes in one feed, newest first, page by page.

Parameters

  • idstringpathrequired

    Conversation ID.

  • limitintegerqueryoptional

    Page size, from 1 to 100. Defaults to 50.

  • cursorstringqueryoptional

    nextCursor from the previous page.

  • type is status_changed (fields oldStatusId, newStatusId) or responsible_changed (fields oldResponsibleUserId, newResponsibleUserId).
  • userId is who made the change. null means it was made via the API or by automation (a broadcast, for example), not by a manager in the dashboard.
curl "https://api.pager.co.ua/v2/conversations/2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d/history" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "f0e1d2c3-b4a5-4968-8776-655443322110",
      "type": "status_changed",
      "userId": null,
      "oldStatusId": "7c9e1a3b-5d7f-4a2c-8e6b-0d2f4a6c8e1b",
      "newStatusId": "5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e",
      "createdAt": "2026-09-24T08:40:03.221Z"
    },
    {
      "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "type": "responsible_changed",
      "userId": "user_2rRfXLYNflNpMgzPBX1MU18z39C",
      "oldResponsibleUserId": null,
      "newResponsibleUserId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
      "createdAt": "2026-09-23T16:01:12.650Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Conversation orders

GET/v2/conversations/{id}/orderscost 2

Orders placed from this conversation, newest first. Returned in full, without pagination.

Parameters

  • idstringpathrequired

    Conversation ID.

  • amount is the order total, an integer.
  • crm is where the order was sent: LP_CRM, ZOHO or null; externalId is the order number in that CRM.
  • userId is the manager who placed the order.
curl "https://api.pager.co.ua/v2/conversations/2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d/orders" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
      "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "userId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
      "amount": 1450,
      "externalId": "184502",
      "crm": "LP_CRM",
      "createdAt": "2026-09-23T16:20:31.000Z",
      "updatedAt": "2026-09-23T16:20:31.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Conversation messages

GET/v2/conversations/{id}/messagescost 2

A conversation's messages page by page, newest first. Format: the message object.

Parameters

  • idstringpathrequired

    Conversation ID.

  • limitintegerqueryoptional

    Page size, from 1 to 100. Defaults to 50.

  • cursorstringqueryoptional

    nextCursor from the previous page.

  • updatedSinceISO 8601queryoptional

    Only messages changed since this moment (new, edited, or with a changed delivery status or reaction).

curl "https://api.pager.co.ua/v2/conversations/2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d/messages?limit=2" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a",
      "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "direction": "incoming",
      "text": "А доставка у Львів скільки йде?",
      "attachments": [],
      "authorId": null,
      "replyToMessageId": null,
      "externalId": "aWdfZAG1faXRlbTo…",
      "reaction": null,
      "isRead": false,
      "isEdited": false,
      "isDelivered": null,
      "errorMessage": null,
      "ad": null,
      "storyReplyUrl": null,
      "createdAt": "2026-09-24T08:31:40.117Z",
      "updatedAt": "2026-09-24T08:31:40.117Z"
    },
    {
      "id": "c9d8e7f6-a5b4-4c3d-9e2f-1a0b9c8d7e6f",
      "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "direction": "outgoing",
      "text": "Відправляємо Новою поштою по всій Україні, доставка 1–2 дні.",
      "attachments": [],
      "authorId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
      "replyToMessageId": null,
      "externalId": "aWdfZAG1faXRlbTo…",
      "reaction": null,
      "isRead": true,
      "isEdited": false,
      "isDelivered": true,
      "errorMessage": null,
      "ad": null,
      "storyReplyUrl": null,
      "createdAt": "2026-09-23T16:05:02.004Z",
      "updatedAt": "2026-09-23T16:05:02.004Z"
    }
  ],
  "hasMore": true,
  "nextCursor": "WyIyMDI2LTA5LTI0VDA4OjMxOjQwLjExN1oiLCJkMWUyZjNhNC1iNWM2LTRkN2UtOGY5YS0wYjFjMmQzZTRmNWEiXQ"
}

Send a message

POST/v2/conversations/{id}/messagescost 1outbound 1

Sends text or a file to the client on behalf of your organization — through the conversation's channel, just like a manager does from the dashboard. Returns the stored message with a 201.

Parameters

  • idstringpathrequired

    Conversation ID.

  • Idempotency-Keystringheaderoptional

    A key that makes a retry not send the message twice — see Idempotency.

  • textstringbodyoptional

    Message text, 1 to 4096 characters. Not sent together with attachments.

  • attachmentsobject[]bodyoptional

    An array with one attachment — the attachment object from POST /files, unchanged. An attachment from another conversation or your own file URL returns 400 invalid_attachment.

  • replyToMessageIdstringbodyoptional

    A message in this conversation you're replying to. A message from another conversation returns 400.

  • Pass either text or attachments with one file — not both, otherwise 400. For uploading a file, see Sending files.
  • Text is up to 4096 characters. Some messengers have a lower limit — then the refusal comes back as 422.
  • authorId in the response is null: the message was sent via the API, not by a manager.
  • Send an Idempotency-Key: if the response got lost to a timeout, a retry with the same key won't message the client twice.
  • The plan is checked before sending: if it has expired or the message balance is exhausted — 402 message_limit_reached. A sent message counts towards the allowance.
  • Sending works for Instagram, Facebook Messenger, Telegram bots, personal Telegram, Viber and WhatsApp via e-chat, website chat and custom channels. Other channels return 422 channel_unsupported.
  • If the messenger refuses (for example, too much time has passed since the client's last message on Instagram or Facebook), the API returns 422 channel_rejected. The message is still saved as undelivered — managers see it in the dashboard, and its id is in the error text.
  • After sending, the conversation becomes read and moves to the top of the list.
  • Besides a cost of 1 in the general bucket, it takes 1 from the outbound bucket — see Rate limits.
curl -X POST "https://api.pager.co.ua/v2/conversations/2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d/messages" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Idempotency-Key: a3f1c9e2-5b7d-4e8a-9c0b-1d2e3f4a5b6c" \
  -H "Content-Type: application/json" \
  -d '{"text":"Доставка у Львів — 1–2 дні, від 80 грн. Оформити замовлення?","replyToMessageId":"d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a"}'
Response · 201
{
  "id": "e5f6a7b8-c9d0-4e1f-a2b3-c4d5e6f7a8b9",
  "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
  "direction": "outgoing",
  "text": "Доставка у Львів — 1–2 дні, від 80 грн. Оформити замовлення?",
  "attachments": [],
  "authorId": null,
  "replyToMessageId": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a",
  "externalId": "aWdfZAG1faXRlbTo…",
  "reaction": null,
  "isRead": false,
  "isEdited": false,
  "isDelivered": true,
  "errorMessage": null,
  "ad": null,
  "storyReplyUrl": null,
  "createdAt": "2026-09-24T08:34:02.771Z",
  "updatedAt": "2026-09-24T08:34:02.771Z"
}

Resources

Messages

Conversation messages. To read a thread, use conversation messages; this resource is for organization-wide search and retrieving a single message.

  • An organization-wide search is heavier than reading one conversation, so it costs 5. If you know the conversation, read its messages instead.
  • attachments[].url links are temporary; they expire at expiresAt.

The message object

  • idstring

    Message ID.

  • conversationIdstring

    The conversation the message belongs to.

  • directionenum

    incoming — from the client, outgoing — from your organization.

  • textstring | null

    Text, or null if the message only has attachments.

  • attachmentsobject[]

    Attachments: type (image, video, audio or document), url, name, mime, size in bytes and expiresAt.

  • authorIdstring | null

    The manager who sent the message. null for incoming messages, and for outgoing ones sent via the API, a broadcast or automation.

  • replyToMessageIdstring | null

    The message this one replies to, or null.

  • externalIdstring | null

    The message ID in the messenger.

  • reactionstring | null

    A reaction to the message, such as an emoji, or null.

  • isReadboolean

    Whether the message has been read.

  • isEditedboolean

    Whether the message was edited.

  • isDeliveredboolean | null

    true / false — the messenger's delivery report. null — the messenger doesn't report status (Viber and WhatsApp via e-chat), or the message is incoming.

  • errorMessagestring | null

    Why the message wasn't delivered, or null.

  • adobject | null

    The ad the client wrote from: id, url, text. null if the message didn't come from an ad.

  • storyReplyUrlstring | null

    The story the client replied to, or null.

  • createdAtISO 8601

    When the message was sent.

  • updatedAtISO 8601

    When the message was last changed.

The message object
{
  "id": "c9d8e7f6-a5b4-4c3d-9e2f-1a0b9c8d7e6f",
  "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
  "direction": "outgoing",
  "text": "Відправляємо Новою поштою по всій Україні, доставка 1–2 дні.",
  "attachments": [],
  "authorId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
  "replyToMessageId": null,
  "externalId": "aWdfZAG1faXRlbTo…",
  "reaction": null,
  "isRead": true,
  "isEdited": false,
  "isDelivered": true,
  "errorMessage": null,
  "ad": null,
  "storyReplyUrl": null,
  "createdAt": "2026-09-23T16:05:02.004Z",
  "updatedAt": "2026-09-23T16:05:02.004Z"
}

Search messages

GET/v2/messagescost 5

Searches messages across all of your organization's conversations, newest first. The example below finds incoming messages since September that mention “розмір” (size).

Parameters

  • qstringqueryoptional

    Case-insensitive search in message text.

  • channelIdstring[]queryoptional

    Only messages from conversations in these channels.

  • directionenumqueryoptional

    incoming — from clients, outgoing — from your organization.

  • fromISO 8601queryoptional

    Start of the period by createdAt, inclusive. ISO 8601.

  • toISO 8601queryoptional

    End of the period by createdAt, inclusive. ISO 8601.

  • updatedSinceISO 8601queryoptional

    Only messages changed since this moment.

  • limitintegerqueryoptional

    Page size, from 1 to 100. Defaults to 50.

  • cursorstringqueryoptional

    nextCursor from the previous page.

curl "https://api.pager.co.ua/v2/messages?q=розмір&direction=incoming&from=2026-09-01T00:00:00Z" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
      "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "direction": "incoming",
      "text": "Доброго дня! Чи є в наявності розмір M?",
      "attachments": [],
      "authorId": null,
      "replyToMessageId": null,
      "externalId": "aWdfZAG1faXRlbTo…",
      "reaction": null,
      "isRead": false,
      "isEdited": false,
      "isDelivered": null,
      "errorMessage": null,
      "ad": {
        "id": "120214567890123456",
        "url": "https://fb.me/…",
        "text": "Осіння колекція −20%"
      },
      "storyReplyUrl": null,
      "createdAt": "2026-09-23T15:58:44.910Z",
      "updatedAt": "2026-09-23T15:58:44.910Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Retrieve a message

GET/v2/messages/{id}cost 1

Returns one message.

Parameters

  • idstringpathrequired

    Message ID.

curl "https://api.pager.co.ua/v2/messages/d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a",
  "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
  "direction": "incoming",
  "text": "А доставка у Львів скільки йде?",
  "attachments": [],
  "authorId": null,
  "replyToMessageId": null,
  "externalId": "aWdfZAG1faXRlbTo…",
  "reaction": null,
  "isRead": false,
  "isEdited": false,
  "isDelivered": null,
  "errorMessage": null,
  "ad": null,
  "storyReplyUrl": null,
  "createdAt": "2026-09-24T08:31:40.117Z",
  "updatedAt": "2026-09-24T08:31:40.117Z"
}

Resources

Comments

Clients' comments under Instagram posts and the replies to them. Each comment belongs to the conversation with its author, so comments are read and written within a conversation. The tree has two levels: a thread is a client's comment under a post, and replies are the replies in it, oldest first. Threads are returned newest first.

  • Pager currently collects comments from Instagram. For conversations in other channels the list is empty, and replying returns 422 channel_unsupported.
  • Pager can notify you about new client comments with the `comment.received` webhook event.

Thread object

  • idstring

    Comment ID in Pager.

  • conversationIdstring

    The conversation with the comment's author.

  • parentIdstring | null

    The thread root for a reply, or null for the root itself.

  • directionenum

    incoming — a client's comment, outgoing — a reply on behalf of the page.

  • textstring

    Comment text.

  • usernamestring

    Who wrote it: the client's username, or the channel name for page replies.

  • authorIdstring | null

    The manager who replied. null for client comments and for replies sent via the API.

  • mediaobject | null

    The post the comment is under: id and url. null if unknown.

  • hasPrivateReplyboolean

    Whether a private reply has already been sent to the author's DMs. Meta allows this only once per comment.

  • createdAtISO 8601

    When the comment was written.

  • updatedAtISO 8601

    When the comment was last changed.

  • repliesobject[]

    Threads only: replies to the comment — same fields, oldest first.

Thread object
{
  "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
  "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
  "parentId": null,
  "direction": "incoming",
  "text": "Скільки коштує доставка у Львів?",
  "username": "olena.koval",
  "authorId": null,
  "media": {
    "id": "18045678901234567",
    "url": "https://www.instagram.com/p/C9xYz1AbCdE/"
  },
  "hasPrivateReply": false,
  "createdAt": "2026-09-24T10:02:15.000Z",
  "updatedAt": "2026-09-24T10:02:15.000Z",
  "replies": [
    {
      "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
      "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "parentId": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
      "direction": "outgoing",
      "text": "Доставка у Львів — від 80 грн, 1–2 дні. Написали вам у директ 🙌",
      "username": "kvitka.shop",
      "authorId": null,
      "media": {
        "id": "18045678901234567",
        "url": "https://www.instagram.com/p/C9xYz1AbCdE/"
      },
      "hasPrivateReply": false,
      "createdAt": "2026-09-24T10:05:41.000Z",
      "updatedAt": "2026-09-24T10:05:41.000Z"
    }
  ]
}

Conversation comments

GET/v2/conversations/{id}/commentscost 2

Returns all comment threads of the conversation with their replies, without pagination.

Parameters

  • idstringpathrequired

    Conversation ID.

curl "https://api.pager.co.ua/v2/conversations/2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d/comments" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
      "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "parentId": null,
      "direction": "incoming",
      "text": "Скільки коштує доставка у Львів?",
      "username": "olena.koval",
      "authorId": null,
      "media": {
        "id": "18045678901234567",
        "url": "https://www.instagram.com/p/C9xYz1AbCdE/"
      },
      "hasPrivateReply": false,
      "createdAt": "2026-09-24T10:02:15.000Z",
      "updatedAt": "2026-09-24T10:02:15.000Z",
      "replies": [
        {
          "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
          "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
          "parentId": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
          "direction": "outgoing",
          "text": "Доставка у Львів — від 80 грн, 1–2 дні. Написали вам у директ 🙌",
          "username": "kvitka.shop",
          "authorId": null,
          "media": {
            "id": "18045678901234567",
            "url": "https://www.instagram.com/p/C9xYz1AbCdE/"
          },
          "hasPrivateReply": false,
          "createdAt": "2026-09-24T10:05:41.000Z",
          "updatedAt": "2026-09-24T10:05:41.000Z"
        }
      ]
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Reply to a comment

POST/v2/conversations/{id}/commentscost 1outbound 1

Replies to a comment publicly under the post or privately in the author's DMs. Responds with 201 and a visibility field: for public, the new reply is in comment; for private, the sent message is in message and the updated comment is in comment (hasPrivateReply: true).

Parameters

  • idstringpathrequired

    Conversation ID.

  • Idempotency-Keystringheaderoptional

    A key that keeps a retried request from sending the reply twice — see Idempotency.

  • replyToCommentIdstringbodyrequired

    id of a comment in this conversation that you're replying to.

  • visibilityenumbodyrequired

    public — a public reply under the post on behalf of the page, private — a direct message to the comment's author.

  • textstringbodyrequired

    Reply text, 1 to 2000 characters.

  • A public reply always goes to the thread root: Instagram doesn't allow replying to a reply. If replyToCommentId is a reply, Pager replies to its root.
  • A private reply is only possible to a client's comment (otherwise 400 not_a_client_comment) and only once: a second one returns 409 private_reply_exists. It shows up in the conversation as a regular outgoing message and triggers the `message.sent` webhook.
  • The plan is checked, as for sending a message: an expired plan returns 402 message_limit_reached.
  • If Meta refuses, the API returns 422 channel_rejected with the reason. A failed private reply is saved as an undelivered message, and its id is in the error text.
  • Accepts Idempotency-Key. Besides a cost of 1 in the general bucket, it takes 1 from the outbound bucket.
curl -X POST "https://api.pager.co.ua/v2/conversations/2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d/comments" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Idempotency-Key: a3f1c9e2-5b7d-4e8a-9c0b-1d2e3f4a5b6c" \
  -H "Content-Type: application/json" \
  -d '{"replyToCommentId":"6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a","visibility":"private","text":"Вітаємо! Доставка у Львів — 1–2 дні, від 80 грн. Оформити замовлення?"}'
Response · 201
{
  "visibility": "private",
  "message": {
    "id": "0a9b8c7d-6e5f-4a3b-9c2d-1e0f2a3b4c5d",
    "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
    "direction": "outgoing",
    "text": "Вітаємо! Доставка у Львів — 1–2 дні, від 80 грн. Оформити замовлення?",
    "attachments": [],
    "authorId": null,
    "replyToMessageId": null,
    "externalId": "aWdfZAG1faXRlbTo…",
    "reaction": null,
    "isRead": false,
    "isEdited": false,
    "isDelivered": true,
    "errorMessage": null,
    "ad": null,
    "storyReplyUrl": null,
    "createdAt": "2026-09-24T10:06:02.114Z",
    "updatedAt": "2026-09-24T10:06:02.114Z"
  },
  "comment": {
    "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
    "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
    "parentId": null,
    "direction": "incoming",
    "text": "Скільки коштує доставка у Львів?",
    "username": "olena.koval",
    "authorId": null,
    "media": {
      "id": "18045678901234567",
      "url": "https://www.instagram.com/p/C9xYz1AbCdE/"
    },
    "hasPrivateReply": true,
    "createdAt": "2026-09-24T10:02:15.000Z",
    "updatedAt": "2026-09-24T10:06:02.530Z"
  }
}

Delete a comment

DELETE/v2/comments/{id}cost 1outbound 1

Deletes the comment on Instagram and in Pager together with all replies to it, and responds with 204.

Parameters

  • idstringpathrequired

    Conversation ID.

  • The comment is deleted in Meta first. If Meta refuses — 422 channel_rejected, and nothing is deleted in Pager.
  • Takes 1 from the outbound bucket, like a reply.
curl -X DELETE "https://api.pager.co.ua/v2/comments/6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 204· No response body

Resources

Files

Uploading a file to Pager's storage so you can send it to a client. The API doesn't accept the file directly: POST /files returns a temporary link to upload the file to, and a ready-made attachment object for the message. Step by step — see Sending files.

  • Up to 20 MB. Images, video and audio (except SVG) are allowed, plus PDF, ZIP, DOC/DOCX, XLS/XLSX, PPT/PPTX, TXT and CSV. Other types return 400 with param: "mime".
  • The file is bound to the conversation in conversationId: it can only be sent to that conversation.

Response fields

  • uploadUrlstring

    Temporary link for uploading the file.

  • methodstring

    HTTP method for the upload — PUT.

  • headersobject

    Headers to send with the upload, verbatim. Content-Type is part of the link's signature.

  • expiresIninteger

    How many seconds the link is valid — 300.

  • expiresAtISO 8601

    The moment by which the upload has to start.

  • maxBytesinteger

    Maximum file size in bytes.

  • attachmentobject

    A ready-made attachment: after the upload, pass this object unchanged in attachments when sending a message.

Get an upload link

POST/v2/filescost 1

Returns an upload link and a ready-made attachment object with 201.

Parameters

  • conversationIdstringbodyrequired

    The conversation you'll send the file to.

  • namestringbodyrequired

    File name, up to 255 characters. The client sees it in the messenger.

  • mimestringbodyrequired

    The file's MIME type, e.g. application/pdf or image/jpeg.

  • sizeintegerbodyrequired

    File size in bytes, up to 20 MB.

  • The link is valid for 5 minutes — that's the time to start the upload. A slow upload of a large file won't be cut off.
  • Upload with a PUT request to uploadUrl using the headers from headers, without Authorization. The request body is the file itself.
  • Size and type are checked again when the message is sent — against the file actually uploaded.
curl -X POST "https://api.pager.co.ua/v2/files" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"conversationId":"2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d","name":"invoice-184502.pdf","mime":"application/pdf","size":248193}'
Response · 201
{
  "uploadUrl": "https://files.pager.co.ua/…/7f3e2d1c-0b9a-4c8d-9e7f-6a5b4c3d2e1f?X-Amz-Expires=300&X-Amz-Signature=…",
  "method": "PUT",
  "headers": {
    "Content-Type": "application/pdf"
  },
  "expiresIn": 300,
  "maxBytes": 20971520,
  "attachment": {
    "type": "document",
    "payload": {
      "key": "org_2rRfXLYNflNpMgzPBX1MU18z39C/2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d/7f3e2d1c-0b9a-4c8d-9e7f-6a5b4c3d2e1f",
      "name": "invoice-184502.pdf",
      "mime": "application/pdf",
      "size": 248193
    }
  },
  "expiresAt": "2026-09-24T08:44:12.000Z"
}

Resources

Clients

A client is a person who wrote to one of your organization's channels. Each client has one conversation. The list is sorted by creation date, newest first. You can fill in the client card via the API.

  • phone is the client's number in the messenger (Telegram, Viber, WhatsApp). The info* fields are the client card filled in by a manager or a CRM.
  • Internal platform IDs (PSID, Telegram ID) are never returned.

The client object

  • idstring

    Client ID.

  • channelIdstring

    The channel the client wrote to.

  • conversationIdstring | null

    The conversation with the client.

  • externalIdstring | null

    The client's ID in an external system, or null.

  • namestring | null

    Name from the messenger profile.

  • usernamestring | null

    Messenger username.

  • imageUrlstring | null

    Avatar.

  • phonestring | null

    Number in the messenger, if known.

  • infoNamestring | null

    First name on the client card.

  • infoLastNamestring | null

    Last name on the client card.

  • infoPhonestring | null

    Phone on the client card.

  • infoEmailstring | null

    Email on the client card.

  • infoAddressstring | null

    Address on the client card.

  • infoNotestring | null

    Manager's note.

  • createdAtISO 8601

    When the client first wrote.

  • updatedAtISO 8601

    When the client's data was last changed.

The client object
{
  "id": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
  "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
  "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
  "externalId": null,
  "name": "Олена Коваль",
  "username": "olena.koval",
  "imageUrl": "https://files.pager.co.ua/…/avatar.jpg",
  "phone": null,
  "infoName": "Олена",
  "infoLastName": "Коваль",
  "infoPhone": "+380671234567",
  "infoEmail": "olena@example.com",
  "infoAddress": "Київ, НП №52",
  "infoNote": "Цікавиться оптовими цінами",
  "createdAt": "2026-08-14T09:21:37.512Z",
  "updatedAt": "2026-09-23T15:02:11.840Z"
}

List clients

GET/v2/clientscost 2

Returns clients page by page. The example below searches by phone number.

Parameters

  • channelIdstring[]queryoptional

    Only clients from these channels.

  • clientGroupIdstring[]queryoptional

    Only clients whose conversation is in these groups. null — clients with no group.

  • updatedSinceISO 8601queryoptional

    Only clients changed since this moment.

  • qstringqueryoptional

    Searches name, username, external ID, messenger phone numbers and card fields: first name, last name, phone, email.

  • limitintegerqueryoptional

    Page size, from 1 to 100. Defaults to 50.

  • cursorstringqueryoptional

    nextCursor from the previous page.

curl "https://api.pager.co.ua/v2/clients?q=0671234567" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
      "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
      "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
      "externalId": null,
      "name": "Олена Коваль",
      "username": "olena.koval",
      "imageUrl": "https://files.pager.co.ua/…/avatar.jpg",
      "phone": null,
      "infoName": "Олена",
      "infoLastName": "Коваль",
      "infoPhone": "+380671234567",
      "infoEmail": "olena@example.com",
      "infoAddress": "Київ, НП №52",
      "infoNote": "Цікавиться оптовими цінами",
      "createdAt": "2026-08-14T09:21:37.512Z",
      "updatedAt": "2026-09-23T15:02:11.840Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Retrieve a client

GET/v2/clients/{id}cost 1

Returns one client.

Parameters

  • idstringpathrequired

    Client ID.

curl "https://api.pager.co.ua/v2/clients/3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "id": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
  "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
  "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
  "externalId": null,
  "name": "Олена Коваль",
  "username": "olena.koval",
  "imageUrl": "https://files.pager.co.ua/…/avatar.jpg",
  "phone": null,
  "infoName": "Олена",
  "infoLastName": "Коваль",
  "infoPhone": "+380671234567",
  "infoEmail": "olena@example.com",
  "infoAddress": "Київ, НП №52",
  "infoNote": "Цікавиться оптовими цінами",
  "createdAt": "2026-08-14T09:21:37.512Z",
  "updatedAt": "2026-09-23T15:02:11.840Z"
}

Update a client

PATCH/v2/clients/{id}cost 1

Fills in the client card. Only the fields you send change; null clears a field, and surrounding whitespace is trimmed.

Parameters

  • idstringpathrequired

    Client ID.

  • infoNamestring | nullbodyoptional

    First name on the card, up to 255 characters.

  • infoLastNamestring | nullbodyoptional

    Last name on the card, up to 255 characters.

  • infoPhonestring | nullbodyoptional

    Phone on the card, up to 50 characters.

  • infoEmailstring | nullbodyoptional

    Email on the card. Must be a valid address, otherwise 400.

  • infoAddressstring | nullbodyoptional

    Address on the card, up to 500 characters.

  • infoNotestring | nullbodyoptional

    Note, up to 8000 characters.

  • externalIdstring | nullbodyoptional

    The client's ID in your system, up to 255 characters, unique within the channel.

  • If the Zoho CRM integration is connected and the first name, last name, phone or email changed, the card is synced to Zoho — same as after an edit in the dashboard.
  • An externalId taken by another client in this channel returns 409 external_id_taken.
  • For custom channel clients, externalId is the ID Pager uses to find the client and deliver replies. Only change it if the ID on your platform changed.
  • A body with no fields returns 400.
curl -X PATCH "https://api.pager.co.ua/v2/clients/3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"infoAddress":"Львів, НП №12","infoNote":"Оптовий клієнт, знижка 10%","externalId":"crm-10482"}'
Response · 200
{
  "id": "3b0f6c1e-5d2a-4f7e-9c41-2a8e7d9b1f03",
  "channelId": "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
  "conversationId": "2b9c4d6e-8f0a-4c2e-9b4d-6f8a0c2e4b6d",
  "externalId": "crm-10482",
  "name": "Олена Коваль",
  "username": "olena.koval",
  "imageUrl": "https://files.pager.co.ua/…/avatar.jpg",
  "phone": null,
  "infoName": "Олена",
  "infoLastName": "Коваль",
  "infoPhone": "+380671234567",
  "infoEmail": "olena@example.com",
  "infoAddress": "Львів, НП №12",
  "infoNote": "Оптовий клієнт, знижка 10%",
  "createdAt": "2026-08-14T09:21:37.512Z",
  "updatedAt": "2026-09-24T09:05:41.132Z"
}

Resources

Members

The organization's managers and admins. userId is the same ID as in conversations' responsibleUserId, messages' authorId and history's userId. Via the API you can read members and change which channels they see.

  • Inviting or removing members and changing their role is only possible in the dashboard. A role field in PATCH returns 400 unknown_parameter.
  • Returned in full, without pagination, in the order they joined the organization.

Member object

  • userIdstring

    User ID, user_….

  • firstNamestring | null

    First name, or null.

  • lastNamestring | null

    Last name, or null.

  • emailsstring[]

    The user's email addresses.

  • imageUrlstring | null

    Avatar, or null.

  • rolestring

    Role in the organization: org:admin — admin, org:member — manager.

  • accessibleChannelsstring[]

    Channels whose conversations the member sees in the dashboard. Conversations from other channels are hidden from them.

  • createdAtISO 8601

    When the user signed up to Pager.

Member object
{
  "userId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
  "firstName": "Ірина",
  "lastName": "Шевчук",
  "emails": [
    "iryna@kvitka.shop"
  ],
  "imageUrl": null,
  "role": "org:member",
  "accessibleChannels": [
    "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a"
  ],
  "createdAt": "2026-08-02T09:40:11.000Z"
}

List members

GET/v2/memberscost 2

Returns all members of the organization.

curl "https://api.pager.co.ua/v2/members" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "userId": "user_2rRfXLYNflNpMgzPBX1MU18z39C",
      "firstName": "Андрій",
      "lastName": "Мельник",
      "emails": [
        "owner@kvitka.shop"
      ],
      "imageUrl": "https://img.clerk.com/…",
      "role": "org:admin",
      "accessibleChannels": [
        "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
        "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c0d"
      ],
      "createdAt": "2026-07-18T12:03:27.000Z"
    },
    {
      "userId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
      "firstName": "Ірина",
      "lastName": "Шевчук",
      "emails": [
        "iryna@kvitka.shop"
      ],
      "imageUrl": null,
      "role": "org:member",
      "accessibleChannels": [
        "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a"
      ],
      "createdAt": "2026-08-02T09:40:11.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Retrieve a member

GET/v2/members/{userId}cost 1

Returns a single member.

Parameters

  • userIdstringpathrequired

    User ID, user_….

curl "https://api.pager.co.ua/v2/members/user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "userId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
  "firstName": "Ірина",
  "lastName": "Шевчук",
  "emails": [
    "iryna@kvitka.shop"
  ],
  "imageUrl": null,
  "role": "org:member",
  "accessibleChannels": [
    "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a"
  ],
  "createdAt": "2026-08-02T09:40:11.000Z"
}

Change channel access

PATCH/v2/members/{userId}cost 1

Replaces the list of channels the member sees and returns the updated member.

Parameters

  • userIdstringpathrequired

    User ID, user_….

  • accessibleChannelsstring[]bodyrequired

    The full new list of channels, up to 500. The member stops seeing channels not in the list; an empty array hides all channels.

  • This is a full replacement, not an addition: to open one more channel, send the current list plus that channel.
  • Every channel must belong to your organization, otherwise 400 channel_not_found with param: "accessibleChannels". Duplicates are removed.
  • Channel IDs are available in the channelId of conversations and clients.
curl -X PATCH "https://api.pager.co.ua/v2/members/user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"accessibleChannels":["e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a","a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c0d"]}'
Response · 200
{
  "userId": "user_2sKq8vB3nT1mZ7wYcR4dF6hJ9pL",
  "firstName": "Ірина",
  "lastName": "Шевчук",
  "emails": [
    "iryna@kvitka.shop"
  ],
  "imageUrl": null,
  "role": "org:member",
  "accessibleChannels": [
    "e4b2a1c9-6d8f-4e3a-b7c5-0f9e8d7c6b5a",
    "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c0d"
  ],
  "createdAt": "2026-08-02T09:40:11.000Z"
}

Resources

Statuses

Conversation statuses — the stages managers mark conversations with in the dashboard. Returned in sortIndex order, same as in the dashboard.

  • Deleting a status doesn't delete conversations: they're left without a status, just like when you delete it in the dashboard.

The status object

  • idstring

    Status ID.

  • namestring

    Name, up to 100 characters.

  • sortIndexinteger

    Position in the list, from 0.

  • systemStatusenum | null

    System category: IN_PROGRESS, CONSIDERING, COMPLETED, DECLINED, SPAM, or null for a regular status.

The status object
{
  "id": "5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e",
  "name": "В роботі",
  "sortIndex": 0,
  "systemStatus": "IN_PROGRESS"
}

List statuses

GET/v2/statusescost 2

Returns all of your organization's statuses.

curl "https://api.pager.co.ua/v2/statuses" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e",
      "name": "В роботі",
      "sortIndex": 0,
      "systemStatus": "IN_PROGRESS"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Create a status

POST/v2/statusescost 1

Creates a status and returns it with a 201.

Parameters

  • namestringbodyrequired

    Status name, 1 to 100 characters.

  • sortIndexintegerbodyoptional

    Position in the list, from 0. If omitted on create, the status goes last.

  • systemStatusenum | nullbodyoptional

    System category (IN_PROGRESS, CONSIDERING, COMPLETED, DECLINED, SPAM) or null.

curl -X POST "https://api.pager.co.ua/v2/statuses" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Чекає на оплату","systemStatus":"CONSIDERING"}'
Response · 201
{
  "id": "5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e",
  "name": "Чекає на оплату",
  "sortIndex": 0,
  "systemStatus": "CONSIDERING"
}

Update a status

PATCH/v2/statuses/{id}cost 1

Changes only the fields you send. A body with no fields returns 400.

Parameters

  • idstringpathrequired

    Status ID.

  • namestringbodyoptional

    Status name, 1 to 100 characters.

  • sortIndexintegerbodyoptional

    Position in the list, from 0. If omitted on create, the status goes last.

  • systemStatusenum | nullbodyoptional

    System category (IN_PROGRESS, CONSIDERING, COMPLETED, DECLINED, SPAM) or null.

curl -X PATCH "https://api.pager.co.ua/v2/statuses/5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Оплачено","systemStatus":"COMPLETED"}'
Response · 200
{
  "id": "5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e",
  "name": "Оплачено",
  "sortIndex": 0,
  "systemStatus": "COMPLETED"
}

Delete a status

DELETE/v2/statuses/{id}cost 1

Deletes the status and responds with 204. Its conversations are left without a status.

Parameters

  • idstringpathrequired

    Status ID.

curl -X DELETE "https://api.pager.co.ua/v2/statuses/5a7e9c1d-3f2b-4d8a-b6e0-9c1f2a3b4d5e" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 204· No response body

Resources

Client groups

Groups managers use to tag clients in the dashboard, like “Wholesale” or “VIP”. Returned in sortIndex order.

  • Deleting a group doesn't delete conversations: they're left without a group, just like when you delete it in the dashboard.

The client group object

  • idstring

    Group ID.

  • namestring

    Name, up to 100 characters.

  • colorstring

    Color as #rrggbb — exactly how the dashboard stores it.

  • sortIndexinteger

    Position in the list, from 0.

The client group object
{
  "id": "1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e",
  "name": "Опт",
  "color": "#7c5cff",
  "sortIndex": 0
}

List client groups

GET/v2/client-groupscost 2

Returns all of your organization's client groups.

curl "https://api.pager.co.ua/v2/client-groups" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e",
      "name": "Опт",
      "color": "#7c5cff",
      "sortIndex": 0
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Create a client group

POST/v2/client-groupscost 1

Creates a group and returns it with a 201.

Parameters

  • namestringbodyrequired

    Group name, 1 to 100 characters.

  • colorstringbodyrequired

    Color as #rrggbb, e.g. #7c5cff. Other formats (red, #fff, rgb(…)) return 400.

  • sortIndexintegerbodyoptional

    Position in the list, from 0. If omitted on create, the group goes last.

curl -X POST "https://api.pager.co.ua/v2/client-groups" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"VIP","color":"#f59e0b"}'
Response · 201
{
  "id": "1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e",
  "name": "VIP",
  "color": "#f59e0b",
  "sortIndex": 0
}

Update a client group

PATCH/v2/client-groups/{id}cost 1

Changes only the fields you send. A body with no fields returns 400.

Parameters

  • idstringpathrequired

    Group ID.

  • namestringbodyoptional

    Group name, 1 to 100 characters.

  • colorstringbodyoptional

    Color as #rrggbb, e.g. #7c5cff. Other formats (red, #fff, rgb(…)) return 400.

  • sortIndexintegerbodyoptional

    Position in the list, from 0. If omitted on create, the group goes last.

curl -X PATCH "https://api.pager.co.ua/v2/client-groups/1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"color":"#16a34a"}'
Response · 200
{
  "id": "1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e",
  "name": "Опт",
  "color": "#16a34a",
  "sortIndex": 0
}

Delete a client group

DELETE/v2/client-groups/{id}cost 1

Deletes the group and responds with 204. Its conversations are left without a group.

Parameters

  • idstringpathrequired

    Group ID.

curl -X DELETE "https://api.pager.co.ua/v2/client-groups/1c3e5a7b-9d2f-4b6c-8e0a-2f4d6b8a0c1e" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 204· No response body

Resources

Saved reply folders

Folders that group saved replies. Returned in sortIndex order.

  • Deleting a folder also deletes all of its saved replies, just like in the dashboard. They can't be restored.
  • updatedSince returns only folders changed since that moment — handy for syncing. Deleted folders aren't in the response: compare against the full list to spot them.

The folder object

  • idstring

    Folder ID.

  • namestring

    Name, up to 100 characters.

  • sortIndexinteger

    Position in the list, from 0.

  • createdAtISO 8601

    When the folder was created.

  • updatedAtISO 8601

    When the folder was last changed.

The folder object
{
  "id": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
  "name": "Доставка",
  "sortIndex": 0,
  "createdAt": "2026-08-02T10:15:00.000Z",
  "updatedAt": "2026-09-20T08:41:27.000Z"
}

List folders

GET/v2/saved-reply-folderscost 2

Returns your organization's saved reply folders.

Parameters

  • updatedSinceISO 8601queryoptional

    Return only folders updated since this moment. ISO 8601 with a time zone, e.g. 2026-09-01T00:00:00Z.

curl "https://api.pager.co.ua/v2/saved-reply-folders?updatedSince=2026-09-01T00:00:00Z" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
      "name": "Доставка",
      "sortIndex": 0,
      "createdAt": "2026-08-02T10:15:00.000Z",
      "updatedAt": "2026-09-20T08:41:27.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Create a folder

POST/v2/saved-reply-folderscost 1

Creates an empty folder and returns it with a 201.

Parameters

  • namestringbodyrequired

    Folder name, 1 to 100 characters.

  • sortIndexintegerbodyoptional

    Position in the list, from 0. If omitted on create, the folder goes last.

curl -X POST "https://api.pager.co.ua/v2/saved-reply-folders" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Оплата"}'
Response · 201
{
  "id": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
  "name": "Оплата",
  "sortIndex": 0,
  "createdAt": "2026-08-02T10:15:00.000Z",
  "updatedAt": "2026-09-20T08:41:27.000Z"
}

Update a folder

PATCH/v2/saved-reply-folders/{id}cost 1

Changes only the fields you send. A body with no fields returns 400.

Parameters

  • idstringpathrequired

    Folder ID.

  • namestringbodyoptional

    Folder name, 1 to 100 characters.

  • sortIndexintegerbodyoptional

    Position in the list, from 0. If omitted on create, the folder goes last.

curl -X PATCH "https://api.pager.co.ua/v2/saved-reply-folders/9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Доставка й оплата"}'
Response · 200
{
  "id": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
  "name": "Доставка й оплата",
  "sortIndex": 0,
  "createdAt": "2026-08-02T10:15:00.000Z",
  "updatedAt": "2026-09-20T08:41:27.000Z"
}

Delete a folder

DELETE/v2/saved-reply-folders/{id}cost 1

Deletes the folder together with all of its saved replies and responds with 204.

Parameters

  • idstringpathrequired

    Folder ID.

curl -X DELETE "https://api.pager.co.ua/v2/saved-reply-folders/9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 204· No response body

Resources

Saved replies

Ready-made replies managers insert into a conversation in one click. Returned grouped by folder, in sortIndex order.

  • folderId must belong to your organization, otherwise the API returns 400 with param: "folderId".
  • Attachments are read-only for now: you can't add or change them via the API.
  • attachments[].url links are temporary; they expire at expiresAt. Don't store them — fetch the saved reply again instead.

The saved reply object

  • idstring

    Saved reply ID.

  • folderIdstring

    The folder the reply is in.

  • textstring

    Reply text, up to 8000 characters.

  • sortIndexinteger

    Position within the folder, from 0.

  • attachmentsobject[]

    Attachments: type (image, video, audio or document), url, name, mime, size in bytes and expiresAt.

  • createdAtISO 8601

    When the reply was created.

  • updatedAtISO 8601

    When the reply was last changed.

The saved reply object
{
  "id": "3f2e1d0c-9b8a-4f7e-a6d5-c4b3a2f1e0d9",
  "folderId": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
  "text": "Відправляємо Новою поштою по всій Україні, доставка 1–2 дні.",
  "sortIndex": 0,
  "attachments": [
    {
      "type": "image",
      "url": "https://files.pager.co.ua/…/tariffs.png?X-Amz-Expires=3600&…",
      "name": "tariffs.png",
      "mime": "image/png",
      "size": 184320,
      "expiresAt": "2026-09-24T13:00:00.000Z"
    }
  ],
  "createdAt": "2026-08-02T10:16:12.000Z",
  "updatedAt": "2026-09-20T08:41:27.000Z"
}

List saved replies

GET/v2/saved-repliescost 2

Returns your organization's saved replies.

Parameters

  • folderIdstringqueryoptional

    Return only replies from this folder.

  • updatedSinceISO 8601queryoptional

    Return only replies updated since this moment. ISO 8601 with a time zone.

curl "https://api.pager.co.ua/v2/saved-replies?folderId=9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "3f2e1d0c-9b8a-4f7e-a6d5-c4b3a2f1e0d9",
      "folderId": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
      "text": "Відправляємо Новою поштою по всій Україні, доставка 1–2 дні.",
      "sortIndex": 0,
      "attachments": [
        {
          "type": "image",
          "url": "https://files.pager.co.ua/…/tariffs.png?X-Amz-Expires=3600&…",
          "name": "tariffs.png",
          "mime": "image/png",
          "size": 184320,
          "expiresAt": "2026-09-24T13:00:00.000Z"
        }
      ],
      "createdAt": "2026-08-02T10:16:12.000Z",
      "updatedAt": "2026-09-20T08:41:27.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Create a saved reply

POST/v2/saved-repliescost 1

Creates a reply in a folder and returns it with a 201.

Parameters

  • folderIdstringbodyrequired

    The reply's folder. On update, moves the reply to another folder.

  • textstringbodyrequired

    Reply text, 1 to 8000 characters.

  • sortIndexintegerbodyoptional

    Position within the folder, from 0. If omitted on create, the reply goes last in the folder.

curl -X POST "https://api.pager.co.ua/v2/saved-replies" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"folderId":"9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b","text":"Оплата при отриманні або на картку ФОП."}'
Response · 201
{
  "id": "3f2e1d0c-9b8a-4f7e-a6d5-c4b3a2f1e0d9",
  "folderId": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
  "text": "Оплата при отриманні або на картку ФОП.",
  "sortIndex": 0,
  "attachments": [],
  "createdAt": "2026-08-02T10:16:12.000Z",
  "updatedAt": "2026-09-20T08:41:27.000Z"
}

Update a saved reply

PATCH/v2/saved-replies/{id}cost 1

Changes only the fields you send. To move the reply to another folder, send a new folderId.

Parameters

  • idstringpathrequired

    Saved reply ID.

  • folderIdstringbodyoptional

    The reply's folder. On update, moves the reply to another folder.

  • textstringbodyoptional

    Reply text, 1 to 8000 characters.

  • sortIndexintegerbodyoptional

    Position within the folder, from 0. If omitted on create, the reply goes last in the folder.

curl -X PATCH "https://api.pager.co.ua/v2/saved-replies/3f2e1d0c-9b8a-4f7e-a6d5-c4b3a2f1e0d9" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Оплата при отриманні, на картку ФОП або через Apple Pay."}'
Response · 200
{
  "id": "3f2e1d0c-9b8a-4f7e-a6d5-c4b3a2f1e0d9",
  "folderId": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
  "text": "Оплата при отриманні, на картку ФОП або через Apple Pay.",
  "sortIndex": 0,
  "attachments": [
    {
      "type": "image",
      "url": "https://files.pager.co.ua/…/tariffs.png?X-Amz-Expires=3600&…",
      "name": "tariffs.png",
      "mime": "image/png",
      "size": 184320,
      "expiresAt": "2026-09-24T13:00:00.000Z"
    }
  ],
  "createdAt": "2026-08-02T10:16:12.000Z",
  "updatedAt": "2026-09-20T08:41:27.000Z"
}

Delete a saved reply

DELETE/v2/saved-replies/{id}cost 1

Deletes the reply and responds with 204.

Parameters

  • idstringpathrequired

    Saved reply ID.

curl -X DELETE "https://api.pager.co.ua/v2/saved-replies/3f2e1d0c-9b8a-4f7e-a6d5-c4b3a2f1e0d9" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 204· No response body

Resources

Webhook subscriptions

Subscriptions Pager sends events to, and their delivery log. It's the same list as Settings → API → Webhooks in the dashboard. For receiving and verifying events, see Webhooks.

  • The signing secret is returned only in responses to creating a subscription and rotating the secret. Other responses never include it.
  • Another organization's subscription returns 404 webhook_not_found, same as a nonexistent one.

Subscription object

  • idstring

    Subscription ID.

  • urlstring

    The address events are sent to.

  • eventsenum[]

    Subscribed events: message.received, message.sent, comment.received.

  • descriptionstring | null

    A note for yourself, or null.

  • enabledboolean

    Whether events are sent to this subscription.

  • consecutiveFailuresinteger

    Failed delivery attempts in a row. Reset by the first successful delivery.

  • failingSinceISO 8601 | null

    Start of the current failure streak, or null. After 7 days of failures the subscription is disabled automatically — see Delivery and retries.

  • disabledAtISO 8601 | null

    When the subscription was disabled, or null if it's enabled.

  • disabledReasonenum | null

    manual — disabled by hand in the dashboard or via the API, delivery_failures — automatically after 7 days of failures, null — the subscription is enabled.

  • createdAtISO 8601

    When the subscription was created.

  • updatedAtISO 8601

    When the subscription was last changed.

Subscription object
{
  "id": "8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b",
  "url": "https://crm.example.com/pager/webhook",
  "events": [
    "message.received",
    "message.sent"
  ],
  "description": "Синхронізація з CRM",
  "enabled": true,
  "consecutiveFailures": 0,
  "failingSince": null,
  "disabledAt": null,
  "disabledReason": null,
  "createdAt": "2026-09-24T09:12:05.301Z",
  "updatedAt": "2026-09-24T09:12:05.301Z"
}

List subscriptions

GET/v2/webhookscost 2

Returns all of the organization's subscriptions at once, without pagination.

curl "https://api.pager.co.ua/v2/webhooks" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b",
      "url": "https://crm.example.com/pager/webhook",
      "events": [
        "message.received",
        "message.sent"
      ],
      "description": "Синхронізація з CRM",
      "enabled": true,
      "consecutiveFailures": 0,
      "failingSince": null,
      "disabledAt": null,
      "disabledReason": null,
      "createdAt": "2026-09-24T09:12:05.301Z",
      "updatedAt": "2026-09-24T09:12:05.301Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Create a subscription

POST/v2/webhookscost 1

Creates a subscription and returns it with 201, along with the signing secret in the secret field. Save the secret right away: the API won't return it again.

Parameters

  • Idempotency-Keystringheaderoptional

    A key that keeps a retried request from creating a second subscription — see Idempotency.

  • urlstringbodyrequired

    A public https:// URL, up to 2000 characters — see Limits.

  • eventsenum[]bodyrequired

    A non-empty array of events: message.received, message.sent, comment.received. Duplicates are removed.

  • descriptionstring | nullbodyoptional

    Up to 500 characters, or null.

  • enabledbooleanbodyoptional

    false — the subscription exists but no events are sent. Defaults to true.

  • The URL is checked on creation. A non-https URL, a private address or a host that doesn't resolve returns 400 invalid_webhook_url with param: "url"; the reason is in message.
  • Send an Idempotency-Key: if the response gets lost, a retry with the same key returns the same subscription with the same secret instead of creating a second one.
  • After creating it, check the endpoint with a test event.
curl -X POST "https://api.pager.co.ua/v2/webhooks" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Idempotency-Key: a3f1c9e2-5b7d-4e8a-9c0b-1d2e3f4a5b6c" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://crm.example.com/pager/webhook","events":["message.received","message.sent"],"description":"Синхронізація з CRM"}'
Response · 201
{
  "id": "8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b",
  "url": "https://crm.example.com/pager/webhook",
  "events": [
    "message.received",
    "message.sent"
  ],
  "description": "Синхронізація з CRM",
  "enabled": true,
  "consecutiveFailures": 0,
  "failingSince": null,
  "disabledAt": null,
  "disabledReason": null,
  "createdAt": "2026-09-24T09:12:05.301Z",
  "updatedAt": "2026-09-24T09:12:05.301Z",
  "secret": "whsec_2Xk9vQ7mB4nR1tY8wZ3cL6pF0hJ5sD2gA9eU4iO7qWx"
}

Retrieve a subscription

GET/v2/webhooks/{id}cost 1

Returns the subscription and delivery stats for the last 30 days in stats: delivered — delivered, pending — queued or waiting for a retry, dead — not delivered after all attempts.

Parameters

  • idstringpathrequired

    Subscription ID.

curl "https://api.pager.co.ua/v2/webhooks/8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "id": "8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b",
  "url": "https://crm.example.com/pager/webhook",
  "events": [
    "message.received",
    "message.sent"
  ],
  "description": "Синхронізація з CRM",
  "enabled": true,
  "consecutiveFailures": 0,
  "failingSince": null,
  "disabledAt": null,
  "disabledReason": null,
  "createdAt": "2026-09-24T09:12:05.301Z",
  "updatedAt": "2026-09-24T09:12:05.301Z",
  "stats": {
    "periodDays": 30,
    "delivered": 1284,
    "pending": 1,
    "dead": 3
  }
}

Update a subscription

PATCH/v2/webhooks/{id}cost 1

Changes only the fields you pass. A body with no fields returns 400.

Parameters

  • idstringpathrequired

    Subscription ID.

  • urlstringbodyoptional

    A public https:// URL, up to 2000 characters — see Limits.

  • eventsenum[]bodyoptional

    A non-empty array of events: message.received, message.sent, comment.received. Duplicates are removed.

  • descriptionstring | nullbodyoptional

    Up to 500 characters, or null.

  • enabledbooleanbodyoptional

    false — disable the subscription (disabledReason: "manual"). true — enable it with a clean slate: consecutiveFailures, failingSince, disabledAt and disabledReason are reset.

  • A new url is checked the same way as on creation. Changing the URL keeps the same signing secret.
  • Re-enabling after automatic disabling is exactly enabled: true.
curl -X PATCH "https://api.pager.co.ua/v2/webhooks/8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b" \
  -H "Authorization: Bearer $PAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":["message.received"]}'
Response · 200
{
  "id": "8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b",
  "url": "https://crm.example.com/pager/webhook",
  "events": [
    "message.received"
  ],
  "description": "Синхронізація з CRM",
  "enabled": true,
  "consecutiveFailures": 0,
  "failingSince": null,
  "disabledAt": null,
  "disabledReason": null,
  "createdAt": "2026-09-24T09:12:05.301Z",
  "updatedAt": "2026-09-24T12:05:17.640Z"
}

Delete a subscription

DELETE/v2/webhooks/{id}cost 1

Deletes the subscription together with its delivery log and responds with 204. Queued deliveries are not sent.

Parameters

  • idstringpathrequired

    Subscription ID.

curl -X DELETE "https://api.pager.co.ua/v2/webhooks/8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 204· No response body

Rotate the secret

POST/v2/webhooks/{id}/rotate-secretcost 1

Creates a new signing secret and returns the subscription with it in the secret field.

Parameters

  • idstringpathrequired

    Subscription ID.

  • The old secret stops working immediately — there's no grace period. Deliveries already in the queue will be signed with the new secret.
curl -X POST "https://api.pager.co.ua/v2/webhooks/8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b/rotate-secret" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "id": "8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b",
  "url": "https://crm.example.com/pager/webhook",
  "events": [
    "message.received",
    "message.sent"
  ],
  "description": "Синхронізація з CRM",
  "enabled": true,
  "consecutiveFailures": 0,
  "failingSince": null,
  "disabledAt": null,
  "disabledReason": null,
  "createdAt": "2026-09-24T09:12:05.301Z",
  "updatedAt": "2026-09-24T12:10:48.093Z",
  "secret": "whsec_Hn4tE8rW1qZ6xC3vB9mK2pL7sD5fG0jA4yU8iO1eRtN"
}

Send a test event

POST/v2/webhooks/{id}/testcost 1

Synchronously sends a webhook.test event to the subscription's URL and returns the delivery record with the result: delivered or dead, your server's response code and body, and the duration.

Parameters

  • idstringpathrequired

    Subscription ID.

  • A test event isn't retried on failure and doesn't affect the subscription's failure streak. It does appear in the log.
  • Works for a disabled subscription too: handy for checking a fixed endpoint before enabling it.
  • In a test event data.message is a string, not a message object.
curl -X POST "https://api.pager.co.ua/v2/webhooks/8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b/test" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "id": "dlv_0f1e2d3c-4b5a-4968-8776-5a4b3c2d1e0f",
  "eventId": "evt_a0b1c2d3e4f5460718293a4b5c6d7e8f",
  "eventType": "webhook.test",
  "status": "delivered",
  "attempt": 1,
  "nextAttemptAt": null,
  "responseStatus": 200,
  "responseBody": "ok",
  "durationMs": 184,
  "createdAt": "2026-09-24T09:13:40.210Z",
  "deliveredAt": "2026-09-24T09:13:40.402Z"
}

Delivery log

GET/v2/webhooks/{id}/deliveriescost 2

The subscription's deliveries, paginated, newest first. One record is one event: attempt shows how many attempts have been made, and responseStatus, responseBody and durationMs show the result of the latest one.

Parameters

  • idstringpathrequired

    Subscription ID.

  • statusenumqueryoptional

    pending, failed, delivered or dead.

  • eventTypestringqueryoptional

    Only deliveries of this event type, e.g. webhook.test.

  • limitintegerqueryoptional

    Page size, 1 to 100. Defaults to 50.

  • cursorstringqueryoptional

    nextCursor from the previous page.

  • status: pending — waiting to be sent, failed — the last attempt failed, the next one is at nextAttemptAt, delivered — delivered at deliveredAt, dead — all attempts used up.
  • responseStatus is null if there was no response (timeout, connection error); the reason is then in responseBody.
  • The log doesn't return the event body itself. Records are kept for 30 days.
curl "https://api.pager.co.ua/v2/webhooks/8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b/deliveries?limit=2" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "dlv_4b7e1f0a-9c2d-4e8b-a6f3-5d1c0b9e8a72",
      "eventId": "evt_6c1f0e9d8b7a45c3a2e1f0d9c8b7a6e5",
      "eventType": "message.received",
      "status": "delivered",
      "attempt": 1,
      "nextAttemptAt": null,
      "responseStatus": 200,
      "responseBody": "ok",
      "durationMs": 184,
      "createdAt": "2026-09-24T08:31:40.582Z",
      "deliveredAt": "2026-09-24T08:31:40.771Z"
    },
    {
      "id": "dlv_9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "eventId": "evt_1d2c3b4a5f6e47d8a9b0c1d2e3f4a5b6",
      "eventType": "message.received",
      "status": "failed",
      "attempt": 3,
      "nextAttemptAt": "2026-09-24T08:36:12.004Z",
      "responseStatus": 503,
      "responseBody": "Service Unavailable",
      "durationMs": 91,
      "createdAt": "2026-09-24T08:29:03.117Z",
      "deliveredAt": null
    }
  ],
  "hasMore": true,
  "nextCursor": "WyIyMDI2LTA5LTI0VDA4OjMxOjQwLjU4MloiLCI0YjdlMWYwYS05YzJkLTRlOGItYTZmMy01ZDFjMGI5ZThhNzIiXQ"
}

Retry a delivery

POST/v2/webhooks/{id}/deliveries/{deliveryId}/retrycost 1

Queues the delivery to be sent immediately and responds with 202 and the record in pending status. The result shows up in the log within a few seconds.

Parameters

  • idstringpathrequired

    Subscription ID.

  • deliveryIdstringpathrequired

    Delivery id from the log, dlv_….

  • Works for any status, including dead. For dead it's a single extra attempt: if it fails, the delivery becomes dead again.
  • Enable a disabled subscription first: the retry would never be sent, so the API returns 409 webhook_disabled.
  • The event goes out with the same id, so if your server has already processed it, it will drop it as a duplicate.
curl -X POST "https://api.pager.co.ua/v2/webhooks/8f3a2c1e-7b6d-4e5f-9a0b-1c2d3e4f5a6b/deliveries/dlv_9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/retry" \
  -H "Authorization: Bearer $PAGER_API_KEY"
Response · 202
{
  "id": "dlv_9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "eventId": "evt_1d2c3b4a5f6e47d8a9b0c1d2e3f4a5b6",
  "eventType": "message.received",
  "status": "pending",
  "attempt": 3,
  "nextAttemptAt": "2026-09-24T08:33:20.518Z",
  "responseStatus": 503,
  "responseBody": "Service Unavailable",
  "durationMs": 91,
  "createdAt": "2026-09-24T08:29:03.117Z",
  "deliveredAt": null
}
Didn't find an answer?Contact us
API Documentation — Pager