A PBX event is a rule of the form when this happens on the PBX, do that. When the trigger happens - a call starts, rings, is answered or ends, a switch is turned on or off, something happens in a queue or a conference room, a user logs in to or out of a hotdesk phone, or a point in time is reached - the event runs its actions:
An event can have any combination of the six actions. Texts in the webhook, the SMS and the variable value can hold variables such as %A_NUMBER%, which are filled in when the event runs.
A customer can have at most 100 events. Each run of an event is logged with the result of each action, and every event can be tested with values of your own choosing.
| API | Description |
|---|---|
| List | List the events of a customer |
| Get | Get a single event |
| Create | Create a new event |
| Update | Update an event |
| Delete | Delete an event |
| Secret | Give the webhook of an event a new secret |
| Log | The latest runs of an event and how each action went |
| Test | Run the actions of an event now, with variables of your own choosing |
The type of an event decides what triggers it, and its target can limit it to a number, a switch, a queue, a conference room or a phone.
| Type | Triggered when | target |
|---|---|---|
| CALL | Any of CALL_START, CALL_RING, CALL_ANSWER and CALL_END below. | Empty for every call, or a phone number or the start of one, e.g. +4570 or 4570. It is matched against both the calling and the called number, and a leading + is ignored on both. |
| CALL_START | A call starts. | |
| CALL_RING | A call rings. | |
| CALL_ANSWER | A call is answered. | |
| CALL_END | A call ends. | |
| SWITCH | A switch changes value - from off to on or from on to off. | Required. The exact name of the switch, prefixed with SHARED: for a shared switch or <employee-id>: for a personal switch, as in Switch set. |
| SWITCH_ON | A switch is turned on. | |
| SWITCH_OFF | A switch is turned off. | |
| QUEUE | A caller enters a queue, is answered, hangs up while waiting or times out - and when a member joins or leaves the queue. | Empty for every queue, or the id of a queue. |
| QUEUE_MEMBER_JOIN | A member joins a queue. | |
| QUEUE_MEMBER_LEFT | A member leaves a queue. | |
| CONFERENCE | A participant enters or leaves a conference room. | Empty for every conference, or the id of a conference. |
| HOTDESK | A user logs in to (grabs) or logs out of (releases) a hotdesk SIP phone. A login that pushes another hotdesk phone off the extension, or moves the phone away from another extension, also counts as a logout of that phone or extension. So does a phone released automatically, when the Communicator that used it has disconnected. | Empty for every hotdesk phone, the id of an extension, or the MAC address of a SIP phone written as 00:15:65:AA:BB:CC (upper or lower case). |
| TIMER | One of the times in timers is reached: every week on a day and time, or once on a date and time - see Create. Times are Danish time. | Not used. |
Good to know:
The actions of an event run in this order: set switch, set variable, queue, webhook, SMS, call. Each action runs on its own - one that fails does not stop the others - and the result of each is written to the run log.
Because the set actions run first, a webhook or SMS of the same event that uses %SWITCH:name% or %VARIABLE:name% sees the value that was just set.
The webhook calls the URL of the event. The URL must be http or https, and may not point at a private, loopback or otherwise non-public address, or at Telecom X's own network. Variables may be used in the path and the query string of the URL, but not in the host name.
| Method | How the request is sent |
|---|---|
| GET | A GET request to the URL, with the variables filled in. |
| POST | A POST request to the URL without its query string. The query string is sent as the body, with the content type application/x-www-form-urlencoded. Write the URL as for GET, e.g. https://hooks.example.com/pbx?caller=%A_NUMBER%&event=%EVENT_TYPE%. |
| POSTJSON | A POST request to the URL, with the json of the event as the body and the content type application/json. |
Every request carries these headers:
| Header | Value |
|---|---|
| User-Agent | TelecomX-PbxEvents/1.0 |
| X-TCX-Event-Id | The id of the PBX event. |
| X-TCX-Signature | sha256= followed by the HMAC-SHA256, in lower case hex, of the request body (POST and POSTJSON) or of the complete URL that was called (GET), with the webhook secret as the key. |
| Content-Type | For POST and POSTJSON, as above. |
Every webhook has a secret. One is generated when the webhook is created, and you can set your own or generate a new one. The secret is shown only to users who may change the event: by Get, in the answers to Create, Update and Secret - never by List. Check the signature to be sure a request comes from your PBX event and was not changed on the way.
https://user:password@hooks.example.com/) is sent as basic authentication.
For GET, the signature is made over the URL exactly as it is called: the URL of the event with the variables filled in, e.g. https://hooks.example.com/pbx?caller=%2B4512345678. If your server sits behind a proxy, rebuild the URL as it is written in the event before you check it. Do not put a #fragment in a GET webhook URL - it is signed, but not sent.
const http = require('node:http') const crypto = require('node:crypto') const SECRET = process.env.PBX_EVENT_SECRET // the webhook secret of the event const BASE = 'https://hooks.example.com' // scheme and host of the webhook URL, as written in the event function signatureOk(signed, header) { const expected = Buffer.from('sha256=' + crypto.createHmac('sha256', SECRET).update(signed).digest('hex')) const received = Buffer.from(String(header || '')) return expected.length === received.length && crypto.timingSafeEqual(expected, received) } http.createServer((req, res) => { const chunks = [] req.on('data', chunk => chunks.push(chunk)) req.on('end', () => { const body = Buffer.concat(chunks).toString('utf8') // GET is signed over the complete URL, POST and POSTJSON over the raw body const signed = req.method === 'GET' ? BASE + req.url : body if (!signatureOk(signed, req.headers['x-tcx-signature'])) { res.writeHead(401).end() return } console.log('PBX event', req.headers['x-tcx-event-id'], req.method === 'GET' ? req.url : body) res.writeHead(204).end() }) }).listen(8080)
Check the signature against the raw body, before it is parsed - a body that is parsed and serialised again can differ by a single character, and then the signature does not match.
The SMS action sends message to each of the recipients (at most 10, in E.164 format, e.g. +4520123456). The message can hold variables.
The sender is from - a phone number in E.164 format, or 1-11 letters, digits, spaces, dots, underscores or dashes. A phone number is sent without its +, and a sender can be at most 11 characters, so a number with more than 11 digits is cut to its first 11. Without from, the sender is the customer's name, cut to 11 characters.
Each SMS is billed to the customer like any other SMS sent from the platform.
The call action rings destination the way the customer's PBX rings that number on an internal call: an extension rings its phones, a group rings its members, and an external number is called like any other outbound call of the PBX. The destination must be an extension, a group or an external number - any other destination is refused, and the run log shows the call as failed. Emergency numbers are never called by the event, not even through a redirect - but once a person has answered, that person can always transfer the call to an emergency number.
The destination rings as long as it normally does - e.g. a large group that rings its members one after the other is not cut short. When a person answers, he or she is connected to source, which is called as an internal call of the customer - e.g. a playback with an announcement, or another extension. Start audio and the recording consent prompt are skipped for these calls.
With autoAnswer, SIP phones that support it answer the call by themselves (as when paging a group), which is useful for announcements over the phones' speakers. Only the phones of the destination itself answer by themselves - not phones the call is redirected to. The caller name shown is PBX event.
A call placed by the call action never triggers events itself - also not after the person who answered transfers it. On Realtime, its events carry source PBX_EVENTS.
Set variable sets a PBX variable to value (variables are filled in, at most 1024 characters). Set switch turns a PBX switch on or off. The names follow the same rules as everywhere else: no prefix for a system variable or switch, SHARED: for a shared one, and <employee-id>: for a personal one.
The change is announced on Realtime like any other change, with source PBX_EVENTS, and it never triggers other events.
The queue action changes the members of a queue of the customer. Its action says how:
| Action | What it does |
|---|---|
| JOIN | Puts extension in queue with priority (1-20, default 1). If the queue remembers the priority of its members (member.persistPriority), an extension that has been a member before gets its last priority instead. An extension that is already a member is left as it is. With exclusive, every other member who is not permanent is then taken out of the queue. |
| LEAVE | Takes extension out of queue. If it is not a member, nothing changes. |
| LEAVE_ALL | Takes extension out of every queue of the customer. queue is not used. |
| EMPTY | Takes every member who is not permanent out of queue - except extension, the member to keep. Without extension, no one is kept. |
queue and extension are the id of a queue and of an extension of the customer - or TRIGGER, to take them from what triggered the event:
| Setting | TRIGGER takes | Allowed for the types |
|---|---|---|
| queue | The queue of the event (the variable QUEUE_ID): the queue of a QUEUE, QUEUE_MEMBER_JOIN or QUEUE_MEMBER_LEFT event, or the queue that placed the call leg of a CALL event. | CALL, CALL_START, CALL_RING, CALL_ANSWER, CALL_END, QUEUE, QUEUE_MEMBER_JOIN and QUEUE_MEMBER_LEFT |
| extension | The extension of the member who joined or left (MEMBER_EXTENSION_ID), else the extension of the event (EXTENSION_ID): the extension of the call leg, or of the hotdesk login or logout. | The same, and HOTDESK |
If what triggered the event has no queue or no extension - e.g. a call that did not come through a queue, or a member who is an external number - the action fails with no_trigger_queue or no_trigger_extension in the run log. A test takes them from the variables you send.
Good to know:
An event of type QUEUE_MEMBER_JOIN with the queue action EMPTY, and TRIGGER as both queue and extension: whenever someone joins the queue, everyone else is taken out of it - except permanent members. Without a target it works on every queue of the customer; give the id of a queue as target to limit it to that queue. See the request in Create.
A variable is written as its name between percent signs, e.g. %A_NUMBER%. In addition to the variables of the trigger below, two kinds of lookups can be used:
| Variable | Description |
|---|---|
| %VARIABLE:name% | The current value of the PBX variable name, e.g. %VARIABLE:SHARED:status%. Empty if the variable does not exist. |
| %SWITCH:name% | The current value of the PBX switch name: 1 for on, 0 for off, empty if the switch does not exist. |
A variable that is not known, or has no value for this run, becomes empty. Variable names are upper case letters and underscores.
Variables are filled in in the webhook URL, the webhook JSON, the SMS message and the value of set variable. They are not filled in in the call numbers, the SMS recipients and sender, or the names of the variable and switch to set. A value is escaped for the place it is put:
| Place | Escaping |
|---|---|
| Webhook URL (GET and POST) | Percent-encoded, so +4512345678 becomes %2B4512345678. |
| Webhook JSON (POSTJSON) | Escaped as the content of a JSON string - quotes and backslashes are escaped, but no quotes are added. Put every variable inside a JSON string: { "caller": "%A_NUMBER%", "duration": "%CALL_DURATION%" }. |
| SMS message, variable value | As it is. |
| Variable | Description |
|---|---|
| EVENT_TYPE | What happened: CALL_START, CALL_RING, CALL_ANSWER, CALL_END, SWITCH_ON, SWITCH_OFF, QUEUE_ENTERED, QUEUE_ANSWERED, QUEUE_CANCELLED, QUEUE_TIMED_OUT, QUEUE_MEMBER_JOIN, QUEUE_MEMBER_LEFT, CONFERENCE_ENTERED, CONFERENCE_LEFT, HOTDESK_LOGIN, HOTDESK_LOGOUT, TIMER or TEST. |
| TIME | Danish time of the run, e.g. 2026-11-04 14:30:05. |
| TIMESTAMP | Time of the run as Unix time (seconds since 1970-01-01 UTC). |
| GUID | A random unique id, new for each run. |
| EVENT_ID | Id of the PBX event. |
| Variable | Description |
|---|---|
| A_NUMBER | Number of the calling party. Empty for a caller with a secret number calling in from outside. |
| A_NUMBER_NO_PREFIX | A_NUMBER without a Danish +45 prefix. |
| A_NAME | Name of the calling party, if known. |
| B_NUMBER | The called number. |
| B_NUMBER_NO_PREFIX | B_NUMBER without a Danish +45 prefix. |
| B_NAME | Name of the called party, if known. |
| DIRECTION | I for a call in from outside, O for a call out from the PBX. |
| CALL_ID | Id of the call - the same in every phase of the call. |
| START | Danish time the call started, e.g. 2026-11-04 14:30:05. |
| CALL_DURATION | Seconds since the call started. CALL_ANSWER and CALL_END only. |
| CONVERSATION_DURATION | Seconds since the call was answered, 0 if it was not. CALL_END only. |
| EXTENSION_ID | Id of the extension of the call leg, if any. |
| QUEUE_ID | Id of the queue that placed the call leg, if any. |
| QUEUE_NAME | Name of that queue. |
| Variable | Description |
|---|---|
| SWITCH_NAME | Name of the switch, with its prefix. |
| SWITCH_STATE | 1 if the switch is now on, 0 if off. |
| PREVIOUS_STATE | 1 or 0 for the value before the change, empty if the switch did not exist. |
QUEUE_MEMBER_JOIN and QUEUE_MEMBER_LEFT are member events only.
| Variable | Description |
|---|---|
| QUEUE_ID | Id of the queue. |
| QUEUE_NAME | Name of the queue. |
| A_NUMBER | Number of the caller (caller events). Empty for a caller with a secret number. |
| A_NUMBER_NO_PREFIX | A_NUMBER without a Danish +45 prefix. |
| A_NAME | Name of the caller, if known. |
| POSITION | Position of the caller in the queue. |
| WAIT_TIME | Seconds the caller waited. QUEUE_ANSWERED, QUEUE_CANCELLED and QUEUE_TIMED_OUT only. |
| ANSWERED_BY | Id of the extension, or the number, that answered. QUEUE_ANSWERED only. |
| MEMBER_EXTENSION_ID | Id of the extension of the member (member events). |
| MEMBER_NUMBER | Number of the member (member events). |
| MEMBER_NAME | Name of the member (member events). |
| Variable | Description |
|---|---|
| CONFERENCE_ID | Id of the conference. |
| CONFERENCE_NAME | Name of the conference. |
| A_NUMBER | Number of the participant. Empty for a secret number - see Triggers. |
| A_NUMBER_NO_PREFIX | A_NUMBER without a Danish +45 prefix. |
| A_NAME | Name of the participant, if known. Empty for a secret number. |
| CALLERS_IN_ROOM | Number of participants in the room after the participant entered or left, when known. |
| DURATION | Seconds the participant was in the room. CONFERENCE_LEFT only. |
| Variable | Description |
|---|---|
| EXTENSION_ID | Id of the extension. |
| EXTENSION_NUMBER | Number of the extension. |
| EXTENSION_NAME | Name of the extension. |
| SIP_PHONE_ID | Id of the SIP phone. |
| SIP_PHONE_MAC | MAC address of the SIP phone as 12 hex digits without separators, if known. |
| EMPLOYEE_ID | Id of the employee the extension belongs to, if any. |
Only the variables of every run.
| Limit | Value |
|---|---|
| Events per customer | 100 |
| SMS recipients per event | 10 |
| Timers per TIMER event | 100 |
| Webhook requests | 60 per customer per minute |
| SMS | 10 per customer per minute - each recipient counts as one |
| Calls | 10 per customer per minute |
| Switches and variables set | 120 per customer per minute, together |
| Queue actions | 30 per customer per minute |
The per minute limits count the last 60 seconds, and test runs count too. One event can only be tested once at a time - see Test. An action that would go over its limit is skipped, and the run log shows it as failed with the error rate_limited. An SMS action with more recipients than are left of the limit is skipped as a whole.
Events run when the PBX reports what happened, and each event runs at most once for each thing that happened. Delivery is best effort: if the service that runs the events is unavailable - e.g. during maintenance - what happens on the PBX in that time does not trigger events, and is not caught up afterwards. A TIMER that is a few minutes late runs when it can. A failed webhook, SMS or call is not retried.
Do not use PBX events for anything that must never be missed, such as billing or statistics - use the CDR for that.
Every run of an event - triggered or a test - is logged with what triggered it, the variables it ran with, and for each action whether it succeeded, the HTTP status, the error and how long it took. Runs are kept for 30 days. See Log.
When an event is deleted, its runs can no longer be read, and they are removed when their 30 days are up.