Custom channels · Webhooks
Custom channels
Connect any messaging platform to Pager: client messages land in the shared inbox, and managers' replies go back to your platform.
A custom channel connects a platform Pager has no ready-made integration for: your own messenger, a website chat, a mobile app or a third-party service. Managers work with these conversations just like with Instagram or Telegram.
A client wrote on your platform — you send message.created to Pager's webhook.
A manager replied in Pager — Pager sends a POST to your server's URL.
- Everything is JSON over HTTPS, and both directions are signed with one channel key.
- You identify clients and messages with your own IDs —
externalId. Pager creates the client and conversation on the first message. - New messages count towards your organization's message allowance, same as other channels.
An organization admin connects the channel in the Pager dashboard.
- 1
Open Settings → Channels and click Підключити власний канал (Connect your own channel).
- 2
Enter a channel name — managers see it in the inbox — and a URL for sending messages: your server's address where Pager will send managers' replies.
- 3
Once connected, the channel card shows an API Key like
chan_sk_live_…— that's the channel key. Add it to your server's settings.
pgr_live_…). Each custom channel has its own key.Both directions are signed with the channel key in the x-channel-key header.
- Your server → Pager. Send the key in
x-channel-keywith every webhook request. Pager uses it to identify the channel and organization. - Pager → your server. Pager sends the same key in
x-channel-key. Compare it with yours and reject requests with any other key — otherwise anyone who knows your URL could message your clients as a manager.
Send all inbound events as a POST to a single URL. The event field sets the event type: message.created, message.edited or message.deleted.
https://api.pager.co.ua/api/webhooks/customHeaders
x-channel-keyheaderChannel key.
Content-Typeheaderapplication/json.
- One event per request. There's no batching.
- Successful processing always returns
200with anactionfield saying what was done — see Responses and errors.
New messagemessage.created
Adds a message to the conversation with a client. If the client writes for the first time, Pager creates the client and conversation.
Request body
eventstringrequiredmessage.created.client.externalIdstringrequiredThe client's ID on your platform, up to 255 characters. Pager uses it to find the client in this channel.
client.namestring | nulloptionalClient name, up to 100 characters. Omitted — stays as is;
null— cleared.client.imageUrlstring | nulloptionalAvatar URL. Omitted — stays as is.
message.externalIdstringrequiredThe message's ID on your platform, up to 255 characters.
message.directionenumrequiredincoming— a message from the client.outgoing— a message your operator sent outside Pager, so the history in Pager stays complete.message.textstring | nulloptionalText, up to 8000 characters.
message.attachmentsobject[]optionalUp to 20 attachments — see Attachments.
- Sending the same
message.externalIdagain doesn't create a duplicate: Pager updates the existing message's text and attachments. So it's safe to retry a request you got no response to. - An incoming message marks the conversation unread. An outgoing one is stored as read and doesn't change the conversation's state.
- Leading and trailing whitespace is trimmed, and
\r\nline breaks become\n. - A new message counts towards your organization's message allowance; a repeat of the same
externalIddoesn't.
curl -X POST "https://api.pager.co.ua/api/webhooks/custom" \
-H "x-channel-key: $PAGER_CHANNEL_KEY" \
-H "Content-Type: application/json" \
-d '{"event":"message.created","client":{"externalId":"user_58213","name":"Олена Коваль","imageUrl":"https://cdn.example.com/avatars/58213.jpg"},"message":{"externalId":"msg_90412","direction":"incoming","text":"Добрий день! Чи є доставка у Львів?","attachments":[]}}'{
"ok": true,
"action": "created_or_deduped"
}Editmessage.edited
Changes the text or attachments of a message Pager already has, and marks it as edited.
Request body
eventstringrequiredmessage.edited.message.externalIdstringrequiredID of the message to change.
message.textstring | nulloptionalNew text, up to 8000 characters. Omitted,
nullor an empty string — the text stays as is.message.attachmentsobject[]optionalA new list of attachments that fully replaces the old one. Omitted — attachments stay as is;
[]— remove all.
- Pager looks for a message with this
externalIdin your channel — incoming or outgoing. Managers' messages only have anexternalIdif your server returned one in its response to the outbound message. - If the message isn't found, Pager returns
200withaction: "edit_ignored_not_found".
curl -X POST "https://api.pager.co.ua/api/webhooks/custom" \
-H "x-channel-key: $PAGER_CHANNEL_KEY" \
-H "Content-Type: application/json" \
-d '{"event":"message.edited","message":{"externalId":"msg_90412","text":"Добрий день! Чи є доставка у Львів і скільки вона коштує?"}}'{
"ok": true,
"action": "edited"
}Deletemessage.deleted
Permanently deletes a message from Pager — managers no longer see it.
Request body
eventstringrequiredmessage.deleted.message.externalIdstringrequiredID of the message to delete.
- Like edits, it works for incoming and outgoing messages with a known
externalId. - If the message isn't found, Pager returns
200withaction: "delete_ignored_not_found".
curl -X POST "https://api.pager.co.ua/api/webhooks/custom" \
-H "x-channel-key: $PAGER_CHANNEL_KEY" \
-H "Content-Type: application/json" \
-d '{"event":"message.deleted","message":{"externalId":"msg_90412"}}'{
"ok": true,
"action": "deleted"
}Successful processing always returns 200 with ok: true. The action field says what was done:
| action | Meaning |
|---|---|
created_or_deduped | The message was created, or already existed and was updated (a repeated message.created). |
edited | The message was changed. |
edit_ignored_not_found | No message with this externalId — nothing changed. |
deleted | The message was deleted. |
delete_ignored_not_found | No message with this externalId — nothing deleted. |
Errors come with an error field:
| HTTP | error | Meaning |
|---|---|---|
400 | Invalid payload | The body doesn't match the schema. details.fieldErrors shows which fields failed. |
401 | Missing x-channel-key | The x-channel-key header is missing. |
401 | Invalid channel key | No such key — the channel may have been deleted. |
403 | Channel is not Custom | The key belongs to a channel of another type. |
403 | Inbound disabled | Inbound messages are turned off for the channel. |
500 | Server error | Internal Pager error. |
{
"error": "Invalid payload",
"details": {
"formErrors": [],
"fieldErrors": {
"client": [
"Invalid input: expected string, received undefined"
]
}
}
}500 and network errors, retry after a pause — thanks to externalId, retries never create duplicates. Retrying won't fix a 4xx: check the key and the request body.When a manager sends a message in a conversation of this channel, Pager immediately makes a POST to the URL set when the channel was connected. Messages sent via the Pager API arrive the same way. The body has the same format as the inbound message.created event.
Headers
x-channel-keystringChannel key — verify it, see Authentication.
Content-Typestringapplication/json.
Request body
eventstringAlways
message.created.client.externalIdstringThe client's ID on your platform — the one you sent in inbound events.
message.pagerMessageIdstringThe message's ID in Pager. Use it to drop repeated deliveries.
message.directionenumAlways
outgoing.message.textstring | nullText, or
nullif the manager only sent files.message.attachmentsobject[]Attachments — see Attachments.
{
"event": "message.created",
"client": {
"externalId": "user_58213"
},
"message": {
"pagerMessageId": "c9d8e7f6-a5b4-4c3d-9e2f-1a0b9c8d7e6f",
"direction": "outgoing",
"text": "Так, відправляємо Новою поштою, доставка 1–2 дні. Ось тарифи:",
"attachments": [
{
"type": "image",
"payload": {
"url": "https://files.pager.co.ua/…/tariffs.png?X-Amz-Expires=86400&…"
}
}
]
}
}Respond with a 2xx as soon as you've accepted the message, and return the message's ID on your platform as JSON.
{
"externalMessageId": "msg_90415"
}- Pager stores
externalMessageId(orexternalId) as the message's ID. Without it you won't be able to edit or delete this message later. - The message counts towards your organization's message allowance only if you responded with a
2xxand returnedexternalMessageId. - The manager only sees the message as sent after your response, so respond quickly and do any heavy processing afterwards.
Delivery and retries
- If your server responds with a non-
2xxor doesn't respond at all, Pager retries twice more — after 0.2 and 0.6 seconds. - If all three attempts fail, the message is saved in Pager marked as undelivered, and the manager sees that. Pager doesn't resend it automatically later.
message.pagerMessageId.A minimal handler: verifies the key, drops repeats, passes the message to your platform and returns its ID.
import express from "express"
const app = express()
app.use(express.json())
// pagerMessageId -> externalMessageId. In production, use a database or Redis
const processed = new Map()
app.post("/pager/outbound", async (req, res) => {
// 1. The request really comes from Pager
if (req.get("x-channel-key") !== process.env.PAGER_CHANNEL_KEY) {
return res.status(401).end()
}
const { client, message } = req.body
// 2. A repeat of an already processed message
if (processed.has(message.pagerMessageId)) {
return res.json({ externalMessageId: processed.get(message.pagerMessageId) })
}
// 3. Send to the client on your platform
const sent = await platform.sendMessage({
to: client.externalId,
text: message.text,
attachments: message.attachments,
})
processed.set(message.pagerMessageId, sent.id)
res.json({ externalMessageId: sent.id })
})platform.sendMessage is your own sending code. The rest can be used as is.Attachments are an array of { type, payload: { url } } objects — the same in inbound events and in requests from Pager.
Fields
typeenumrequiredimage,video,audio,documentorfile.payload.urlstringrequiredA public
http(s)link to the file, up to 2000 characters.
[
{
"type": "image",
"payload": {
"url": "https://cdn.example.com/uploads/photo.jpg"
}
},
{
"type": "document",
"payload": {
"url": "https://cdn.example.com/uploads/invoice.pdf"
}
}
]- Pager stores the link, not a copy of the file, so it must stay available for as long as managers need the conversation history.
- Links to
localhostand internal networks (127.*,10.*,192.168.*) are silently dropped; the rest of the message is saved. - Up to 20 attachments per message.
- In requests from Pager, links are temporary and expire after 24 hours — download files right away.