Slack Endpoints¶
Slack delivery rides the webhook transport: the pg_relay Processor (≥ 1.2) POSTs a JSON body — built entirely by this extension — to Slack's chat.postMessage API, and hands the response back to this extension to classify. The Processor knows nothing about Slack specifically; the provider adapter (provider = 'slack') lives in SQL, in this extension.
One-time Slack setup¶
- Create a Slack app at api.slack.com/apps → Create New App → From scratch, in your workspace.
- Add the bot scope: OAuth & Permissions → Scopes → Bot Token Scopes →
chat:write. - Install the app to the workspace (same page). Copy the Bot User OAuth Token (
xoxb-...). -
Export the token on the Processor host (and only there):
The token is never stored in the database — the profile references it as
"_env:SLACK_BOT_TOKEN", resolved by the Processor from its own environment at send time, exactly like SMTP passwords and M365 client secrets (see SMTP Endpoints for the full rationale). The Processor reads its environment at start, so rotating the token means restarting the Processor. -
Invite the bot to each target channel (
/invite @your-appin Slack) — a bot can only post where it is a member; otherwise sends fail withnot_in_channel.
The profile¶
SELECT pgrelay_notifier.create_channel('slack'); -- or reuse an existing notify channel
SELECT pgrelay_notifier.create_profile(
p_profile_name => 'slack_bot',
p_transport => 'webhook',
p_provider => 'slack',
p_profile => '{
"url": "https://slack.com/api/chat.postMessage",
"auth": {"style": "bearer_header", "secret": "_env:SLACK_BOT_TOKEN"}
}'::jsonb,
p_channel => 'slack'
);
| Key | Required | Meaning |
|---|---|---|
url |
✔ | https://slack.com/api/chat.postMessage |
auth.style |
✔ | bearer_header — the Processor sends Authorization: Bearer <token> |
auth.secret |
✔ | Must be an "_env:VAR_NAME" reference to the bot token — a literal value is rejected |
body_merge |
Optional, generic to the webhook transport (not needed for Slack): a JSON object overlaid onto the request body at send time — see The body_merge overlay below |
|
timeout_seconds |
Default 30, cap 120 |
p_provider is required for webhook profiles: it selects the provider adapter — the pair of functions that build the request body and interpret the response. Supported today: 'slack', plus 'resend', 'telnyx', and 'pagerduty' (their own page); the transport itself is generic, and new providers are added by an extension release with no Processor change.
Senders then post with ordinary send()/compose() calls, using the Slack channel id as the recipient — formatting, Block Kit layouts, and threading are covered in Sending to Slack in the User Guide.
The body_merge overlay (generic webhook feature)¶
Slack doesn't need it, but the webhook transport carries one more optional profile key, for providers that authenticate inside the request body rather than in a header: body_merge, a JSON object whose top-level keys the Processor overlays onto the request body immediately before sending (pg_relay ≥ 1.2).
- Values may be
_env:VAR_NAMEreferences. Becausebody_mergelives in the profile, its string values are resolved from the Processor host's environment exactly likeauth.secret— which is the whole point: a body-carried credential (PagerDuty'srouting_key, Vonage's legacy SMSapi_key/api_secret) never enters the database. - Merged keys always win. A producer-supplied value for the same top-level key is silently overwritten. The key is profile-owned, so a sender can never inject, spoof, or occupy it — this is a security property, not a default.
- Shape-blind. Top-level key assignment only — no recursive merge, no inspection or interpretation of the message; values of any JSON type pass through verbatim.
- Validated up front.
create_profile()/update_profile()accept abody_mergeof JSON type object and reject any other type with a clear message. The Processor treats a non-objectbody_mergeas a permanent failure with no request sent — exactly the malformed-authcontract — so validation front-runs what would otherwise fail at send time.
The motivating example, PagerDuty Events API v2 (provider adapter still on the roadmap):
// profile — the routing key lives on the Processor host, not in the database
{
"url": "https://events.pagerduty.com/v2/enqueue",
"auth": {"style": "bearer_header", "secret": "unused"},
"body_merge": {"routing_key": "_env:PD_ROUTING_KEY"},
"timeout_seconds": 30
}
// request body rendered by the adapter — no routing_key anywhere;
// the profile injects it at send time
{"event_action": "trigger", "dedup_key": "pgrelay-33", "payload": {"...": "..."}}
Full Processor-side semantics are in WEBHOOK_TRANSPORT_PROCESSOR_SPEC.md §2.1 in the repository.
How responses are classified¶
Slack answers HTTP 200 for both success and failure, distinguishing in the JSON body — the adapter handles that quirk:
| Slack's response | Outcome |
|---|---|
200 with "ok": true |
sent — the message timestamp (ts) is recorded as provider_ref (also the handle for threading replies) |
200 with "ok": false |
failed, with the Slack error in status_detail (channel_not_found, not_in_channel, invalid_auth, msg_too_long, invalid_blocks, ...) |
| 429 | retry (Slack rate limit; the Retry-After header is quoted in the detail) |
| 5xx, or no response at all | retry |
| Anything else | failed |
Retries follow the channel's max_retries and backoff exactly as for email — see Channels and Concurrency.
Things worth knowing¶
not_in_channelis the most common first failure — invite the bot to the channel.invalid_authmeans the token is wrong, revoked, or the env var is unset/misspelled on the Processor host (an unset_env:variable is itself a clean permanent failure naming the variable).- Posting to a user instead of a channel works with the user's member id (
U...) if the app hasim:write; the adapter passes whatever id the sender supplies straight through aschannel. - Slack rate-limits
chat.postMessageper channel (roughly one message per second, in bursts). A busy alert channel is a good candidate forconcurrency_mode = 'channel'so 429-retries stay ordered. - File attachments added with
attach()are not delivered to Slack —chat.postMessagecannot carry them. - The debug trace (Debug Tracing) additionally records the HTTP status of each webhook attempt.