Webhooks
For production live data, polling GET /v2/livescore in a loop is the wrong default once your
contract includes push. Webhooks deliver batched JSON to a URL you register: events and incidents
for competitions you are entitled to, in a shape meant for client integrations (not the raw internal
feed).
The sections below are the delivery contract your handler must implement, plus an API to probe that URL before you turn live traffic on.
1. What your endpoint must accept
Section titled “1. What your endpoint must accept”Each delivery is a single HTTP request:
| Aspect | Value |
|---|---|
| Method | POST |
| Body | JSON array of transformed messages (one batch can hold multiple items) |
| Timeout | 10 seconds from the sender's perspective |
| Success | Status 200 and response body exactly Accepted (plain text, case-sensitive) |
Anything else is treated as a failed delivery (retries and monitoring are on the StatsCore side - implement the success contract first).
Signature headers
Section titled “Signature headers”Every POST includes:
| Header | Meaning |
|---|---|
X-Signature-Expiration-Timestamp | Unix time; signatures older than about 5 minutes should be rejected on your side too |
X-Signature-SHA256 | HMAC-SHA256 hex digest of the signing string below |
The signed string is {payload}.{expirationTimestamp} where:
{payload}is the exact raw request body as UTF-8 bytes on the wire (the JSON array string). Read it from the socket beforeJSON.parse. Do not re-serialize a parsed object - whitespace, key order, and number formatting must match what was signed.{expirationTimestamp}is the header value ofX-Signature-Expiration-Timestampas a decimal Unix timestamp in seconds (same characters as in the header, no extra padding).
Verify in this order:
- Expiration - reject if the header is missing, not a positive integer, or
now > expiration(allow a small clock skew if you need it). Signature expiry is about 5 minutes; older deliveries must not be accepted even when the HMAC is valid. - HMAC -
expected = HMAC-SHA256(secret, raw_body + "." + expiration_timestamp)as lowercase hex. Compare toX-Signature-SHA256with a constant-time equality check (for examplecrypto.timingSafeEqualin Node orhmac.compare_digestin Python). Do not use==on strings. - Parse JSON - only after steps 1-2 succeed.
- Idempotency - each object in the batch should carry a stable message identifier (commonly
idoruuidin the payload). Persist processed identifiers and make handlers idempotent: replays inside the expiry window are possible, and expiry alone is not replay protection. Apply side effects at most once per identifier across the whole batch.
signing_input = raw_utf8_body + "." + expiration_header_valueexpected_hex = HMAC-SHA256(secret, signing_input) # lowercase hexconstant_time_equal(expected_hex, X-Signature-SHA256)2. Test your URL before going live
Section titled “2. Test your URL before going live”Call the test webhook URL endpoint with the HTTPS URL and secret you plan to use in production.
The service sends one signed POST with an empty array [] as the body - same headers and signing
as real traffic, no live messages.
Replace YOUR_LIVESCORE_HOST with the hostname StatsCore gave you for the Livescore webhook service
(staging and production differ).
curl -sS -X POST "https://YOUR_LIVESCORE_HOST/test-webhook-url/" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-service.example.com/webhooks/livescore", "secret": "OpJ2i39nru98xwKzqoKid78dxufnhoJ6", "clientId": 1 }'Request body
Section titled “Request body”| Field | Type | Rules |
|---|---|---|
url | string | HTTPS only, valid URL, max 1024 characters |
secret | string | Exactly 32 characters (shared HMAC secret) |
clientId | number | Positive integer (your Sports API client_id) |
What the test checks on your server
Section titled “What the test checks on your server”| Requirement | Value |
|---|---|
| Status code | 200 |
| Response body | Exactly Accepted |
Verify the signature on the incoming test POST the same way you will in production.
Responses from the test API
Section titled “Responses from the test API”Success - 200 Ok
{ "status": "Ok", "message": "Webhook url has been successfully tested"}Validation error - 400 Bad Request
{ "status": "ValidationFailed", "message": "Validation failed", "details": { "url": ["Only secure URLs are allowed"], "secret": ["Secret must be 32 characters long"], "clientId": ["Client ID must be greater than 0"] }}details lists only fields that failed.
Webhook could not be verified - 400 Bad Request
message | Meaning |
|---|---|
Webhook request has timed out | Your endpoint did not answer within 10 seconds |
Webhook request has failed | Network or connection error reaching your URL |
Webhook request resulted non 200 status | Non-200 HTTP status from your server |
Couldn't read webhook resource response body | Response body could not be read |
Webhook resource responded with different text than "Accepted" | Body was not exactly Accepted |
{ "status": "WebhookRequestError", "message": "Webhook request has timed out"}3. Related docs
Section titled “3. Related docs”| Goal | Page |
|---|---|
| Poll live scores over REST while you build | Build a livescore board |
| Diff-based REST sync vs live push | Incremental sync |