Skip to main content

API Reference

HTTP API reference for the self-hosted PocketPing bridge server (the standalone Go binary).

Scope

This page documents the bridge server's HTTP API. If you embed PocketPing into your own backend with an SDK, you call the SDK's handlers directly instead of these routes — see the SDK docs. The hosted SaaS exposes its own endpoints and is not covered here.

Base URL

The bridge server listens on port 3001 by default. All application endpoints live under /api:

  • Local: http://localhost:3001
  • Self-hosted: your deployed bridge URL (e.g. https://bridge.yourdomain.com)

Authentication

If the server is started with an API_KEY, every /api/* endpoint (and the SSE stream) requires a matching Authorization header. Webhook endpoints and /health are not API-key protected.

Authorization: Bearer your_api_key

A missing or incorrect key returns 401 Unauthorized. If no API_KEY is configured, authentication is skipped.

Endpoints

MethodPathAuthDescription
GET/healthNoHealth check + configured bridges
POST/api/eventsYesGeneric event ingestion (typed envelope)
POST/api/sessionsYesNotify bridges of a new session
POST/api/messagesYesForward a visitor message to the bridges
POST/api/operator/statusYesUpdate operator online status
POST/api/custom-eventsYesForward a custom event
POST/api/disconnectYesNotify bridges a visitor left
GET/api/events/streamYesServer-Sent Events stream (operator replies, edits, deletes)
POST/webhooks/telegramNoTelegram webhook (operator replies in)
POST/webhooks/slackNoSlack Events API webhook
POST/webhooks/discordNoDiscord webhook

Most endpoints return {"ok": true} on success.


Health

GET /health

Response:

{
"status": "ok",
"bridges": ["telegram", "discord"]
}

New Session

Notify the configured bridges that a new chat session has started (e.g. so Telegram creates a Forum Topic).

POST /api/sessions

The request body is a Session object:

{
"id": "sess_abc123",
"visitorId": "vis_xyz789",
"createdAt": "2024-01-15T10:30:00Z",
"lastActivity": "2024-01-15T10:30:00Z",
"operatorOnline": false,
"aiActive": false,
"metadata": {
"url": "https://example.com/pricing",
"country": "France",
"browser": "Chrome",
"deviceType": "desktop"
},
"identity": {
"id": "user_123",
"email": "[email protected]",
"name": "John Doe"
}
}

Response:

{ "ok": true }

Send Message

Forward a visitor message to the bridges. The body wraps a message and the session it belongs to.

POST /api/messages

Request:

{
"message": {
"id": "msg_001",
"sessionId": "sess_abc123",
"content": "Hello, I have a question",
"sender": "visitor",
"timestamp": "2024-01-15T10:30:00Z",
"replyTo": "msg_000"
},
"session": {
"id": "sess_abc123",
"visitorId": "vis_xyz789"
}
}

message.replyTo is optional and references the id of the message being replied to. Response: { "ok": true }.


Operator Status

POST /api/operator/status

Request:

{ "online": true }

When the operator is offline, AI fallback (if configured) may take over after the takeover delay.


Custom Events

Forward a custom event from the widget. If EVENTS_WEBHOOK_URL is configured, the event is also relayed to that webhook (optionally HMAC-signed with EVENTS_WEBHOOK_SECRET).

POST /api/custom-events

Request:

{
"event": {
"name": "clicked_pricing",
"data": { "plan": "pro" },
"timestamp": "2024-01-15T10:30:00Z",
"sessionId": "sess_abc123"
},
"session": {
"id": "sess_abc123",
"visitorId": "vis_xyz789"
}
}

Disconnect

Notify the bridges that a visitor left the page (used to post a "visitor left" note in the thread).

POST /api/disconnect

Request:

{
"session": { "id": "sess_abc123", "visitorId": "vis_xyz789" },
"duration": 320,
"reason": "page_unload"
}
FieldTypeDescription
sessionobjectThe session (required)
durationnumberSeconds the visitor was on the page
reasonstringpage_unload, inactivity, or manual

Generic Events

POST /api/events accepts a single typed envelope and dispatches it like the convenience endpoints above. The body must include a type field; the remaining fields depend on the type.

POST /api/events

Example (a visitor message):

{
"type": "visitor_message",
"message": { "id": "msg_001", "sessionId": "sess_abc123", "content": "Hi", "sender": "visitor" },
"session": { "id": "sess_abc123", "visitorId": "vis_xyz789" }
}

Supported type values:

TypePurpose
new_sessionNew chat session
visitor_messageVisitor sent a message
visitor_message_editedVisitor edited a message
visitor_message_deletedVisitor deleted a message
message_readRead/delivery receipt
operator_statusOperator online/offline
custom_eventCustom event
identity_updateVisitor identity changed
ai_takeoverAI took over the conversation

An unknown type returns 400.


Event Stream (SSE)

Subscribe to operator-originated events (replies, edits, deletes) coming back from the bridges. This is a Server-Sent Events stream — there is no WebSocket endpoint.

GET /api/events/stream
const es = new EventSource('http://localhost:3001/api/events/stream');
// (EventSource cannot set custom headers; place the stream behind a
// proxy or use a fetch-based SSE client if API_KEY auth is enabled.)

es.onmessage = (e) => {
const event = JSON.parse(e.data);
switch (event.type) {
case 'operator_message':
console.log('Operator reply:', event.content);
break;
case 'operator_message_edited':
console.log('Edited:', event.messageId, event.content);
break;
case 'operator_message_deleted':
console.log('Deleted:', event.messageId);
break;
}
};

The server sends a : heartbeat comment every 30 seconds to keep the connection alive.

Example operator_message payload:

{
"type": "operator_message",
"sessionId": "sess_abc123",
"messageId": "msg_010",
"content": "Hi! How can I help?",
"sourceBridge": "telegram",
"operatorName": "Sarah"
}

Bridge Webhooks

These endpoints receive inbound traffic from the messaging platforms. You normally do not call them yourself — you register them with each platform so operator replies flow back into the bridge server (and out via SSE).

POST /webhooks/telegram
POST /webhooks/slack
POST /webhooks/discord

See the Telegram, Slack, and Discord guides for how to register each webhook URL.


Message Senders

The sender field on a message is one of:

SenderDescription
visitorMessage from the website visitor
operatorMessage from a human operator (via a bridge)
aiMessage from AI fallback

Error Responses

Errors are returned as JSON with an error field and the corresponding HTTP status:

{ "error": "Unauthorized" }
StatusDescription
400Bad request (invalid JSON / unknown event type / missing field)
401Unauthorized (missing or invalid API_KEY)
403Forbidden (blocked by User-Agent filter)
500Internal server error

SDKs

Prefer to embed PocketPing in your own backend instead of calling these routes directly? Use a backend SDK: