# Entity matching

`/rest/match/*` resolves names supplied by *your* provider feed to OddsHawk's canonical entities
(events, selections, teams, competitions). It is a **case-resolution / entity-mapping** API, not part
of the odds catalog: use it when you need to join your own bookmaker or event data to OddsHawk rows.

Like every other `/rest` endpoint — `/rest/odds` included — `/rest/match/*` is available to any
**authenticated** account. See the [coverage model](./coverage.md) for the catalog endpoints.

## Authentication

Authentication is unchanged from the rest of `/rest`: `X-OH-USER` + `X-OH-HASH` (or the `oh-id`
session cookie) — see the [quickstart](./quickstart.md). A request without valid authentication
returns **403**.

## When to use it

Use matching when you ingest odds from a provider yourself and need OddsHawk's canonical names
before comparing with `GET /rest/odds`. If you only consume the catalog, you do not need it — catalog
responses already carry canonical names.

## Endpoints

| Endpoint | Resolves | Required query params |
|----------|----------|-----------------------|
| `GET /rest/match/event` | Event | `provider`, `name`, `time`, `sport` |
| `GET /rest/match/selection` | Selection | `provider`, `name`, `time`, `sport`, `event` |
| `GET /rest/match/team` | Team | `provider`, `name`, `time`, `sport` |
| `GET /rest/match/competition` | Competition | `provider`, `name`, `time`, `sport` |

| Param | Meaning |
|-------|---------|
| `provider` | Provider (bookmaker) name as stored, e.g. `Bet365` |
| `name` | The name to resolve (URL-encoded) — a canonical name, a stored provider spelling, or a known alias |
| `time` | Event start time in **unix seconds** (integer). It scopes the `init` candidate lookup and is echoed in the `Home v Away` pair; the dictionary lookup itself does not filter on it |
| `sport` | Sport name, e.g. `Horse Racing` |
| `event` | The **canonical** event name — e.g. the `event.name` returned by `/rest/match/event`. Required on `/match/selection` |
| `init` | Optional boolean, default `false` — see [Retraining misses](#retraining-misses-init) |

```bash
curl -sS "https://www.odds.software/rest/match/event?provider=Bet365&name=Arsenal%20v%20Chelsea&time=1734567890&sport=Football" \
  -H "X-OH-USER: ${USER}" \
  -H "X-OH-HASH: ${HASH}"
```

## Responses

### Canonical dictionary match

The canonical dictionary is searched by exact `string`/`slug`, then by known aliases
(`matches.string`/`matches.slug`), and the matching record is returned:

```json
{
  "_id": "event_...",
  "type": "event",
  "sport": "Horse Racing",
  "time": 12345,
  "string": "Canonical Name",
  "slug": "canonical name",
  "matches": [{ "string": "provider spelling", "slug": "provider spelling" }]
}
```

`matches` lists the provider spellings already known for that entity. Extra fields may be present —
`provider` appears on provider-scoped records.

There is **no fuzzy fallback**: a near-miss name returns **404**. (`fuzzball` scores only rank the
candidate list that [`init=true`](#retraining-misses-init) records for OddsHawk review.)

### `Home v Away` names

When `name` contains ` v ` and both sides resolve to a canonical team, the endpoint returns the
composite pair instead of a single entity. The split runs before the per-type lookup, so **any** of
the four endpoints can return it — including `/match/selection` and `/match/competition` — whenever
`name` is a pair:

```json
{ "_id": "arsenal_liverpool", "string": "Arsenal v Liverpool", "time": 12345, "home": "Arsenal", "away": "Liverpool" }
```

### Name-only results

Some lookups return just the canonical name instead of a dictionary record:

| Endpoint | Body |
|----------|------|
| `/rest/match/event` | `{ "event": { "name": "Canonical Name" } }` |
| `/rest/match/selection` | `{ "selection": { "name": "Canonical Name" } }` |
| `/rest/match/competition` | `{ "competition": { "name": "Canonical Name" } }` |

Treat these as successful resolutions; they are the `Match*Result` response schemas in the OpenAPI
document. `/rest/match/team` always resolves through the dictionary.

## Retraining misses (`init`)

`init=true` registers names the dictionary cannot resolve. The call itself still returns **404**
either way:

- **No candidates exist** → the name is stored as a new canonical entry for that
  provider/sport/time, and repeating the call resolves it.
- **Candidates exist** → the top 10 candidates are stored in `possibles` for OddsHawk review. The
  name is *not* made resolvable, so repeating the call returns **404** again (re-recording
  `possibles`) until OddsHawk curates the entry.

Use `init` sparingly — it mutates the dictionary (or the review queue) and is intended for names you
genuinely receive from your provider feed.

## Statuses

| Status | Meaning |
|--------|---------|
| **200** | Resolved. Metered as one data point. |
| **400** | A required query parameter is missing — body `{ "error": "..." }` (message text varies per endpoint) |
| **403** | Not authenticated |
| **404** | No match — body `{ "error": "Not found" }` (with `init=true` the name is registered, but this call is still 404; see [Retraining misses](#retraining-misses-init) for when a repeat succeeds) |

A capped account over its hourly limit gets **429** before the lookup runs — see the
[error guide](./errors.md).

## SDKs

Both SDKs expose matching methods (JS first, Python mirrors the signatures):

| JS | Python |
|----|--------|
| `rest.matchEvent(provider, name, time, sport, init)` | `client.match_event(provider, name, time, sport, init)` |
| `rest.matchSelection(provider, name, time, sport, eventName, init)` | `client.match_selection(provider, name, time, sport, event_name, init)` |
| `rest.matchTeam(provider, name, time, sport, init)` | `client.match_team(provider, name, time, sport, init)` |
| `rest.matchCompetition(provider, name, time, sport, init)` | `client.match_competition(provider, name, time, sport, init)` |

These methods return a falsy value (`false` / `False`) when the API has no match instead of throwing.

## Related

- [Error handling](./errors.md)
- [Coverage model](./coverage.md)
- [Quickstart](./quickstart.md)
