Send Keys
Send messages and files to SuperApp from scripts, alerts, and other tools. A send key posts as you to a channel or DM in one workspace. Messages appear as “Owner via Label.” SuperApp checks your posting access on every request.
Create a Key
- Open Settings → Webhooks and select your workspace.
- Select Create webhook and enter a name, up to 64 characters. This is the label shown on messages sent with the key.
- Optionally choose a default channel and an expiry.
- Create the webhook and copy its delivery URL. The secret is shown only once.
Active workspace members who are not blocked can create a key. The default channel is optional: each request can choose another channel or DM in that workspace where you can post.
Keep the delivery URL and key private. Anyone holding either can send as you. Revoke a leaked key in Settings → Webhooks; revocation normally takes effect within 30 seconds.
Keys can send messages and upload files by default. They cannot read messages
or files. Enable Allow requests to run an agent using my access and
credits only if your integration needs agent runs. This grants
agent:run; a SuperApp-format request can then set run: true
with an idempotency key. Runs use your access and credits and refuse tool calls
that need a person's confirmation.
Send a Message
Send a POST request to the delivery URL:
/webhooks/inbound/{key}. Use the host from the URL you copied.
In these shell examples, set WEBHOOK_URL to that delivery URL.
This request uses the key's default channel:
curl --fail-with-body "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
--data '{"markdown":"Deploy **finished**."}'
If your sender supports headers, keep the key out of the URL. Set
API_ORIGIN to the delivery URL's origin (scheme and host, with no
trailing slash) and SEND_KEY to its final path segment. Send to
POST /webhooks/inbound with Authorization: Bearer <key>:
curl --fail-with-body "$API_ORIGIN/webhooks/inbound" \
-H "Authorization: Bearer $SEND_KEY" \
-H 'Content-Type: application/json' \
--data '{"markdown":"Deploy **finished**."}'
Use a send key created in Webhooks settings, not a full API key.
The destination is the first valid SuperApp channel or DM ID or link found in:
- The body's
channelfield, for SuperApp or Slack bodies. - The URL's
channelquery parameter. - The key's default channel.
For a tool whose body you cannot edit, append ?channel=CHANNEL_ID
to the delivery URL. Use a SuperApp ID or link; Slack channel IDs are ignored.
If no destination resolves, the request returns 400 invalid_payload.
Message Fields and Mentions
Use Content-Type: application/json for a message with text or
files uploaded beforehand. The JSON body is limited to 64 KiB. Replace the
example UUIDs with your SuperApp IDs:
{
"channel": "8f0c2a64-6a3e-4c1b-9d41-0a7f5d2b9e10",
"markdown": "Deploy failed. <@3b1d9c47-2f6e-4a8b-b5d0-7c9e1f2a4b6d> is on call.",
"broadcast": "here",
"label": "Release Bot",
"thread_key": "deploy/release-42",
"idempotency_key": "release-42/failed"
}
| Field | Meaning |
|---|---|
channel | Destination channel or DM ID or link. |
markdown | Message text in SuperApp Markdown. |
broadcast | here or all to notify the channel. |
label | Label for this message, overriding the key's name; at most 64 characters. |
thread_root_id | Top-level message ID to reply to. |
thread_key | Your identifier for a thread in the destination. |
idempotency_key | Your identifier for retrying one request safely for 24 hours. |
upload_ids | Array of completed direct upload IDs to attach. |
A message needs text, a broadcast, or a file. A label longer than
64 characters returns 400 invalid_payload.
Mention People and Link Channels
In markdown, <@user_id> mentions a person and
<#channel_id> links a channel. Use SuperApp IDs, without a
display name; SuperApp fills in the current name. A person mention can notify
that person. A channel link does not notify its members.
The person must be a member of the workspace or destination, and you must be able to read a linked channel. Unresolvable tokens stay literal. A message can name at most 100 distinct people and channels.
Tokens inside code spans or blocks do not mention anyone. Escape a token's opening angle bracket with a backslash to show it literally. When inserting text from another system, escape ASCII punctuation with backslashes to keep it from adding Markdown formatting, links, or mentions.
Notify a Channel
Set broadcast to here or all to notify
the channel as SuperApp's @here or @all does. A
broadcast in a reply notifies channel members of that reply without posting
it again in the channel. In a DM, every message already notifies every member.
Text alone never broadcasts in markdown: @here,
@all, and <!here> stay literal without the
broadcast field. For a broadcast without text, send:
{"markdown":"","broadcast":"here"}
A body with only broadcast is an event, not a broadcast request.
Channels above the broadcast audience limit return
400 broadcast_too_large.
Threads
Use either thread_root_id or thread_key, never both.
Both select a thread within the destination; neither changes the channel.
thread_root_idreplies to an existing, live top-level SuperApp message in that channel. Use the root'smessage_idfrom a previous send response.thread_keylets you group related events without storing message IDs. The first send starts a top-level message; later sends with the same key in the same channel reply to it.
For example, use deploy/release-42 for all updates to one
deployment. A thread key belongs to the channel, so different send keys
using it share the thread. After 365 days, or if the root is deleted, the
next send with that thread key starts a new thread.
Retries and Idempotency
Give each event an idempotency_key and reuse it when retrying
that event. You can instead use the Idempotency-Key header.
Keys are limited to 256 bytes. If you provide both, they must match.
Requests with run: true require an idempotency key.
For 24 hours after a successful send, the same request with the same
idempotency key returns the original result without creating another message
or file. Different content with that key returns
409 idempotency_key_reused. Use a new key for each new event.
Deduplication is scoped to the send key. Replacing your send key starts a new namespace. Retries still require posting access and count against rate limits. Without an idempotency key, or after 24 hours, a retry can post another message.
Upload Files
Inline Uploads
Send multipart/form-data with the JSON payload
part first, followed by file parts. The entire request can be
up to 25 MiB, with at most 10 files. The JSON payload is still limited to 64 KiB.
curl --fail-with-body "$WEBHOOK_URL" \
-F 'payload={"markdown":"Nightly report","idempotency_key":"report/2026-10-21"}' \
-F 'file=@report.pdf'
This example uses the default channel. Add channel to the
payload if your key has no default. Let your HTTP client set the multipart
boundary; do not manually set a JSON content type for this request.
Direct Uploads
For larger files, upload the bytes before sending the message. Direct uploads allow up to 1 GiB per file, or 5 GiB for video, subject to your upload budget. Reserve at most 10 files per request:
- Send
POST /webhooks/inbound/{key}/uploads, orPOST /webhooks/inbound/uploadswith bearer authentication. Provide a destination and each file'sname, MIMEtype, and exact bytesize. - Upload each file's bytes to its returned
upload_url. - Send a message with the returned
upload_idvalues inupload_ids, using the same send key and destination.
Reserve an upload (replace the size with your file's actual byte count):
curl --fail-with-body "$WEBHOOK_URL/uploads" \
-H 'Content-Type: application/json' \
--data '{
"channel":"8f0c2a64-6a3e-4c1b-9d41-0a7f5d2b9e10",
"files":[{"name":"report.pdf","type":"application/pdf","size":1048576}]
}'
The response has ok: true and an uploads array.
Each entry contains upload_id, upload_url, and
expires_at. Set UPLOAD_URL to the returned resumable
upload URL, then upload the matching file:
curl --fail-with-body -X PUT "$UPLOAD_URL" \
-H 'Content-Type: application/pdf' \
--data-binary @report.pdf
After the upload completes, send this JSON to the send endpoint, replacing
the example upload ID with the returned upload_id:
{
"channel": "8f0c2a64-6a3e-4c1b-9d41-0a7f5d2b9e10",
"markdown": "Nightly report",
"upload_ids": ["ac96a8d0-5f73-4726-987d-efbd7167b515"],
"idempotency_key": "report/2026-10-21"
}
Attach direct uploads within 6 hours of reservation. The stored size must match the declared size. A message can attach at most 10 files total, including inline files. Uploaded files become channel attachments only when the message is saved.
Slack Bodies and Other Events
You can point a tool's Slack incoming-webhook integration at your SuperApp
delivery URL. A JSON body containing text, blocks,
or attachments, and neither markdown nor
upload_ids, uses Slack format:
{
"text": "Deploy *finished*.",
"username": "Release Bot",
"thread_ts": "deploy/release-42"
}
- Slack text, blocks, and attachments become SuperApp Markdown.
thread_tsacts asthread_root_idwhen it is a SuperApp message ID, and asthread_keyotherwise.usernameoverrides the label, truncated to 64 characters.channelmust contain a SuperApp ID or link. Slack channel IDs are ignored, so set a default channel or URL query parameter.- Person and channel mentions work with SuperApp IDs. Slack IDs become plain text rather than resolving to SuperApp users or channels.
<!here>broadcasts tohere;<!channel>and<!everyone>broadcast toall. Unlike Slack, these notify channel members on replies too.
Slack parsing rules apply to special mentions. Plain forms such as
@channel can broadcast where Slack parses them, such as
text with link_names enabled or a mrkdwn
object without verbatim: true. Escape text from other systems
according to Slack's rules. The guarantee that copied text never broadcasts
applies to SuperApp's markdown format.
SuperApp-only fields, including broadcast, run,
and idempotency_key, are ignored in Slack bodies. Use the
Idempotency-Key header for safe retries.
Other JSON and Plain Text
Any other valid JSON body posts whole as a formatted JSON code block.
This includes bodies that mix SuperApp and Slack formats. None of the
event's fields control routing, threads, labels, broadcasts, or runs.
Use the URL's channel parameter or the default destination,
and the Idempotency-Key header for retries.
A field selects a format only with the expected type: markdown
and text are strings; upload_ids, blocks,
and attachments are arrays. Invalid JSON returns
400 invalid_payload.
A multipart payload mixing the two formats returns
400 invalid_payload instead of posting as an event. Files can
accompany either format; a multipart payload with neither format's text
fields uses SuperApp format.
A text/plain body posts literally, without formatting, links,
or mentions. It also uses the URL or default destination and the idempotency header.
For application/x-www-form-urlencoded, a JSON payload
field follows the JSON rules; without one, the raw body posts as a code block.
Responses and Errors
A saved message returns 200:
{"ok":true,"message_id":"…","channel_id":"…","thread_root_id":"…"}
Errors return a status code and a JSON body such as:
{"ok":false,"error":"invalid_payload"}
| Status | Error | What to Do |
|---|---|---|
400 | invalid_payload | Check JSON, destination, field types, thread options, and idempotency rules. |
400 | too_many_mentions | Reduce to at most 100 distinct people and channels. |
400 | broadcast_too_large | Remove the broadcast or choose a channel within the audience limit. |
401 | invalid_key | Check the send key; it may be unknown, revoked, expired, or the wrong key type. |
403 | run_not_permitted | Enable agent runs for the key or omit run. |
404 | channel_not_found | Check the destination and your permission to post there. |
404 | thread_not_found | Use a live top-level message in the destination. |
404 | upload_not_found | Check completion, the 6-hour window, send key, and destination. |
409 | idempotency_key_reused | Use a new idempotency key for different content. |
410 | channel_is_archived | Choose an active destination. |
413 | payload_too_large | Reduce the body or files, or use direct uploads. |
429 | rate_limited | Wait for Retry-After before retrying. |
503 | unavailable | Retry after Retry-After. |
Reuse the same idempotency key when retrying a temporary failure or an
uncertain response. A 404 does not distinguish a nonexistent
channel from one you cannot access.
Need help with a request? Contact Support.