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.
This feature is aimed at developers integrating with Assignr's API. There's no in-app "Webhooks"
page or setting to turn on — creating a subscription through the API is itself what opts your site
in. As soon as you have an active subscription listing an event type, that event starts being sent;
deleting the subscription (or disabling it, or removing the event type) stops it. Optionally, a
subscription can also include criteria to narrow things down further — see
Scoping deliveries with criteria below.
Before you start
You'll need:
- An OAuth application with access to the
webhooksscope (Step 1 below). - 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: Get API access with the webhooks scope
Webhook subscriptions are created and managed through the Assignr API, so you need an OAuth
application first. If you don't have one, please contact Assignr Support. You will need to ask for the webhooks scope in order to access this functionality.
Your OAuth Application will have a client ID/secret — you'll use these to obtain an access token
the same way as any other Assignr API integration. Request the webhooks scope explicitly when
you authorize or request a token; it isn't granted by default.
Step 2: 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 1. Creating it is what turns the events on: as soon as this subscription
exists and is enabled, matching events start being sent to target_url.
POST https://api.assignr.com/api/v2/webhooks/subscriptions
| Field | Required | Description |
|---|---|---|
site_id | Yes | The site this subscription belongs to. |
event_types | Yes | One or more event type strings (see Event reference). |
target_url | Yes | The URL Assignr will POST to when a matching event fires. |
target_method | Yes | The HTTP method to use — use POST. |
target_headers | No | An object of custom headers to include on every delivery (e.g. an API key your endpoint expects). |
enabled | No | Defaults to true. Set to false to pause deliveries without deleting the subscription. |
criteria | No | An object that narrows which occurrences of an event type get sent (e.g. only games in certain leagues). See Scoping deliveries with criteria. |
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" },
"criteria": { "league_id": [111, 222] }
}
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" },
"criteria": { "league_id": [111, 222] },
"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:
| Action | Request |
|---|---|
| List your subscriptions | GET /api/v2/webhooks/subscriptions |
| View one subscription | GET /api/v2/webhooks/subscriptions/:id |
| Update a subscription | PUT /api/v2/webhooks/subscriptions/:id (send only the fields you want to change) |
| Delete a subscription | DELETE /api/v2/webhooks/subscriptions/:id |
Pause deliveries temporarily with PUT .../subscriptions/:id and {"enabled": false}, rather than
deleting and recreating the subscription. The same call can add, change, or clear criteria.
Scoping deliveries with criteria
By default, a subscription's event_types gets every occurrence of that event for the site. The
optional criteria object lets you narrow that down, and what it accepts depends on the event type:
game.game.publishedandgame.official.changed—criteriacan include any of the game
filter fields you'd use to search games, such asleague_id,venue_id,game_type_id,
age_group_id,gender_id,home_team_id, oraway_team_id. Each can be a single value or an
array of values. Only games matching the filter trigger a delivery; a subscription with no
criteria (or an empty object) receives all games for the site.site.league_official.created— the only key that matters isleague_id(a single value or
array). Set it to only be notified when an official is added for the first time to specific
leagues. Without it, you're notified for every league on the site.site.user.created—criteriaisn't applicable to this event; any value you set here is
ignored for it.
A single subscription can list several event_types while sharing one criteria object — each
event type only looks at the keys that are meaningful to it and ignores the rest. Unrecognized keys
are silently ignored rather than rejected, so double-check spelling if a criteria filter doesn't
seem to be working as expected.
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:
- Build the signed string:
{timestamp}.x-event-id x-event-type.{event id}.{event topic}.{raw request body}. - Compute an HMAC-SHA256 of that string, keyed with your subscription's
secret. - Compare it to the
v1value in the header (constant-time comparison recommended). - 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 (Ruby)
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
Example use in a Rails controller:
class Webhooks::AssignrController < ActionController::API
def create
event = Assignr.verify_webhook!(
raw_body: request.raw_post,
signature_header: request.headers["X-Hook0-Signature"],
secret: Rails.application.credentials.assignr_webhook_secret
)
case event["topic"]
when "game.game.published"
GamePublishedJob.perform_later(event)
when "game.official.changed"
GameOfficialChangedJob.perform_later(event)
end
head :ok
rescue Assignr::WebhookVerificationError
head :unauthorized
end
end
A few things worth keeping in mind:
- Verify against the raw, unparsed request body — re-serializing parsed JSON can change
whitespace/key order and break the signature check. OpenSSL.fixed_length_secure_compareraises if the two strings differ in length, so check
bytesizefirst (as above) rather than letting a mismatched signature raise instead of just
failing verification.- Reject old timestamps (the
toleranceabove) to guard against replayed requests.
Sample verification code (Python)
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
Example use in a Flask endpoint:
from flask import Flask, request, abort
app = Flask(__name__)
ASSIGNR_WEBHOOK_SECRET = "bd957e7b-c79c-496b-8ba7-d023c31be30e" # from your app's config/secrets
@app.route("/webhooks/assignr", methods=["POST"])
def assignr_webhook():
try:
event = verify_webhook(
raw_body=request.get_data(),
signature_header=request.headers.get("X-Hook0-Signature", ""),
secret=ASSIGNR_WEBHOOK_SECRET,
)
except WebhookVerificationError:
abort(401)
if event["topic"] == "game.game.published":
handle_game_published(event)
elif event["topic"] == "game.official.changed":
handle_game_official_changed(event)
return "", 200
hmac.compare_digest is already constant-time, so there's no separate secure_compare helper
needed here.
Event reference
| Event type | Fires when | Criteria support |
|---|---|---|
game.game.published | A game is published, if the game is public. | Game filter fields (e.g. league_id, venue_id) |
game.official.changed | An official is added, removed, or replaced on a public game that hasn't happened yet. | Game filter fields (e.g. league_id, venue_id) |
site.user.created | A user is added to your site. | Not applicable |
site.league_official.created | An official is attached to a league on your site for the first time. | league_id 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_methodisn'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.
Frequently asked questions
I created a subscription but I'm not receiving an event.
Check that: the subscription's event_types includes that event type, enabled is true on the
subscription, and — if you set criteria — the specific game, league, or user actually matches it
(see Scoping deliveries with criteria).
Can I subscribe to events for more than one site with one subscription?
No — a subscription is tied to a single site_id. Create one subscription per site.
I lost my subscription's secret. Can I get it back?
Yes — fetch the subscription with GET /api/v2/webhooks/subscriptions/:id; the secret is returned
in the response.
