Team approvals: sending agent requests to a shared channel
A DM to one operator is the easy case. The harder, more common one: an approval needs to land somewhere a whole on-call rotation or team channel can see it, so whoever's actually awake answers it. Here's what ships today for that — a shared email inbox and a webhook pointed at Slack or Discord — the real payload shapes, not the aspirational ones, plus an honest read on Telegram groups and native Slack/Discord apps.
On this page
A shared inbox or alias (email channel)
The simplest way to get an approval in front of a team: verify a shared address —
oncall@yourteam.com, a distribution list, whatever your mail provider already fans
out to multiple inboxes — as an email channel:
POST /v1/channels
{ "type": "email", "address": "oncall@yourteam.com" }
A new email channel starts unverified — a 6-digit code (15-minute TTL, single-use) is sent to
the address, and POST /v1/channels/:id/verify with that code flips it to verified.
For a distribution list, whoever's monitoring it at setup time reads the one verification email
and completes it once; delivery afterward goes to everyone on the list.
Approval and question emails carry one signed one-click link per option — Approve/Deny, or one
per question option, each a plain GET to /a/:request_id/:sig
that preselects the answer without recording anything by itself (only the page's own
POST does). Worth being direct about what that means for a shared inbox:
the link is the only authorization check. Anyone who can see the email — anyone
with access to the shared inbox or list, a delegate, an old forwarding rule nobody cleaned up —
can click it and answer. There's no per-recipient identity in the loop, just one signed URL mailed
to every subscriber. That's the point for "whoever's on call answers fastest," and worth weighing
if the inbox's audience is wider than the on-call group.
A webhook pointed at Slack or Discord
The webhook channel type is generic — any URL that accepts a signed JSON
POST, including a Slack or Discord incoming webhook URL, works today
with no code changes on pendnt's side:
POST /v1/channels
{ "type": "webhook",
"url": "https://hooks.slack.com/services/T000/B000/xxxxxxxx",
"secret": "optional-per-channel-secret" }
Unlike email, a webhook channel is verified immediately on creation — there's
no proof-of-control step for the destination URL in v1, so anyone holding a valid workspace API
key can point delivery at any URL they choose. Response is the usual serialized channel:
{ id, type, verified, created_at, config }.
Here's the real body (src/queue/channels/webhook.ts), not a paraphrase of it:
POST <your url>
content-type: application/json
x-agentio-signature: <hmac>
{
"type": "agentio.delivery",
"request_id": "req_...",
"workspace_id": "ws_...",
"kind": "approval",
"title": "Deploy release v1.2.3 to prod?",
"details": "triggered by nightly-deploy cron",
"status": "pending",
"answer_url": "https://api.pendnt.dev/a/req_.../7f3ac1e9...",
"options": null
}
The signature is HMAC-SHA256 over the exact raw body above, using the channel's own
secret if you set one, else falling back to the workspace's SIGNING_SECRET
— verify it before trusting a delivery came from pendnt. answer_url is the same
signed /a/:id/:sig link the email channel uses, with the same property: it's scoped
to the request, not to a person. Post that message into a shared Slack or Discord channel and
anyone with read access to the channel can open the link and answer it — same
"first responder wins" model as the shared inbox above, just with presence more visible.
Tell pendnt the destination is Slack or Discord by setting format when you create the channel: {"type":"webhook","url":"https://hooks.slack.com/services/...","format":"slack"} (or "discord"). Delivery then posts a plain-text body — {"text": ...} for Slack, {"content": ...} for Discord — carrying the title, details, and the signed answer link, which incoming webhooks accept directly. The default "json" keeps the structured envelope shown above (with its x-agentio-signature header) for receivers you write yourself.
Telegram: groups vs. DMs, honestly
The Telegram channel links a chat by having a
human send /start <code> to the bot, which stores that chat's chat_id
against the workspace (src/routes/telegram.ts). The handler never inspects
message.chat.type — it just reads message.chat.id off whatever update
arrives. Group chats have their own chat_id, same shape as a private chat's, and a
bot added to a group receives its messages the same way.
So it's plausible that inviting the bot to a team group and running
/start <code> there would link the group's chat_id the same way a
DM does, sending every future approval to the whole group — nothing in the code blocks it. But
that's a read of the source, not a verified behavior: test/telegram.test.ts and the
README's own writeup only exercise linking from a private chat, and the DEV_MODE mock
in src/lib/telegram.ts hardcodes chat.type: "private" on every fabricated
response, which doesn't touch this path either way. Treat group linking as untested, not as
"supported" or "blocked," until someone tries it against a real group.
Native Slack/Discord apps: not yet
Purpose-built Slack and Discord integrations — interactive Block Kit or component buttons,
channel selection instead of a raw webhook URL — are not built.
src/queue/channels/slack.ts and src/queue/channels/discord.ts are
registered stubs: calling either throws "slack channel not implemented (request ...)"
/ "discord channel not implemented (request ...)". Their TODOs sketch the real
shape — a Slack app with OAuth plus a /slack/interactions route, or a Discord
Application with an Interactions Endpoint and Ed25519 signature verification — but neither exists
yet. Use the webhook channel above in the meantime; it's real and working, just not native.
Which one to use
If your team lives in Slack or Discord, the webhook channel with format set gets you shared approvals today: the message lands in-channel as text and whoever taps the signed link first answers. Native apps with in-channel buttons remain on the roadmap.