SocialCrawl

Billing Webhooks

Configurable low-credit alerts and signed outbound webhooks for billing events (credits low, exhausted, payment succeeded, auto-recharge)

Billing Webhooks

Silent 402s in production are how you lose an account overnight. SocialCrawl warns you before your app breaks: a configurable low-credit email alert, plus signed outbound webhooks so your own infrastructure can machine-react to a draining balance (top up, page on-call, flip a feature flag).

Configure both on your dashboard under Billing → Payments.

Low-credit alerts

By default we email you the moment your balance crosses 20% of your most recently purchased pack. That scales with you: a 2,500-credit pack alerts at 500, a 150,000-credit pack alerts at 30,000. (A hard-coded threshold like "10 credits left" is meaningless when a single request can cost more than that.)

You can override this per account:

  • Absolute threshold: set an exact number (e.g. alert me at 5,000).
  • Disable: turn the email off entirely, or set the threshold to 0.

The alert fires once per crossing: on the single call that takes you from above the threshold to at or below it, not on every subsequent call. A 7-day de-duplication window is the backstop.

Webhook events

When you configure a webhook URL, SocialCrawl POSTs a signed JSON body for these events:

EventFires when
credits.lowYour balance crosses the low-credit threshold (see above).
credits.exhaustedYour balance reaches zero.
payment.succeededA one-time credit pack purchase is fulfilled.
auto_recharge.succeededAn automatic top-up added credits.
auto_recharge.failedAn automatic top-up failed (card declined, auth required).

Payload shape

{
  "event": "credits.low",
  "created": 1700000000,
  "data": {
    "balance": 480,
    "threshold": 500
  }
}

created is a Unix timestamp (seconds). The data object varies per event:

// payment.succeeded / auto_recharge.succeeded
{ "credits_added": 2500, "new_balance": 2600, "amount_gbp": "£15", "plan": "starter" }

// credits.exhausted
{ "balance": 0 }

// auto_recharge.failed
{ "reason": "card_declined", "action_required": false }

How we sign and retry

SocialCrawl signs every webhook delivery. Billing webhooks and Monitor webhooks share the exact same signing scheme, so one verification routine covers both.

Signature

Each delivery carries an x-socialcrawl-signature header, Stripe-style:

x-socialcrawl-signature: t=1700000000,v1=<hex>

The signature is HMAC-SHA256(secret, "<t>.<rawBody>"), where <t> is the Unix timestamp and <rawBody> is the exact bytes of the request body. Fold t into your check to enforce a replay window. Your signing secret (a whsec_... value) is shown once, when you create or replace the webhook. Store it immediately; we keep only an encrypted copy and cannot show it again.

Verifying a signature (Node.js)

import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=")),
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(parts.v1, "hex"),
    Buffer.from(expected, "hex"),
  );
}

Verifying a signature (Python)

import hashlib, hmac

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(kv.split("=", 1) for kv in header.split(","))
    expected = hmac.new(
        secret.encode(),
        f"{parts['t']}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(parts["v1"], expected)

Always verify against the raw request body. Parsing and re-serializing the JSON will change the bytes and break the signature.

Retries and delivery

Deliveries go out through a retrying queue (up to 5 attempts with exponential backoff). Respond 2xx promptly to acknowledge. An endpoint that fails repeatedly is automatically paused; re-save the URL on your dashboard to reactivate it. Requirements:

  • The URL must be HTTPS on a public host (loopback, private, and link-local addresses are rejected).
  • Handlers should be idempotent: treat retries and occasional duplicates as normal, and de-duplicate on (event, created) if you need exactly-once semantics.
Billing Webhooks | SocialCrawl