Skip to content

Webhook

Webhooks allow an external server to get notified when certain events happen.

Prerequisites

  • A server reachable from the Internet (through public IP or port forwarding)
  • Ability to write (or whip your coding agent to write) server-side programs that work with our schema

Schema

All webhooks are sent as POST requests with JSON body. The body schema is as follows:

typescript
{
    imageID: number,
    text: string,
    rating: 'violent' | 'moderate' | 'none',
    tags: string[],
    imageURL: string,
    createdAt: string,      // ISO 8601
}

The invocation request is as follows:

http
POST https://example.com/webhook/endpoint
Content-Type: application/json
X-Signature: 0123456890abcdef

{
    "imageID": 2,
    ...
}

Verification

In order to prove identity and integrity, webhooks are signed with the secret registered at creation. The signature is a hexadecimal string generated by signing the request body (in raw bytes encoded with UTF-8) with the webhook secret (also encoded with UTF-8) using HMAC-256, and will be sent in the X-Signature HTTP header.

To verify the signature, use whatever cryptography library you like. For example:

py
import hashlib
import hmac
import os

from flask import Flask, request, jsonify

app = Flask()
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"].encode("utf-8")


@app.post("/hooks/longhub")
def receive_webhook():
    raw_body = request.get_data(cache=False)

    received_signature = request.headers.get("X-Signature", "")
    expected_signature = hmac.new(
        WEBHOOK_SECRET,
        raw_body,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(received_signature, expected_signature):
        return jsonify(error="invalid signature"), 401

    payload = request.get_json()
    print("Verified webhook:", payload)

    return "", 204
javascript
const express = require("express");
const crypto = require("crypto");

const app = express();

const WEBHOOK_SECRET = Buffer.from(process.env.WEBHOOK_SECRET, "utf8");

app.post("/hooks/longhub",
  express.raw({ type: "*/*" }),
  (req, res) => {
    const rawBody = req.body;

    const receivedSignature = req.get("X-Signature") || "";

    const expectedSignature = crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(rawBody)
      .digest("hex");

    const received = Buffer.from(receivedSignature, "utf8");
    const expected = Buffer.from(expectedSignature, "utf8");

    const valid =
      received.length === expected.length &&
      crypto.timingSafeEqual(received, expected);

    if (!valid) {
      return res.status(401).json({ error: "invalid signature" });
    }

    const payload = JSON.parse(rawBody.toString("utf8"));

    console.log("Verified webhook:", payload);

    return res.sendStatus(204);
  }
);

const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

Restrictions

To protect our server from being affected by malicious invocations, we have applied the following restrictions to webhooks:

  • A user can have up to 10 webhooks.
  • Webhooks are invoked via a remote Worker. You may see an IP from Cloudflare.
  • Webhooks do not follow redirects, and redirect responses (HTTP 3xx) are considered failures.
  • Webhooks that fail (HTTP 3xx, 4xx, 5xx) for 3 invocations in a row will be suspended until manual re-activation.
  • Webhook invocations have a timeout of 30s.