Skip to content

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.

Each delivery is a single HTTP request:

AspectValue
MethodPOST
BodyJSON array of transformed messages (one batch can hold multiple items)
Timeout10 seconds from the sender's perspective
SuccessStatus 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).

Every POST includes:

HeaderMeaning
X-Signature-Expiration-TimestampUnix time; signatures older than about 5 minutes should be rejected on your side too
X-Signature-SHA256HMAC-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 before JSON.parse. Do not re-serialize a parsed object - whitespace, key order, and number formatting must match what was signed.
  • {expirationTimestamp} is the header value of X-Signature-Expiration-Timestamp as a decimal Unix timestamp in seconds (same characters as in the header, no extra padding).

Verify in this order:

  1. 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.
  2. HMAC - expected = HMAC-SHA256(secret, raw_body + "." + expiration_timestamp) as lowercase hex. Compare to X-Signature-SHA256 with a constant-time equality check (for example crypto.timingSafeEqual in Node or hmac.compare_digest in Python). Do not use == on strings.
  3. Parse JSON - only after steps 1-2 succeed.
  4. Idempotency - each object in the batch should carry a stable message identifier (commonly id or uuid in 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_value
expected_hex = HMAC-SHA256(secret, signing_input) # lowercase hex
constant_time_equal(expected_hex, X-Signature-SHA256)

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).

Terminal window
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
}'
FieldTypeRules
urlstringHTTPS only, valid URL, max 1024 characters
secretstringExactly 32 characters (shared HMAC secret)
clientIdnumberPositive integer (your Sports API client_id)
RequirementValue
Status code200
Response bodyExactly Accepted

Verify the signature on the incoming test POST the same way you will in production.

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

messageMeaning
Webhook request has timed outYour endpoint did not answer within 10 seconds
Webhook request has failedNetwork or connection error reaching your URL
Webhook request resulted non 200 statusNon-200 HTTP status from your server
Couldn't read webhook resource response bodyResponse 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"
}
GoalPage
Poll live scores over REST while you buildBuild a livescore board
Diff-based REST sync vs live pushIncremental sync