Skip to main content

Webhooks

Webhooks allow BandTools to notify external applications when something happens with your newsletter. When a subscribed event occurs, BandTools sends an HTTP POST request with a JSON payload to a URL you specify.

You can create up to 10 webhooks, each subscribing to any combination of the events listed below. Webhooks are available on the Headliner plan.

1.How it works

  1. Create a webhook in your Settings and provide an HTTPS endpoint URL
  2. Choose which events to subscribe to
  3. When an event occurs, BandTools sends a JSON payload to your endpoint via HTTP POST

Every request includes a Content-Type: application/json header, a User-Agent: BandTools-Webhooks/1.0 header, and an X-BandTools-Signature header containing an HMAC-SHA256 signature of the request body. You can use the signing secret (shown on the webhook edit page) to verify that payloads are genuinely from BandTools.

Top

2.Events

Event type Trigger
newsletter.scheduled A newsletter is scheduled for future delivery
newsletter.sent A newsletter is sent to subscribers
subscriber.confirmed A subscriber confirms their email address
subscriber.created A new subscriber is added via the subscribe page or embed code
subscriber.removed A subscriber is removed by the owner, an admin, or the system
subscriber.unsubscribed A subscriber unsubscribes or is unsubscribed by an admin

Top

3.Payload format

All payloads share a common envelope:

{
  "event_type": "subscriber.created",
  "occurred_at": "2026-04-12T14:30:00Z",
  "data": {
    ...
    "newsletter_name": "Cora Vale Mailing List",
    "newsletter_description": "All about me and my music. No spam!"
  }
}

Every payload includes your newsletter_name and newsletter_description (plain text, from your newsletter settings) for context.

Top

3.1 newsletter.scheduled

{
  "event_type": "newsletter.scheduled",
  "occurred_at": "2026-04-12T12:00:00Z",
  "data": {
    "newsletter": {
      "subject": "May Preview",
      "scheduled_for": "2026-05-01T09:00:00Z"
    },
    "newsletter_name": "Cora Vale Mailing List",
    "newsletter_description": "All about me and my music. No spam!"
  }
}

Top

3.2 newsletter.sent

{
  "event_type": "newsletter.sent",
  "occurred_at": "2026-04-12T18:00:00Z",
  "data": {
    "newsletter": {
      "subject": "April Update",
      "sent_at": "2026-04-12T18:00:00Z",
      "recipient_count": 542,
      "archive_url": "https://example.com/newsletters/april-update"
    },
    "newsletter_name": "Cora Vale Mailing List",
    "newsletter_description": "All about me and my music. No spam!"
  }
}

The archive_url is included only if the newsletter is published to your public archive. If you have a custom domain configured for your archive, the URL will use that domain.

Top

3.3 subscriber.confirmed

{
  "event_type": "subscriber.confirmed",
  "occurred_at": "2026-04-12T15:00:00Z",
  "data": {
    "subscriber": {
      "email_address": "fan@example.com",
      "confirmed_at": "2026-04-12T15:00:00Z"
    },
    "newsletter_name": "Cora Vale Mailing List",
    "newsletter_description": "All about me and my music. No spam!"
  }
}

Top

3.4 subscriber.created

{
  "event_type": "subscriber.created",
  "occurred_at": "2026-04-12T14:30:00Z",
  "data": {
    "subscriber": {
      "email_address": "fan@example.com",
      "created_at": "2026-04-12T14:30:00Z",
      "source": "subscribe_page"
    },
    "newsletter_name": "Cora Vale Mailing List",
    "newsletter_description": "All about me and my music. No spam!"
  }
}

The source field indicates how the subscriber was added. Possible values are subscribe_page and embed_code.

Top

3.5 subscriber.removed

{
  "event_type": "subscriber.removed",
  "occurred_at": "2026-04-12T17:00:00Z",
  "data": {
    "subscriber": {
      "email_address": "fan@example.com"
    },
    "newsletter_name": "Cora Vale Mailing List",
    "newsletter_description": "All about me and my music. No spam!"
  }
}

Top

3.6 subscriber.unsubscribed

{
  "event_type": "subscriber.unsubscribed",
  "occurred_at": "2026-04-12T16:00:00Z",
  "data": {
    "subscriber": {
      "email_address": "fan@example.com",
      "unsubscribed_at": "2026-04-12T16:00:00Z"
    },
    "newsletter_name": "Cora Vale Mailing List",
    "newsletter_description": "All about me and my music. No spam!"
  }
}

Top

4.Verifying webhook signatures

Each webhook has a unique signing secret (a 64-character hex string), shown on the webhook edit page. When BandTools delivers a payload, it computes an HMAC-SHA256 signature of the JSON request body using this secret and includes the result in the X-BandTools-Signature header.

To verify a payload, compute the same HMAC on the raw request body and compare it to the header value.

import hmac
import hashlib

expected = hmac.new(
    signing_secret.encode("utf-8"),
    request.body,
    hashlib.sha256
).hexdigest()

if hmac.compare_digest(expected, request.headers["X-BandTools-Signature"]):
    # Payload is authentic
const crypto = require("crypto");

const signature = req.headers["x-bandtools-signature"];
const expected = crypto
  .createHmac("sha256", signingSecret)
  .update(requestBody)
  .digest("hex");

if (crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
  // Payload is authentic
}
signature := r.Header.Get("X-BandTools-Signature")
mac := hmac.New(sha256.New, []byte(signingSecret))
mac.Write(requestBody)
expected := hex.EncodeToString(mac.Sum(nil))

if hmac.Equal([]byte(expected), []byte(signature)) {
    // Payload is authentic
}
expected = OpenSSL::HMAC.hexdigest("SHA256", signing_secret, request.body.read)
if Rack::Utils.secure_compare(expected, request.headers["X-BandTools-Signature"])
  # Payload is authentic
end
String signature = request.getHeader("X-BandTools-Signature");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(signingSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = HexFormat.of().formatHex(mac.doFinal(requestBody.getBytes(StandardCharsets.UTF_8)));

if (MessageDigest.isEqual(expected.getBytes(), signature.getBytes())) {
    // Payload is authentic
}
$expected = hash_hmac("sha256", $requestBody, $signingSecret);

if (hash_equals($expected, $_SERVER["HTTP_X_BANDTOOLS_SIGNATURE"])) {
    // Payload is authentic
}

Top

5.Delivery and retries

BandTools expects your endpoint to return a 2xx or 3xx HTTP status code to acknowledge receipt.

  • On transient failures (timeouts, 5xx, or 429 responses), BandTools retries up to 3 times with increasing delays
  • On permanent failure (4xx other than 429), the delivery is not retried
  • After 10 consecutive delivery failures, the webhook is automatically disabled to prevent ongoing requests to an unresponsive endpoint
  • You can re-enable a disabled webhook from the edit page, which resets the failure counter

Top