Receive provider events
Get Slack and GitHub webhooks into your backend without holding a provider signing secret or mapping provider accounts to your users yourself.
Each integration has a receive URL. Slack or GitHub posts to it, Super
Connect verifies the request with the integration's inbound secret, works
out which connections the event concerns, answers the provider within its
deadline, and then delivers the event to your integration's webhook URL as
a signed forward event. Every request, accepted or not, is logged on the
integration page in the admin panel.
Each provider template ships a webhook adapter that verifies its
requests, answers its handshakes and chooses which connections receive an
event. Slack and GitHub have adapters today. An integration on a provider
whose template has none has a receive URL that answers 404. The API tells
you: GET /api/v1/integrations returns webhooks with the adapter's setup
instructions and the label of the secret it expects, or null.
Before you start you need an integration on the slack or github preset,
an organization key, and an HTTPS endpoint in your app. If your endpoint
does not yet verify Super Connect's signature, read
Receive webhooks first: forward events use the same
headers and signature as auth events.
1. Configure the integration
Forwarding needs two things on the integration: an inbound secret, so requests from the provider can be verified, and a webhook URL, so verified events have somewhere to go.
curl https://super-connect.dogar.biz/api/v1/integrations/slack -X PUT \
-H "Authorization: Bearer $SUPER_CONNECT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"providerId": "slack",
"scopes": "channels:read chat:write users:read",
"webhookUrl": "https://app.example.com/super-connect/events",
"webhookSecret": "GENERATE_A_LONG_RANDOM_STRING",
"inboundSecret": "THE_PROVIDER_SIGNING_SECRET"
}'The response carries receiveId. Your receive URL is
https://super-connect.dogar.biz/webhook/<receiveId>. In the admin panel,
open the integration and use the copy button under Received.
inboundSecretis encrypted at rest and never returned. The integration reportshasInboundSecretinstead.- Omit
inboundSecreton an update to keep the stored value. Sendnullto clear it. Without a secret every inbound request is rejected with401. receiveIdis minted when the integration is created and does not change. There is no rotation yet, so do not publish the URL.
2. Register the receive URL with the provider
Slack
Slack calls the Request URL the moment you save it, so the secret must be in
place first. A request that arrives before the secret is saved is rejected
with 401, and Slack marks the URL as failing.
- In your Slack app, open Basic Information and copy the Signing Secret.
- Store it as the integration's inbound secret, in the admin panel or with
the
PUTrequest above. - In Slack, open Event Subscriptions and set the Request URL to the
receive URL. Super Connect answers the
url_verificationhandshake, and Slack shows a green check. - Open Interactivity & Shortcuts and set the same receive URL. Events, interactivity and slash commands all route through one URL.
- Under Subscribe to events, choose only workspace-wide public-channel
events (
message.channels,channel_created,user_changeand the like) and the user's own events (channel_joined,channel_left,dnd_updated,manual_presence_change,pref_change,user_status_changed…). Do not subscribe tomessage.groups,message.im,message.mpim, reactions, files or pins: Slack sends those to whoever can see the content, which connection metadata cannot express, so they would reach the wrong connections.
GitHub
GitHub webhooks are created per repository or per organization, and each one carries its own secret. Point them all at one receive URL and give them all the same secret, because the integration stores a single inbound secret.
- In the repository or organization settings, open Webhooks and add a webhook.
- Set the payload URL to the receive URL and the content type to
application/json. - Set the secret to the integration's inbound secret. Reuse that one secret for every hook a customer points at this integration. A customer who cannot share one secret needs a second integration.
- Choose the events and save. GitHub sends a
pingat once. Super Connect answers it with200and logs it; apingis never forwarded.
3. Make events find connections
An event is delivered once per recipient: a connection the provider's adapter chose from the integration's connections and their metadata. An event with no recipients is not delivered at all. How the adapter chooses depends on the provider.
Slack routes on the workspace or the user, automatically. When a user
connects, Super Connect copies team.id and authed_user.id from Slack's
token response into the connection's metadata. A workspace-wide event goes
to every connection whose team.id equals the event's team_id. A personal
event such as channel_joined or dnd_updated goes to the one connection
whose authed_user.id equals the event's authorizations[0].user_id.
Interactivity payloads and slash commands route by workspace. You do not set
these keys, and the API refuses an attempt to.
Connections made before authed_user.id was captured hold only team.id;
they receive workspace events but no personal events until the user
reconnects.
GitHub routes on metadata you set. A GitHub payload carries no Super Connect identity, so tell the connection which repositories and organizations it stands for, one key per id:
curl https://super-connect.dogar.biz/api/v1/connections/$CONNECTION_ID \
-X PATCH \
-H "Authorization: Bearer $SUPER_CONNECT_API_KEY" \
-H "X-User-Id: user_123" \
-H 'Content-Type: application/json' \
-d '{ "metadata": { "repository.612345678": "1", "organization.9876543": "1" } }'- The key is
repository.<id>ororganization.<id>with the numeric ID from the payload'srepository.idororganization.id; the value is always"1". Names change on a rename; IDs do not. - A connection holds as many keys as it needs, so one connection hears several repositories. An event matches when any of its keys matches: a repository event matches on the repository, and also on the owning organization when the payload carries one.
- Send a key as
nullto delete it. The whole object must serialize to at most 4096 characters, about 180 ids.
With the SDK: client.connections.setMetadata(connectionId, metadata, userId).
4. Handle the forward event
Your endpoint receives one request per matched connection:
{
"id": "evt_9f21…",
"version": "1",
"type": "forward",
"provider": "slack",
"integrationId": "slack",
"connectionId": "conn_4b0e…",
"endUserId": "user_123",
"receivedAt": "2026-09-13T09:15:00.000Z",
"eventType": "message",
"providerEventId": "Ev09ABCDEF",
"headers": {
"content-type": "application/json"
},
"payload": {}
}provideris the provider ID of the integration that received the request, the same field theauthenvelope uses.eventTypeis the provider's own name: the Slack inner event type, slash command or interactivity payload type, or GitHub'sX-GitHub-Event.providerEventIdis Slack'sevent_id, Slack'strigger_idfor interactivity, or GitHub'sX-GitHub-Delivery. It isnullwhen the provider sent none.payloadis the parsed provider body. Slack form posts arrive decoded: thepayloadJSON for interactivity, the fields for a slash command.headersholds at most six request headers, lowercased, only when the provider sent them:content-type,x-github-event,x-github-delivery,x-github-hook-id,x-slack-retry-num,x-slack-retry-reason. Signature headers are never forwarded.
Then, in order:
- Verify the request exactly as you verify
authevents, withX-Super-Connect-Webhook-TimestampandX-Super-Connect-Hmac-Sha256overtimestamp + "." + rawBodyand your webhook secret. Branch ontypeafter verifying. - Answer
2xxquickly. Delivery is retried on network errors and5xxafter 3 and 6 seconds, then marked failed. - Deduplicate on
providerEventId, not onid. Providers resend: Slack retries up to three times when it gets no answer within three seconds, and GitHub reusesX-GitHub-Deliveryon a manual redelivery. Each resend is a fresh request to Super Connect with a freshid. WhenproviderEventIdisnull, fall back toid, which still covers Super Connect's own retries. A Slack retry carriesx-slack-retry-numinheaders. - Do not expect unmatched events. A verified event with no recipients
is logged as
unmatchedand never delivered, soconnectionIdandendUserIdare always set. A workspace or repository your customer never connected shows up in the received log, not at your endpoint.
[!IMPORTANT] Slack interactivity is answered with an empty
200before the event is forwarded, so you cannot return aresponse_action. Aview_submissioncloses the modal; validation errors,updateandpushare not available through forwarding. Update the view afterwards withviews.update.
5. Read the received log
Every request to a receive URL is logged, verified or not, with its raw body
kept for seven days. The admin panel shows the log on the integration page
under Received, with each row's headers, body and the forwards it
produced. The same rows are available from
GET /api/v1/integrations/:id/webhooks.
| Outcome | What happened |
|---|---|
forwarded |
Verified and the adapter chose at least one recipient; matched counts them |
unmatched |
Verified but the adapter chose no recipient; nothing delivered |
filter_failed |
Verified but the adapter failed while choosing recipients; error holds the message |
replied |
Answered directly: a Slack challenge or a GitHub ping |
rejected |
Refused before routing, with error naming the reason |
too_large |
The body was over 1 MiB and was not read |
A rejected row with a 401 usually means the secret in the provider's
console and the integration's inbound secret differ, or the secret was saved
after the provider's first call.
6. Develop locally
The local Worker listens on port 4242. A tunnel to it gives you a public receive URL to paste into Slack or GitHub.
npm run dev
cloudflared tunnel --url http://localhost:4242Use the tunnel's HTTPS host in place of https://super-connect.dogar.biz,
both in the receive URL you give the provider and in the webhookUrl on the
integration.
To replay an event without touching the provider, fetch a logged body and post it back with a fresh signature. The original headers cannot be reused: Super Connect verifies every request against the stored secret, and Slack timestamps older than five minutes are rejected.
curl "$SUPER_CONNECT/api/v1/integrations/slack/webhooks/$INBOUND_ID" \
-H "Authorization: Bearer $SUPER_CONNECT_API_KEY" | jq -r .body > event.json
BODY=$(cat event.json)
TIMESTAMP=$(date +%s)
SIGNATURE="v0=$(printf 'v0:%s:%s' "$TIMESTAMP" "$BODY" |
openssl dgst -sha256 -hmac "$SLACK_SIGNING_SECRET" -r | cut -d' ' -f1)"
curl "$TUNNEL/webhook/$RECEIVE_ID" -X POST \
-H 'Content-Type: application/json' \
-H "X-Slack-Request-Timestamp: $TIMESTAMP" \
-H "X-Slack-Signature: $SIGNATURE" \
--data "$BODY"For GitHub, sign the body alone with no timestamp and send it as
X-Hub-Signature-256: sha256=<hex>, with the event name in
X-GitHub-Event.