# Guide to Live Tennis API Webhooks: Features and Best Practices

Live Tennis API webhooks push live tennis data to your servers as it changes: every accepted score update, every played point, and every break point as it arises and resolves. If you build scoreboards, betting tools or broadcast graphics on Live Tennis API, webhooks are an alternative to holding a WebSocket open or polling the scores endpoints.

This guide covers how Live Tennis API webhooks are registered and signed, the frames they carry, the delivery rules that make them unusual, and the practices that keep a receiver from missing points.

## What are Live Tennis API webhooks?

A Live Tennis API webhook is an HTTPS URL registered through the API. It receives the same frames as the push feed, one `POST` per frame. Webhooks are an ULTRA plan feature and need a direct API key: marketplace keys can't register them.

Each delivery has three headers. `X-LTAPI-Signature` holds `sha256=` and the hex HMAC-SHA256 of the raw body, keyed with the webhook's secret. `X-LTAPI-Event` names the frame type. `X-LTAPI-Timestamp` is the send time in Unix seconds, and it is not part of the signature.

What sets these webhooks apart is the delivery model. Live Tennis API makes one attempt per frame, with a timeout of about three seconds, and never retries or replays. Score frames recover on their own because each one is the full current score. Point frames don't, so a receiver needs a catch-up path.

## Live Tennis API webhook features

| Feature | Details |
| --- | --- |
| Configuration | API only: `POST`, `GET` and `DELETE` on `/api/public/v1/webhooks` |
| Requirements | ULTRA plan and a direct (not marketplace) API key |
| Limit | 3 webhooks per key |
| Frame type | The `X-LTAPI-Event` header |
| Signature header | `X-LTAPI-Signature: sha256=<hex>` |
| Scheme | HMAC-SHA256, hex digest |
| Signed content | The raw request body only |
| Secret | One per webhook, shown only in the `201` registration response |
| Timestamp | `X-LTAPI-Timestamp`, Unix seconds. Not signed |
| Delivery | At-most-once: one attempt per frame, no retries, no replay |
| Timeout | About 3 seconds. Redirects are not followed |
| Auto-disable | After 25 consecutive failures |
| Source IPs | Not published |

## Common events

`X-LTAPI-Event` carries one of four frame types:

| Frame | Sent when | Missed delivery |
| --- | --- | --- |
| `score` | A live score commit is accepted. The frame is the complete current score | Self-corrects on the next commit |
| `point` | A point is played. The frame carries one point, with a per-match `seq` | Lost unless you fetch it again |
| `break_point` | A break point arises, with `server`, `returner` and `break_points` | Lost |
| `break_point_result` | A break point resolves, with `outcome` of `held` or `broken` | Lost |

The registration body's `events` array decides what a webhook receives. It defaults to `["score"]`. The API spec lists `score`, `break_point`, `point`, `status` and `stoppages` as accepted values, where `break_point` covers both break point frames and `point` is sent only where the point surface is enabled and the match has `pbp_coverage: "point"`. The webhook documentation describes only the four frame types above, so log the `X-LTAPI-Event` value of anything else before relying on it.

A `point` frame carries `type`, `match_id`, `published_at`, `pbp_coverage`, `quality` and a `point` object. Inside it, `seq` is per-match, monotonic and never skips a value, which makes it the deduplication and resume key across webhooks, the WebSocket and the REST catch-up endpoint.

## Setting up Live Tennis API webhooks

Register a webhook with your API key:

```bash
curl -X POST "https://api.livetennisapi.com/api/public/v1/webhooks" \
  -H "X-API-Key: $LIVE_TENNIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/webhooks/live-tennis-api", "events": ["score", "point"]}'

```

The `201` response includes the webhook's `id` and its `secret`. Store the secret straight away, for example as `LIVE_TENNIS_API_WEBHOOK_SECRET`: this is the only time it is shown, and `GET /webhooks` never returns it. A fourth webhook on the same key returns `409 webhook_limit`, so delete one first with `DELETE /webhooks/{webhookId}`.

The URL must be HTTPS and publicly routable, and it must answer directly: redirects are disabled, so a URL that redirects from `http` to `https`, or adds a trailing slash, fails every delivery.

## Securing Live Tennis API webhooks

Read `X-LTAPI-Signature`, require and strip `sha256=`, compute an HMAC-SHA256 of the raw body with the webhook secret, and compare in constant time.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyLiveTennisSignature(rawBody, header, secret) {
  const match = /^sha256=([0-9a-f]{64})$/i.exec(typeof header === 'string' ? header : '');
  if (!match || !secret) return false; // check the hex shape before Buffer.from(x, 'hex')

  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  const received = Buffer.from(match[1], 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/live-tennis-api', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.LIVE_TENNIS_API_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyLiveTennisSignature(req.body, req.get('x-ltapi-signature'), secret)) {
    return res.sendStatus(401);
  }

  const frame = JSON.parse(req.body.toString('utf8'));
  const frame_type = frame.type || req.get('x-ltapi-event');
  // Queue the frame and return straight away: the timeout is about 3 seconds.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac

def verify_live_tennis_signature(raw_body: bytes, header, secret: str) -> bool:
    if not header or not secret or not header.startswith("sha256="):
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    # Compare bytes so a non-ASCII header fails verification instead of raising.
    received = header[len("sha256="):].lower().encode("utf-8")
    return hmac.compare_digest(received, expected.encode("utf-8"))

```

Live Tennis API suggests rejecting stale replays using `X-LTAPI-Timestamp`. Because that header is not signed, anyone replaying a captured request can change it, so it is a weak check at best. Deduplicate point frames on `seq` instead.

> **Make Live Tennis API webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) answers each delivery as soon as it arrives, verifies the `X-LTAPI-Signature` header, and queues every score and point frame for your service, so a slow handler never misses the three-second window.

## Live Tennis API webhook limitations and pain points

### At-most-once delivery

**The Problem:** A frame that your endpoint doesn't accept in time is gone.

**Why It Happens:** Live Tennis API makes a single attempt per frame with a timeout of about three seconds, and has no retries or replay. A deploy, a cold start or a slow database write is enough to lose a frame.

**Workarounds:**

* Acknowledge first and process afterwards, so the response never depends on your own work.
* For point frames, track the last `seq` per match and fetch gaps from `GET /matches/{matchId}/points?after_seq=`.
* For score frames, wait for the next commit or read `GET /matches/{matchId}/score`.

**How Hookdeck Can Help:** Hookdeck accepts each delivery straight away and stores it, then delivers it to your service with [automatic retries](/docs/retries). Your own downtime then delays frames instead of losing them, as long as Hookdeck can be reached.

### 25 failures and the webhook switches off

**The Problem:** Deliveries stop, and nothing comes back until you register the webhook again.

**Why It Happens:** After 25 consecutive failures, Live Tennis API sets `enabled` to `false` and records `last_error`. There is no re-enable call: you delete the webhook and register a new one, which also means a new secret. During a busy day of matches, 25 failures can happen in seconds.

**Workarounds:**

* Poll `GET /webhooks` and alert when `enabled` is `false` or `consecutive_failures` climbs.
* Script re-registration so you can restore the webhook and deploy the new secret quickly.

**How Hookdeck Can Help:** With Hookdeck in front, Live Tennis API delivers to Hookdeck rather than to your service, so an outage on your side doesn't count towards the 25 failures. If you do re-register, you update the secret on one Hookdeck source.

### The timestamp isn't signed

**The Problem:** A replay check built on `X-LTAPI-Timestamp` can be bypassed.

**Why It Happens:** The signature covers only the body. The timestamp is a separate, unsigned header.

**Workarounds:**

* Deduplicate on `seq` for point frames, and treat score frames as idempotent state.
* Keep the endpoint on HTTPS.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeated frames based on body fields you choose, such as `match_id` with `point.seq`.

## Best practices

### Verify against the raw body

The HMAC covers the exact bytes sent. Use `express.raw()` in Express, `await request.text()` in a Next.js route handler and `await request.body()` in FastAPI, and parse only after the check passes.

### Treat score frames as state and point frames as events

A score frame replaces whatever you held before, so apply the latest one and move on. A point frame adds to a sequence, so store it by `match_id` and `seq` and fill gaps from the points endpoint.

### Respond in well under three seconds

Do nothing but verify and enqueue before you return `200`. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Keep the URL redirect-free

Register the exact URL that serves the handler, with the right scheme, host and path. Redirects are not followed.

### Watch webhook health

Read `last_delivery_at`, `consecutive_failures` and `last_error` from `GET /webhooks` and alert before the 25-failure limit is reached.

## Conclusion

Live Tennis API signs each frame with `X-LTAPI-Signature`: `sha256=` and a hex HMAC-SHA256 of the raw body. Store the secret from the registration response, verify against the raw bytes, answer within three seconds, and build a catch-up path for point frames, because delivery is at-most-once and 25 failures in a row disable the webhook.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks every signature, and holds each frame until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Live Tennis API webhooks reliably in minutes.