CollabOps

Examples

Incoming webhook examples — a one-line curl, tokens, threads, the blocks form, and CI workflow notifications

Ready-to-copy incoming webhook examples. Replace <token> and <secret> in every example with the values you received. Fields and limits are on Incoming Webhooks; the full set of formatting and tokens is in the Message Format Reference.

1. The shortest notification

curl -X POST "https://collabops.ai/api/v2/integration/webhook/<token>" \
  -H "Authorization: Bearer <secret>" \
  -H "Content-Type: application/json" \
  -d '{"text": "**Deployed** `web` 0.4.2"}'

On success you get {"delivery_id": 1234, "deduped": false} back, and a message with bold text and a code chip appears in the channel.

2. Markdown formatting and tokens

For a multi-line body, write it to a file and send it with --data-binary @file to avoid shell quoting trouble. Line breaks inside a JSON string are written \n.

{
  "title": "Login error hotfix deployed",
  "text": "**Hotfix deployed**\nThe fix for {{issue:slug=acme|key=42|label=Login error}} is live.\n\n- Change request: {{cr:slug=acme|key=318|label=Fix session expiry}}\n- Deployed at: {{date:iso=2026-09-23T15:30:00+09:00|hasTime=1}}\n\n| Service | Version |\n|:--|--:|\n| api | 1.8.2 |\n| web | 0.4.2 |"
}
curl -X POST "https://collabops.ai/api/v2/integration/webhook/<token>" \
  -H "Authorization: Bearer <secret>" \
  -H "Content-Type: application/json" \
  --data-binary @message.json

title is used only as the preview in the channel list and notifications; it does not appear in the body. The issue and change request become chips, and the date becomes a date chip. Chat shows headings (#) as plain text, so the first line uses bold for emphasis.

Example 2 as posted — bold first line, issue, change request and date chips, aligned table

Figure 1 — Example 2 as posted in the channel

3. Continuing in a thread

Keep the delivery_id from the first response and pass it as parent_delivery_id in the next request. It is extracted with sed so the script works where jq is not installed.

URL="https://collabops.ai/api/v2/integration/webhook/<token>"
AUTH="Authorization: Bearer <secret>"

# Parent message
RESP=$(curl -sS -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"text": "**Nightly batch started**"}')
PARENT=$(printf '%s' "$RESP" | sed -n 's/.*"delivery_id":\([0-9]*\).*/\1/p')

# Reply, attached to the parent's thread
curl -sS -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"text\": \"Step 1 done (12 min)\", \"parent_delivery_id\": ${PARENT}}"

Using a reply's delivery_id as parent_delivery_id does not start a new thread; the message joins the same thread.

A 1 reply link under the parent and the reply in the thread panel on the right

Figure 2 — Replies join the parent's thread

4. The blocks form — color, table, toggle, issue chip

When you need something Markdown cannot express, send blocks. This example shows check results as a table and folds the failure log inside a toggle.

{
  "blocks": [
    {"id": "title", "type": "paragraph",
     "children": [{"text": "Nightly checks", "marks": [{"type": "bold"}]}]},

    {"id": "summary", "type": "paragraph", "children": [
      {"text": "1 of 3 "},
      {"text": "failed", "marks": [{"type": "bold"}, {"type": "textColor", "attrs": {"color": "red"}}]},
      {"text": " — tracked in "},
      {"type": "embed", "embedType": "entityRef",
       "attrs": {"kind": "issue", "workspaceSlug": "acme", "entityKey": 57, "title": "Auto-renew certificates"}}
    ]},

    {"id": "c00", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 0, "colIdx": 0, "colCount": 2},
     "children": [{"text": "Check"}]},
    {"id": "c01", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 0, "colIdx": 1},
     "children": [{"text": "Result"}]},
    {"id": "c10", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 1, "colIdx": 0},
     "children": [{"text": "DB backup"}]},
    {"id": "c11", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 1, "colIdx": 1},
     "children": [{"text": "Passed", "marks": [{"type": "textColor", "attrs": {"color": "green"}}]}]},
    {"id": "c20", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 2, "colIdx": 0},
     "children": [{"text": "Certificate expiry"}]},
    {"id": "c21", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 2, "colIdx": 1},
     "children": [{"text": "7 days left", "marks": [{"type": "textColor", "attrs": {"color": "red"}}]}]},

    {"id": "log", "type": "toggle", "children": [{"text": "Show failure log"}]},
    {"id": "log-body", "parentId": "log", "type": "codeBlock", "attrs": {"language": "text"},
     "children": [{"text": "cert-check: api.example.com expires in 7 days\nexit 1"}]}
  ]
}
curl -X POST "https://collabops.ai/api/v2/integration/webhook/<token>" \
  -H "Authorization: Bearer <secret>" \
  -H "Content-Type: application/json" \
  --data-binary @nightly.json

Example 4 as posted — red 1 failed, issue chip, colored table cells, expanded toggle

Figure 3 — Example 4 with the toggle expanded

Key points

Each table cell is one block. This table has 6 cells including the header, so the table alone is 6 blocks and the message is 10.

colCount goes only on the first cell of the table (rowIdx: 0, colIdx: 0).

Table cells are colored with a textColor mark on the text inside the cell, not with the cell's attrs. Block-level attrs colors are not painted in chat.

Blocks inside the toggle point to its id with parentId and come right after the toggle in the array. The toggle is posted collapsed and expands on click.

If any block breaks the spec, the whole message is rejected with 422 and error.title names the block index.

5. Success and failure notifications from a CI workflow

Put separate success and failure notification jobs after the deploy job. Notifications are secondary, so the pipeline must still finish green when a notification request fails. That is why a curl failure is caught with || HTTP_CODE="000" and only the response code is logged.

name: deploy

triggers:
  push:
    branches: [main]

jobs:
  deploy:
    steps:
      - name: deploy
        run: ./scripts/deploy.sh               # your actual deploy command

  # ─────────────────────────────────────────
  # Success notification — only when deploy succeeds
  # ─────────────────────────────────────────
  notify-success:
    needs: [deploy]
    if: "success()"
    steps:
      - name: notify
        image: curlimages/curl:8.17.0          # a small image that only needs curl
        env:
          # the URL and the secret are different values; store each in the secret store
          COLLABOPS_WEBHOOK_URL: ${{ secrets.COLLABOPS_WEBHOOK_URL }}
          COLLABOPS_WEBHOOK_SECRET: ${{ secrets.COLLABOPS_WEBHOOK_SECRET }}
          REF_NAME: ${{ collabops.ref_name }}
          COMMIT_SHA: ${{ collabops.sha }}
          COMMIT_MESSAGE: ${{ collabops.commit_message }}
          ACTOR: ${{ collabops.actor }}
        run: |
          # escape backslashes and double quotes before putting a value in a JSON string
          esc() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }

          TITLE=$(esc "$(printf '%s\n' "$COMMIT_MESSAGE" | head -1)")   # first line of the commit message only
          SHORT_SHA=$(printf '%s' "$COMMIT_SHA" | cut -c1-8)

          BODY=$(printf '{"text": "✅ **Deploy succeeded** `%s` `%s`\\n%s\\nby %s"}' \
            "$REF_NAME" "$SHORT_SHA" "$TITLE" "$(esc "$ACTOR")")

          # a failed notification must not fail the pipeline; just log the response code
          HTTP_CODE=$(curl -sS -o /tmp/webhook-resp -w '%{http_code}' -X POST \
            -H "Authorization: Bearer ${COLLABOPS_WEBHOOK_SECRET}" \
            -H 'Content-Type: application/json' \
            -H "Idempotency-Key: deploy-${SHORT_SHA}-success" \
            --data "$BODY" \
            "${COLLABOPS_WEBHOOK_URL}") || HTTP_CODE="000"
          echo "webhook http=${HTTP_CODE} $(cat /tmp/webhook-resp 2>/dev/null)"

  # ─────────────────────────────────────────
  # Failure notification — only when deploy fails
  # ─────────────────────────────────────────
  notify-failure:
    needs: [deploy]
    if: "failure()"
    steps:
      - name: notify
        image: curlimages/curl:8.17.0
        env:
          COLLABOPS_WEBHOOK_URL: ${{ secrets.COLLABOPS_WEBHOOK_URL }}
          COLLABOPS_WEBHOOK_SECRET: ${{ secrets.COLLABOPS_WEBHOOK_SECRET }}
          REF_NAME: ${{ collabops.ref_name }}
          COMMIT_SHA: ${{ collabops.sha }}
        run: |
          SHORT_SHA=$(printf '%s' "$COMMIT_SHA" | cut -c1-8)
          BODY=$(printf '{"text": "❌ **Deploy failed** `%s` `%s`\\nCheck the run log."}' \
            "$REF_NAME" "$SHORT_SHA")
          HTTP_CODE=$(curl -sS -o /tmp/webhook-resp -w '%{http_code}' -X POST \
            -H "Authorization: Bearer ${COLLABOPS_WEBHOOK_SECRET}" \
            -H 'Content-Type: application/json' \
            -H "Idempotency-Key: deploy-${SHORT_SHA}-failure" \
            --data "$BODY" \
            "${COLLABOPS_WEBHOOK_URL}") || HTTP_CODE="000"
          echo "webhook http=${HTTP_CODE} $(cat /tmp/webhook-resp 2>/dev/null)"

Execution flow

deploy
  ├── success → notify-success  (✅ deploy succeeded)
  └── failure → notify-failure  (❌ deploy failed)

If the notification request fails → only http=000 or 4xx is logged, and the job still succeeds

Key points

Store the URL and the secret separately in the secret store and read them with ${{ secrets.… }}. Never write the values in the workflow file.

Values that may contain quotes or backslashes, such as commit messages, are escaped with esc before going into the JSON. Without escaping, the body stops being valid JSON and returns 422.

With the commit SHA and the outcome in Idempotency-Key, re-running the job does not post the same notification twice.

See Workflow Syntax for workflow syntax and Expressions for conditions.

6. Checking the response code

To act on the result in a script, capture the status code separately with -w '%{http_code}'. Anything other than 202 carries the reason in the body's error.title.

CODE=$(curl -sS -o resp.json -w '%{http_code}' -X POST "$URL" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"text": "Checks done", "status": "ok"}')
echo "$CODE"; cat resp.json
# 422
# {"error":{"code":"VALIDATION_ERROR","title":"요청 형식이 올바르지 않습니다: extra_forbidden at status", …}}

The unaccepted field (status) caused the 422. The text form accepts only text · title · parent_delivery_id.

Table of Contents