Webhooks

Webhooks let your own application receive a near-real-time notification whenever certain things happen on your Assignr site — a game gets published, an official is changed on a game, a user is added to your site, or an official is attached to a league. Instead of polling the API for changes, Assignr sends an HTTP request to a URL you control as soon as the event happens.

There's no single in-app "Webhooks" page that does everything — setting it up involves two places in the app plus one API call, described below.

If you don't see the settings described below, this feature may not be turned on for your site yet, contact Assignr support.

This feature is currently in private beta. If you wish to be considered for access, please contact Assignr support.

Before you start

You'll need:

  1. A site administrator to turn on the specific events you want to send (Step 1 below).
  2. An OAuth application with access to the webhooks scope (Step 2 below).
  3. A publicly reachable URL on your own server that can receive an HTTPS POST request and
    respond quickly (Assignr does not wait around for a slow endpoint).

Step 1: Choose which events can be sent

Before any event will actually go out, it has to be turned on for your site (and, for
league-scoped events, for the specific league).

Site-wide events:

  1. Go to Site Settings and open the Webhooks panel.
  2. In Webhooks Enabled, select the events you want to allow:
    • Game Published
    • Game Official Changed
    • User Created
  3. Save.

League-scoped events:

  1. Open the league and go to its settings.
  2. In the same Webhooks Enabled field, select from:
    • Game Published
    • Game Official Changed
    • Official Attached to a League
  3. Save.

Only the events checked here are eligible to fire at all — even if your API subscription (Step 3)
asks for an event type, Assignr won't send it unless it's also enabled here.

Step 2: Get API access with the webhooks scope

Webhook subscriptions are created and managed through the Assignr API, so you need an OAuth
application first. OAuth Applications are created by the Assignr support team, you'll need to get in touch to set one up.

Step 3: Create a subscription

There's no in-app form for the actual subscription — you create it with an API call, using the
access token from Step 2.

POST https://api.assignr.com/api/v2/webhooks/subscriptions
FieldRequiredDescription
site_idYesThe site this subscription belongs to.
event_typesYesOne or more event type strings (see Event reference).
target_urlYesThe URL Assignr will POST to when a matching event fires.
target_methodYesThe HTTP method to use — use POST.
target_headersNoAn object of custom headers to include on every delivery (e.g. an API key your endpoint expects).
enabledNoDefaults to true. Set to false to pause deliveries without deleting the subscription.

Example request body:

{
  "site_id": 12345,
  "event_types": ["game.game.published", "game.official.changed"],
  "target_url": "https://example.com/webhooks/assignr",
  "target_method": "POST",
  "target_headers": { "X-My-App-Key": "abc123" }
}

A successful response (201) returns the subscription, including a generated secret you'll use
to verify deliveries:

{
  "id": 987,
  "site_id": 12345,
  "event_types": ["game.game.published", "game.official.changed"],
  "enabled": true,
  "secret": "bd957e7b-c79c-496b-8ba7-d023c31be30e",
  "target_method": "POST",
  "target_url": "https://example.com/webhooks/assignr",
  "target_headers": { "X-My-App-Key": "abc123" },
  "created": "2026-08-24T12:00:00Z"
}

Save the secret when you create the subscription — there's no separate screen to look it up
later
other than fetching the subscription again via the API (see below).

Managing an existing subscription

All of these also require the webhooks scope:

ActionRequest
List your subscriptionsGET /api/v2/webhooks/subscriptions
View one subscriptionGET /api/v2/webhooks/subscriptions/:id
Update a subscriptionPUT /api/v2/webhooks/subscriptions/:id (send only the fields you want to change)
Delete a subscriptionDELETE /api/v2/webhooks/subscriptions/:id

Pause deliveries temporarily with PUT .../subscriptions/:id and {"enabled": false}, rather than
deleting and recreating the subscription.

What you'll receive

Each delivery is a small JSON "pointer" — it tells you what happened and where to look, rather
than including the full record. Fetch the linked resource (with your own API access token) to get
full details.

Example delivery for game.game.published:

{
  "id": 55501,
  "topic": "game.game.published",
  "_links": {
    "self": { "resource-type": "webhook-event", "href": "https://api.assignr.com/api/v2/webhooks/events/55501.json" },
    "resource": { "resource-type": "game", "href": "https://api.assignr.com/api/v2/games/778899.json" },
    "owner": { "resource-type": "site", "href": "https://api.assignr.com/api/v2/sites/12345.json" },
    "game": { "resource-type": "game", "href": "https://api.assignr.com/api/v2/games/778899.json" }
  }
}

For game.official.changed, the resource link points at the affected assignment, and a
separate game link is included so you don't have to look up the assignment first to find its
game.

Verifying a delivery is genuinely from Assignr

Each delivery includes a signature header so you can confirm it wasn't spoofed:

X-Hook0-Signature: t=<unix_timestamp>,h=x-event-id x-event-type,v1=<hmac_sha256_hex>

To verify:

  1. Build the signed string: {timestamp}.x-event-id x-event-type.{event id}.{event topic}.{raw request body}.
  2. Compute an HMAC-SHA256 of that string, keyed with your subscription's secret.
  3. Compare it to the v1 value in the header (constant-time comparison recommended).
  4. Reject requests whose timestamp is too old, to guard against replay.

The request also includes X-Event-Id, X-Event-Type, Content-Type: application/json, and any
custom target_headers you configured on the subscription.

Sample Verification Code

require "openssl"
require "json"

module Assignr
  class WebhookVerificationError < StandardError; end

  module_function

  # raw_body:         the exact, unparsed request body (do not re-serialize parsed JSON)
  # signature_header:  the value of the X-Hook0-Signature header
  # secret:            the subscription's secret, from the API response when you created it
  def verify_webhook!(raw_body:, signature_header:, secret:, tolerance: 300)
    fields = signature_header.to_s.split(",").each_with_object({}) do |part, memo|
      key, value = part.split("=", 2)
      memo[key] = value
    end
    timestamp, header_list, signature = fields.values_at("t", "h", "v1")

    if timestamp.nil? || header_list.nil? || signature.nil?
      raise WebhookVerificationError, "Malformed signature header"
    end

    if (Time.now.to_i - timestamp.to_i).abs > tolerance
      raise WebhookVerificationError, "Timestamp outside of tolerance"
    end

    event = JSON.parse(raw_body)
    signed_payload = [timestamp, header_list, event["id"], event["topic"], raw_body].join(".")
    expected_signature = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)

    unless secure_compare(expected_signature, signature)
      raise WebhookVerificationError, "Signature mismatch"
    end

    event
  end

  def secure_compare(a, b)
    a.bytesize == b.bytesize && OpenSSL.fixed_length_secure_compare(a, b)
  end
end
import hashlib
import hmac
import json
import time


class WebhookVerificationError(Exception):
    pass


def verify_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> dict:
    """
    raw_body:          the exact, unparsed request body bytes (don't re-serialize parsed JSON)
    signature_header:  the value of the X-Hook0-Signature header
    secret:            the subscription's secret, from the API response when you created it
    """
    fields = dict(part.split("=", 1) for part in signature_header.split(","))
    timestamp = fields.get("t")
    header_list = fields.get("h")
    signature = fields.get("v1")

    if not timestamp or not header_list or not signature:
        raise WebhookVerificationError("Malformed signature header")

    if abs(int(time.time()) - int(timestamp)) > tolerance:
        raise WebhookVerificationError("Timestamp outside of tolerance")

    event = json.loads(raw_body)
    signed_payload = ".".join([
        timestamp,
        header_list,
        str(event["id"]),
        event["topic"],
        raw_body.decode("utf-8"),
    ])
    expected_signature = hmac.new(
        secret.encode("utf-8"), signed_payload.encode("utf-8"), hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected_signature, signature):
        raise WebhookVerificationError("Signature mismatch")

    return event

Event reference

Event typeLabel in Site/League settingsFires when
game.game.publishedGame PublishedA game is published, if the game is public.
game.official.changedGame Official ChangedAn official is added, removed, or replaced on a public game that hasn't happened yet.
site.user.createdUser CreatedA user is added to your site. Site-level only.
site.league_official.createdOfficial Attached to a LeagueAn official is attached to a league. League-level only.

Limitations

  • There's currently no delivery log in the app — no list of past deliveries, response codes, or a
    redelivery button. If you suspect deliveries are failing, double-check your endpoint is
    reachable and returning a fast response, or contact Assignr support.
  • target_method isn't restricted in the API to a specific list of values, but POST is the
    supported and expected method — other values aren't guaranteed to work.