Slack incoming webhook "invalid_payload" error in CI
Slack returns invalid_payload with HTTP 400 when the body sent to an incoming webhook is not valid JSON or has no usable message field. In CI this almost always comes from a commit message or branch name interpolated into the JSON without escaping, which breaks the quoting.
What this error means
The notify step posts to the Slack webhook and gets HTTP 400 with the body invalid_payload. It often starts failing only for commits whose messages contain quotes, newlines, or backslashes.
< HTTP/2 400
invalid_payloadCommon causes
Unescaped values break the JSON
A commit message or PR title with a double quote, newline, or backslash is pasted directly into the {"text":"..."} body, producing invalid JSON that Slack refuses.
Missing text or blocks field
A legacy incoming webhook needs a top-level text (or blocks). Sending an empty object or only unknown keys yields invalid_payload.
How to fix it
Build the JSON with a tool that escapes
- Never string-concatenate untrusted text into JSON.
- Use
jqor a scripting step to encode the message safely. - Pass the result as the request body.
msg="$(git log -1 --pretty=%s)"
payload=$(jq -n --arg t "build passed: $msg" '{text:$t}')
curl -sS -X POST -H 'Content-type: application/json' \
--data "$payload" "$SLACK_WEBHOOK_URL"Always include a text field
Ensure the payload has a non-empty text even when you also send blocks, so older webhooks accept it.
{"text":"CI result","blocks":[{"type":"section","text":{"type":"mrkdwn","text":"build passed"}}]}How to prevent it
- Encode all interpolated CI values (commit, branch, actor) with jq before sending.
- Include a fallback
textfield alongside anyblocks. - Test the notify step against a real commit message containing a quote.