Skip to content

Delivery

Ryan M. edited this page Sep 17, 2026 · 2 revisions

Delivery

delivery tells Ordryn how to POST when a hook fires.

Field Required Notes
type yes One of the types below.
url_from yes Must be a settings key (almost always a secret).
format no Only for http.webhook. One of json, text, or content. Defaults to text ({"text": "…"}) if omitted. Rejected on provider-specific types.

Every delivery is POST with Content-Type: application/json, a 5s timeout, and no redirects. Interpolated message text is trimmed to 2000 characters; @everyone / @here are stripped.

Outbound headers:

Header When
User-Agent: Ordryn-Webhook/1 Always
X-Ordryn-Event-Id Always (event id)
X-Ordryn-Delivery-Id Queued deliveries
X-Ordryn-Signature: sha256=<hex> After rotate_signing; HMAC-SHA256 of the raw JSON body

Project owners can send a test post from Project settings → Extensions (bypasses enable/trigger filters; still needs a webhook URL). That control is send_test. See Settings and controls.

Types

Prefer the dedicated Discord / Slack / Teams / Google Chat / ntfy examples when targeting those services: they send the payload the service expects and restrict the URL host. Use http.webhook for everything else. delivery.type: "email" is rejected (site email is not available to extensions).

delivery.type Use for Body
http.webhook Any public HTTPS receiver (n8n, Zapier, Mattermost, custom listeners, email relays) Chosen by format
discord.webhook Discord incoming webhooks {"content": "…"} plus a small embed
slack.webhook Slack incoming webhooks {"text": "…"} plus Block Kit with an Open button
teams.webhook Teams Workflows incoming webhooks (Office 365 Connectors are retired) Adaptive Card
googlechat.webhook Google Chat incoming webhooks Chat text plus cardsV2 (generic extra JSON fields are rejected by Chat)
ntfy.webhook ntfy topic URL (ntfy.sh or a public HTTPS ntfy server) ntfy push; optional Authorization: Bearer from ntfy_auth

http.webhook formats

format Body
json Structured event (below). Includes text and content plus event fields.
text {"text": "…"} (Slack-style). Default if format is omitted.
content {"content": "…"} (Discord-style).

Example format: json body

From the webhook example:

{
  "text": "Task Ship updated to Done in project Ordryn",
  "content": "Task Ship updated to Done in project Ordryn",
  "event": "task.updated",
  "event_id": "00000000-0000-0000-0000-000000000001",
  "occurred_at": "2026-09-15T16:00:00Z",
  "changed": ["status"],
  "id": "42",
  "name": "Ship",
  "task": "Ship",
  "status": "Done",
  "old_status": "In progress",
  "project": "Ordryn",
  "actor": "ada",
  "url": "https://todo.example.com/tasks/42",
  "priority": "High",
  "comment": "",
  "claimed_by": "ada",
  "due_date": "2026-09-20",
  "sprint": "Sprint 1",
  "tags": "release",
  "mentions": "",
  "member": "",
  "fields": {"severity.level": "high"},
  "actor_detail": {"id": 7, "name": "ada"},
  "task_detail": {
    "id": 42,
    "title": "Ship",
    "status": "Done",
    "priority": "High"
  },
  "project_detail": {"id": 3, "name": "Ordryn"},
  "changes": [{"field": "status", "old": "In progress", "new": "Done"}]
}

Also present when applicable:

Field When
count task.reordered, import.completed
join_email join.*
config Generic select / status / user / string / int setting values
digest_events Digest batches (up to 10 {name} ({event}) lines)
callback_token, callback_url Manifest declares any permissions

Email/due-dates relays read text as the email body. Use that same JSON shape.

Queue

Deliveries are queued: a short coalesce window (~3s), quiet hours, and hourly/daily digest still apply. Failed rows retry up to 8 attempts, then become dead. Owners can retry from Admin → Extensions, the project Extensions tab, or Settings → Integrations (POST …/deliveries/{id}/retry).

Provider notes

Discord (discord.webhook): messages include content plus an embed (title, status, actor, Open URL). When Discord returns a message id, later task.commented events post as thread replies (wait=true on create). Use Discord **bold** in templates.

Slack (slack.webhook): Block Kit with an Open button. Incoming webhooks cannot thread reliably without a Slack bot. Ordryn stores a provider message id when the webhook response includes ts and will thread later comments only in that case. Use Slack mrkdwn (*bold*).

Teams (teams.webhook): Adaptive Card with title and Open action. Use a Teams Workflow (“Post to a channel when a webhook request is received”). URLs often live on *.logic.azure.com or *.api.powerplatform.com and may include a query string.

Google Chat (googlechat.webhook): text plus cardsV2 (header, status/actor fields, Open button). Comments on the same task reuse a threadKey (ordryn-task-{id}) when Chat allows it. Use Chat text formatting (*bold*). Do not send this payload through http.webhook — Chat rejects extra JSON fields.

ntfy (ntfy.webhook): HTTPS topic URL such as https://ntfy.sh/my-topic. Optional setting ntfy_auth is sent as Authorization: Bearer …. Private/LAN ntfy hosts are rejected.

URL rules

All types require HTTPS, no credentials in the URL, and no fragment. Loopback, private, link-local, and similar destinations are rejected (SSRF protection). Ordryn will not POST to localhost.

Type Extra host / path rules
http.webhook Any public HTTPS host. Non-443 ports are allowed.
discord.webhook discord.com, discordapp.com, canary.discord.com, or ptb.discord.com; path /api/webhooks/…; no query string.
slack.webhook hooks.slack.com; path /services/…, /triggers/…, or /workflows/….
teams.webhook *.logic.azure.com, *.api.powerplatform.com, webhook.office.com, or Outlook Office hosts; path required. Query strings are allowed (Workflows URLs include them).
googlechat.webhook Google Chat incoming webhook hosts (chat.googleapis.com URLs with key / token query params).
ntfy.webhook Public HTTPS ntfy host. Private/LAN self-hosted ntfy is rejected.

Signing

Outbound requests include X-Ordryn-Signature: sha256=<hex> when a signing secret is rotated on the Extensions panel (rotate_signing). The HMAC is over the raw JSON body.

Sample relays in email/relay, due-dates/relay, and callback-bot/relay verify that header against ORDRYN_SIGNING_SECRET. Copy the secret once after rotating it; the API does not return secret values later.

Inbound HMAC uses a different string (timestamp + "." + body). See Callbacks and inbound.

Clone this wiki locally