CTFFactory Docs

Webhooks

CTFFactory webhooks let your systems receive real-time notifications when key lifecycle events occur on the platform. Instead of polling the API, you register an endpoint URL and CTFFactory sends a signed JSON POST to it whenever a subscribed event fires.


Registering a Webhook Endpoint

From the dashboard:

  1. Open Webhooks from the main navigation.
  2. Under Register a webhook, enter your endpoint URL (must start with http:// or https://; use HTTPS in production).
  3. Tick the events you want to receive. Leave every box unchecked to receive all events.
  4. Click Register webhook. CTFFactory generates a unique signing secret and shows it once β€” copy it immediately and store it securely. It is used to verify the signature on every delivery.

Registered endpoints appear in the Registered webhooks table, where each row can be deleted.

You can also manage endpoints from the REST API. Unlike the dashboard, the API expects you to supply the signing secret (16–128 characters):

POST /api/v1/webhooks
Authorization: Bearer ctff_...
Content-Type: application/json

{
  "url": "https://your-app.example.com/hooks/ctffactory",
  "events": ["ctf.deployed", "challenge.published"],
  "secret": "whsec_your_own_shared_secret_value"
}
  • events may be an empty list ([]) to subscribe to every event.
  • Registering requires the ctf:admin scope; listing requires ctf:read.

List and delete endpoints:

GET    /api/v1/webhooks             # returns registered webhooks + available_events
DELETE /api/v1/webhooks/{id}        # requires ctf:admin

Event Types

Event Trigger
ctf.deployed A managed CTFd instance finished deploying and is reachable
ctf.archived A managed CTF's final competition snapshot was archived
ctf.deleted A managed CTF and its infrastructure were deleted
challenge.published A challenge was published to CTFd
challenge.unpublished A challenge was removed from CTFd
deployment.deployed A standalone single-challenge deployment went live
deployment.stopped A single-challenge deployment was stopped (its window elapsed)
member.invited A competitor was invited to a CTF (or re-invited / self-enrolled)
member.joined An invited competitor accepted their invitation and joined

The authoritative list is always returned in the available_events field of GET /api/v1/webhooks. Subscribe only to the events your integration needs.


Payload Structure

Each delivery is a JSON POST to your endpoint. Every event shares the same envelope:

{
  "event": "ctf.deployed",
  "delivered_at": 1768137120,
  "data": { }
}
  • event β€” the event name.
  • delivered_at β€” Unix timestamp (seconds) when the delivery was built.
  • data β€” event-specific payload.

The body is serialized compactly with sorted keys, so the exact bytes are stable for signature verification.

The following headers accompany every delivery:

Header Value
Content-Type application/json
X-CTFFactory-Event the event name (e.g. ctf.deployed)
X-CTFFactory-Signature sha256=<hex_digest> (see below)
User-Agent CTFFactory-Webhook/1

Example data payloads

ctf.deployed

{
  "data": {
    "managed_ctf_id": "mctf_01JXABC456",
    "title": "Spring Boot Camp CTF",
    "url": "https://ctf.yourcompany.com/",
    "is_public": true
  }
}

challenge.published

{
  "data": {
    "managed_ctf_id": "mctf_01JXABC456",
    "challenge_id": "mchal_01JXABC999",
    "title": "Token Forge",
    "category": "web",
    "ctfd_challenge_id": 42
  }
}

Verifying the HMAC-SHA256 Signature

Every delivery includes an X-CTFFactory-Signature header β€” an HMAC-SHA256 digest of the raw request body, keyed with your endpoint's signing secret. Always verify it before processing the payload.

X-CTFFactory-Signature: sha256=<hex_digest>

Python Verification Example

import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)

WEBHOOK_SECRET = "your_signing_secret_here"  # shown once when you registered the endpoint

@app.route("/hooks/ctffactory", methods=["POST"])
def handle_webhook():
    signature_header = request.headers.get("X-CTFFactory-Signature", "")
    if not signature_header.startswith("sha256="):
        abort(400, "Missing or malformed signature header")

    received_sig = signature_header[len("sha256="):]

    # Compute the expected signature over the RAW body bytes.
    expected_sig = hmac.new(
        key=WEBHOOK_SECRET.encode("utf-8"),
        msg=request.get_data(),  # raw bytes, not re-serialized JSON
        digestmod=hashlib.sha256,
    ).hexdigest()

    # Constant-time comparison to prevent timing attacks.
    if not hmac.compare_digest(expected_sig, received_sig):
        abort(401, "Signature verification failed")

    event = request.json
    if event["event"] == "ctf.deployed":
        handle_ctf_deployed(event["data"])

    return "", 200  # acknowledge receipt

def handle_ctf_deployed(data):
    print(f"CTF deployed: {data['title']} at {data['url']}")

Important: Always compute the HMAC over the raw request body bytes, not over a re-serialized version of the parsed JSON β€” re-serialization can change the bytes and break verification.


Delivery and Retries

CTFFactory expects a 2xx HTTP status within 8 seconds to acknowledge a delivery. If the endpoint times out or returns a non-2xx response, the delivery is retried up to 2 times (3 attempts total) with a short linear backoff before being dropped.

Deliveries are dispatched in the background: a failing endpoint never blocks or breaks the lifecycle action that triggered the event. Make your handler idempotent β€” under retries the same event may be delivered more than once.

πŸ‡¨πŸ‡¦ Data hosted in Canada Β· Β© 2026 ExamBoot Β· Terms Β· Privacy