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

  1. Open Settings → Webhooks and select your workspace.
  2. Select Create webhook and enter a name, up to 64 characters. This is the label shown on messages sent with the key.
  3. Optionally choose a default channel and an expiry.
  4. 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:

  1. The body's channel field, for SuperApp or Slack bodies.
  2. The URL's channel query parameter.
  3. 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"
}
FieldMeaning
channelDestination channel or DM ID or link.
markdownMessage text in SuperApp Markdown.
broadcasthere or all to notify the channel.
labelLabel for this message, overriding the key's name; at most 64 characters.
thread_root_idTop-level message ID to reply to.
thread_keyYour identifier for a thread in the destination.
idempotency_keyYour identifier for retrying one request safely for 24 hours.
upload_idsArray 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.

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:

  1. Send POST /webhooks/inbound/{key}/uploads, or POST /webhooks/inbound/uploads with bearer authentication. Provide a destination and each file's name, MIME type, and exact byte size.
  2. Upload each file's bytes to its returned upload_url.
  3. Send a message with the returned upload_id values in upload_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 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"}
StatusErrorWhat to Do
400invalid_payloadCheck JSON, destination, field types, thread options, and idempotency rules.
400too_many_mentionsReduce to at most 100 distinct people and channels.
400broadcast_too_largeRemove the broadcast or choose a channel within the audience limit.
401invalid_keyCheck the send key; it may be unknown, revoked, expired, or the wrong key type.
403run_not_permittedEnable agent runs for the key or omit run.
404channel_not_foundCheck the destination and your permission to post there.
404thread_not_foundUse a live top-level message in the destination.
404upload_not_foundCheck completion, the 6-hour window, send key, and destination.
409idempotency_key_reusedUse a new idempotency key for different content.
410channel_is_archivedChoose an active destination.
413payload_too_largeReduce the body or files, or use direct uploads.
429rate_limitedWait for Retry-After before retrying.
503unavailableRetry 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.