The missing Webhooks documentation for Fizzy

This the documentation I used to build bubblehook, a Fizzy webhook receiver that sends updates to Slack. I hope this is useful. The post is written by AI, with a lot of rough notes fed to it from my reading the code, and observing the webhooks. Enjoy!

Fizzy doesn’t currently publish full webhook documentation, so I’ve put together a complete reference for anyone building integrations, automations, or notifications around Fizzy boards. This guide covers every event Fizzy emits, how the webhook delivery works, how signatures are generated, and what the payloads look like in JSON, Slack, Campfire, and Basecamp formats.

If you’re implementing a webhook receiver for Fizzy – this is the spec.


Supported Webhook Actions

Webhooks can subscribe to a specific set of event types. Fizzy sends a separate request for each event that occurs on the board the webhook belongs to.

Supported actions:

  • card_published — card created or published (drafts only fire when published)
  • card_assigned / card_unassigned — a user added to or removed from a card
  • card_closed / card_reopened — moved to or from “Done”
  • card_postponed / card_auto_postponed — moved to “Not Now”, manually or by entropy
  • card_triaged — placed into a column after triage
  • card_sent_back_to_triage — returned to “Maybe?”
  • card_board_changed — moved to a different board
  • comment_created — non-system user posts a comment

Only events from the webhook’s own board are delivered. If a webhook becomes inactive, Fizzy skips it.


Request Envelope & Delivery Behavior

Every webhook delivery is a POST request with predictable headers and a signing mechanism.

HTTP Method

POST to your configured webhook URL.

Headers

  • User-Agent: fizzy/1.0.0 Webhook
  • Content-Type: varies by endpoint type (JSON / Slack / Campfire / HTML)
  • X-Webhook-Signature: hex HMAC-SHA256 of the raw request body
  • X-Webhook-Timestamp: UTC timestamp when the event was created

Security & Timeouts

  • DNS timeout: 2 seconds
  • Connect/read timeout: 7 seconds
  • Requests to private/loopback/link-local/broadcast addresses are blocked

Delivery Success Criteria

A delivery counts as successful only when your endpoint returns a 2xx HTTP status.

Failures are logged. If a webhook accumulates 10 consecutive failures within one hour, Fizzy automatically deactivates it to prevent endless retries.


Verifying Signatures

Fizzy signs the exact raw request body using the webhook’s signing_secret.
Example verification in Ruby:

payload = request.raw_post
timestamp = request.headers["X-Webhook-Timestamp"]
signature = request.headers["X-Webhook-Signature"]

expected = OpenSSL::HMAC.hexdigest("SHA256", signing_secret, payload)
ActiveSupport::SecurityUtils.secure_compare(signature, expected)

Payload Formats

Fizzy supports four output formats, depending on the webhook URL pattern.


1) Generic JSON (Default)

This is used for any non-Slack, non-Campfire/Basecamp URL.

Content-Type: application/json

Example (card_published):





{
  "id": "evt_123",
  "action": "card_published",
  "created_at": "2024-05-01T15:04:05Z",
  "eventable": {
    "id": "card_456",
    "title": "Logo refresh",
    "status": "published",
    "image_url": null,
    "golden": false,
    "last_active_at": "2024-05-01T15:04:05Z",
    "created_at": "2024-04-29T10:00:00Z",
    "url": "https://fizzy.localhost:3006/1234567/cards/42",
    "board": {
      "id": "brd_1",
      "name": "Brand",
      "all_access": true,
      "created_at": "2024-04-01T12:00:00Z",
      "creator": {
        "id": "usr_1",
        "name": "David",
        "role": "owner",
        "active": true,
        "email_address": "[email protected]",
        "created_at": "2024-03-01T12:00:00Z",
        "url": "https://fizzy.localhost:3006/1234567/users/usr_1"
      }
    },
    "column": null,
    "creator": {
      "id": "usr_1",
      "name": "David",
      "role": "owner",
      "active": true,
      "email_address": "[email protected]",
      "created_at": "2024-03-01T12:00:00Z",
      "url": "https://fizzy.localhost:3006/1234567/users/usr_1"
    }
  },
  "board": { "...same as above..." },
  "creator": { "...event initiator..." }
}

Card Fields

Match Fizzy’s internal card serializer:

  • id, title, status (drafted or published)
  • image_url (nullable)
  • golden boolean flag
  • last_active_at, created_at
  • url
  • board (nested summary)
  • column (or null)
  • creator (user summary)

Comment Fields

When action: "comment_created", the eventable object contains:

  • id
  • timestamps
  • body.plain_text and body.html
  • creator
  • reactions_url
  • url

Notes

  • Specific “particulars” (e.g., which user was assigned/unassigned) are not included — clients should re-fetch the card for detailed context.
  • URLs include the account slug to support multi-tenancy.
  • The column information is added only when there is something happening which is based on column (like changing the column).

2) Slack-Compatible Format

Triggered when the webhook URL matches:

https://hooks.slack.com/services/...

Content-Type: application/json

Body:

{ "text": "<mrkdwn text>" }

Conversion rules:

  • HTML → Slack mrkdwn
  • <a href="url">text</a><url|text>
  • <b>*bold*
  • <i>_italic_
  • All other HTML stripped

Example output:

David added Logo refresh ↗︎ https://fizzy.localhost:3006/1234567/cards/42

3) Basecamp Campfire (Chat Line)

Recognized when the URL matches:

/\d+/integrations/[^/]+/buckets/\d+/chats/\d+/lines

Content-Type: application/x-www-form-urlencoded

Body:

content=<html_snippet>

The snippet is the same HTML described below, URL-encoded.


4) Campfire-Compatible HTML

Matched when the URL resembles:

/rooms/\d+/\d+-[^/]+/messages

Content-Type: text/html

Body: raw HTML snippet.


HTML Template (Shared by Slack / Basecamp / Campfire)

Rendered from app/views/webhooks/event.html.erb:

<plain sentence about the event>
<a href="https://...">↗︎</a>

Rules:

  • Sentences come from Event::Description
  • Personalized when Fizzy knows the acting user (“You added…”)
  • Comments use “{creator} commented on {card title}”
  • Link points to the card or comment URL

Delivery Flow Overview

Inside Fizzy:

  1. Cards and comments emit events through the Eventable concern
  2. Event::WebhookDispatchJob identifies relevant active webhooks
  3. Each webhook delivery is enqueued as a Webhook::Delivery
  4. Webhook::DeliveryJob sends the request, records metadata, and handles success/failure
  5. Excessive failures deactivate the webhook automatically

This keeps webhook delivery reliable and auditable without blocking the main application.


Final Notes

This post should serve as a complete, implementable reference until official documentation appears. If you’re building integrations – Slack apps, internal automations, reporting scripts, or custom activity feeds, this covers everything Fizzy emits today.