Pingtower Docs
Docs Self-hosting Relay & pairing

Relay & pairing

How a box you run pages a phone it cannot push to. The box calls out; pingtower.com forwards; nothing calls in.

The shape of it

  1. The box mints a join code: pingtower pair --email [email protected].
  2. The app redeems the code against the box and gets a box-minted ptk_ key, plus a receipt.
  3. pingtower.com confirms the receipt with the box, once, at GET /v1/pair/verify.
  4. The app writes the relay credential to the box with PUT /v1/relay.
  5. From then on, every project on the box has a webhook integration named “pingtower.com relay”. Every alert event goes out through it like any other webhook, and pingtower.com turns it into a push to the paired devices of the members allowed to see that alert’s project, critical or ordinary by the same rule as a hosted page. Project grants live on the box, so the box sends the list of its accounts allowed to hear each event, and pingtower.com pushes to no one outside it. A box older than Release 1 sends no list, and then every paired member is paged.

The box never accepts a connection from pingtower.com beyond the one-time verify. Everything is outbound from the box, so it works from behind NAT as long as the app can reach the box’s URL when it pairs.

What the box needs

  • A public URL the app can reach, with TLS in front. See Reverse proxy.
  • To answer /v1/pair/verify from pingtower.com at pairing time.
  • -secret-key set, so the relay credential is sealed at rest like any other integration secret. See the warning before you set it: the key is one-way.

The relay integration has one writer

PUT /v1/relay is the only thing that writes the relay’s rows, for every role, owners included. The ordinary integration and webhook routes answer 409 managed by /v1/relay to any edit, disable, enable or delete of one, and a new channel may not take its name (422). So a member cannot repoint or switch off the box’s pages, and re-running PUT /v1/relay is also how you re-enable a relay row that failures tripped. GET /v1/relay lists each project’s row as it stands, so a tripped or missing one is visible; so does Account → Who gets paged in the app. Test-firing it needs an owner, an admin or any role with team_settings:write.

Seats

Each paired account is a seat. POST /v1/pair/codes on the box, by an owner, an admin or any role with members:write, mints another code, for a role whose permissions the minter also holds (a code for owner or a role holding members:write also needs the minter’s "current_password", unless the call comes from the minter’s own paired phone), and answers with that role’s role_name; ttl_seconds defaults to an hour and is capped at seven days. Seats are a box’s alone: pingtower.com neither mints nor redeems one, and answers 409. Removing the minter, or changing their role or project grants, expires every code they minted that is still unused, and redeeming one is then a 401 unknown or expired pairing code. At redemption the minter must also still be a member who is not project-limited, holds members:write and could give the code’s role. A change that expires nothing, such as an owner editing the minter’s custom role, can still break that, and the box then answers 409 the person who issued this code can no longer grant its role and the code stays unused. DELETE /v1/tenants/{tid}/pairings/{aid} on pingtower.com revokes a seat, which stops that person’s pages and nothing else. Revoking someone else’s seat needs members:write and works only on a member whose team role and every project grant’s role you could give; anyone else is a 403 cannot revoke the pairing of a higher role.

Rotating the relay key

POST /v1/tenants/{tid}/relay-key on pingtower.com mints a new credential; write it to the box with PUT /v1/relay. Until you do, relayed pages fail and show in the relay integration’s delivery log on the box.

Without pairing

A self-hosted box needs none of this to alert. Webhook, Slack, Telegram and Discord deliver from the box directly. Pairing is only for the phone.

Last updated 4 Oct 2026 Report a problem with this page