# How we verify listings

> Where a listing comes from, and what confidence and verified_at tell you.

Source: https://api.tailend.dev/docs/how-we-verify

This page is for anyone deciding how much to trust a listing before showing
it to a user — what "verified" means here, where the numbers come from, and
which fields to read if you want to show your own trust signal instead of
just taking ours.

```bash
curl -sS -H "X-API-Key: $TAILEND_API_KEY" --get https://api.tailend.dev/v1/philadelphia/events \
  --data-urlencode "min_confidence=0.8" \
  --data-urlencode "include=source" \
  --data-urlencode "limit=1"
```

```javascript
const res = await fetch(
  "https://api.tailend.dev/v1/philadelphia/events?min_confidence=0.8&include=source&limit=1",
  { headers: { "X-API-Key": process.env.TAILEND_API_KEY } },
);
const { data } = await res.json();
console.log(data[0].confidence, data[0].verified_at, data[0].source.verification.status);
```

## Where a listing comes from

Every event, class, camp and place starts from an organizer's own page —
their site, a calendar system, a listing directory. We don't invent dates,
prices or descriptions: what you get back is what the source states, read
into a consistent shape. When a value isn't on the source page, the field
is `null`, not a guess.

## Re-checking

Listings are re-checked against their source over time, prioritized around
what's about to happen — an event coming up soon, a registration deadline,
an enrollment window — rather than checked on a flat schedule regardless of
date. There's no published SLA with a numeric guarantee yet, so this page
won't quote a specific "checked every N hours" promise. What you can rely
on today is the two fields below, which tell you — per listing, right
now — whether and when it was last checked.

## The fields

Every response carries two flat fields, on every resource, always:

| Field | Meaning |
| --- | --- |
| `confidence` | 0–1 trust score. The same number `min_confidence` filters on |
| `verified_at` | UTC instant of the last check against the source, or `null` if the listing hasn't been checked yet |

`confidence` starts at a documented baseline for a newly parsed listing and
becomes a verified score once a check has run; `verified_at` is `null`
until then. Neither field tells you *why* — for that, ask for
`include=source` (a paid add-on — see [Plans & access](https://api.tailend.dev/docs/plans-and-access.md)),
which attaches:

| Field | Meaning |
| --- | --- |
| `source.url` | The page the listing was parsed from |
| `source.crawled_at` | When that page was last fetched |
| `source.verification.status` | The outcome of the last check: `clear` (still matches), `changed` (source has changed since), `flagged` (a specific problem was found, evidenced), `unverifiable` (couldn't be confirmed either way), `unreachable` (source didn't respond), `gone` (source no longer has it), or `null` before any check |
| `source.verification.basis` | `verified` once a check has produced the score, else `baseline` |
| `source.verification.date_basis` | How the date was established: `stated` (the source gave it directly), `inferred` (derived, e.g. from a recurrence rule), `unevidenced`, or `unknown` |

See the [API reference](https://api.tailend.dev/docs/api.md) for the full shape.

## Fields providers often leave unstated

`null` on a value field means **not stated by the source**. It is never a
"no": a class with `adult_participation: null` may or may not need a parent
to stay, and `drop_in_price: null` does not mean drop-ins are unavailable. We
only fill these from what the provider's own text states; we never work them
out from dates, ages or category, and we leave a field empty rather than fill
it with a value we are not sure is right.

Some class fields are stated by far fewer providers than others. Plan your UI
for `null` on these rather than treating it as missing data:

| Field | What `null` means | Notes |
| --- | --- | --- |
| `program.adult_participation` | Not stated for this class | Values: `required`, `optional`, `drop_off`, `not_allowed`, `varies`. Many providers put this rule on an FAQ or policy page rather than on the class |
| `tuition.drop_in_price` | No single-class price stated for everyone | Offered by a minority of providers; member-only or age-specific prices are not filled |
| `tuition.trial_price` | No trial price stated | `0` is a free trial; conditional trials are not filled |
| `term.total_classes` | No series length stated by the class's own text | Rolling, drop-in, open and year-round classes have none |
| `registration.spots_remaining` | The provider does not publish a seat count | Rarely published; read `registration.status` instead. `registration.has_spots_available` is `true` unless we have read a full/closed statement, so it is not a guarantee of open seats |

Schedule and price fields (`schedule.days`, `schedule.start_time`,
`schedule.duration_minutes`, `tuition.amount`) are stated more often but still
not by every provider, and follow the same rule: `null` is "not stated", not
"none".

## `min_confidence`

Every list endpoint drops rows below a confidence floor by default — you
never have to ask for this, and you can always loosen or tighten it:

| Resource | Default `min_confidence` |
| --- | --- |
| Places | `0.5` |
| Events, classes, camps | `0.7` |

Pass `min_confidence=0` to see everything regardless of score, or a higher
value to see only what's cleared a stricter bar. `place_id`-scoped and
`organization_id`-scoped lists use the same default as their resource.

## What "fresh" means, honestly

Two different things get called "freshness," and it's worth keeping them
apart when you're deciding what to show a user:

- **When we last looked** — `verified_at` / `source.crawled_at`. This is a
  fact about our process, always available per listing.
- **Whether what we found is still true** — `source.verification.status`.
  A `clear` result from three days ago is a stronger signal than a
  `changed` or `unverifiable` result from this morning.

Neither one is a promise that seats are still open or a price hasn't
changed since the last check — for time-sensitive facts like registration
status, read the listing's own `registration`/`enrollment` fields and treat
`verified_at` as "as of," not "guaranteed as of this second."

## What to do

- Use `confidence`/`min_confidence` as your baseline trust filter; only
  reach for `include=source` when you want to show your own "last checked"
  or "verified" badge to a user.
- Branch on `source.verification.status`, not on `verified_at` alone — a
  listing can be checked recently and still come back `unverifiable` or
  `flagged`.
- Don't build on a specific re-check interval as a guarantee; build on the
  per-listing fields above, which are accurate regardless of how the
  underlying schedule evolves.
