# API

What the platform API does, and the conventions every endpoint shares.


The platform API is how your backend talks to TAGBASE. You provision tags for
your items and submit the scans your users make. The API answers whether a
tap was genuine, and the page or app your users see stays yours.

It is a JSON:API over HTTPS. A sandbox key lets you build against virtual tags
and emulated scans before any hardware ships. A production key works the same
way against physical tags. See [Authentication](/docs/api/authentication) for
how to get either one.

<div class="docs-cards">
  <a class="docs-card" href="/docs/api/authentication" data-icon="identification">
    <strong>Authentication</strong>
    <span>Bearer keys, what they can reach, and the errors they answer.</span>
  </a>
  <a class="docs-card" href="/docs/api/teams" data-icon="users">
    <strong>Teams</strong>
    <span>Create subteams and mint their API keys.</span>
  </a>
  <a class="docs-card" href="/docs/api/api-keys" data-icon="key">
    <strong>API keys</strong>
    <span>Mint and revoke the keys your integrations present.</span>
  </a>
  <a class="docs-card" href="/docs/api/tags" data-icon="tag">
    <strong>Tags</strong>
    <span>Provision the digital identities that get scanned.</span>
  </a>
  <a class="docs-card" href="/docs/api/verifications" data-icon="check-circle">
    <strong>Verifications</strong>
    <span>Submit a scan and read back the authenticity verdict.</span>
  </a>
  <a class="docs-card" href="/docs/api/sessions" data-icon="arrows-right-left">
    <strong>Sessions</strong>
    <span>The unit that groups related scans into one verification flow.</span>
  </a>
  <a class="docs-card" href="/docs/api/webhooks" data-icon="bell-alert">
    <strong>Webhooks</strong>
    <span>Signed notifications when a team's resources change.</span>
  </a>
</div>

## Conventions

Every endpoint follows the same conventions, so once you have made one request
the rest are predictable.

### Base URL

```
https://platform.tagbase.io/api/v1
```

All paths in this reference are relative to that base.

### Content type

Send both headers on every `POST` and `PATCH`:

```
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
```

`Accept` is required on those requests. Most HTTP clients send `Accept: */*` or
`application/json` when you set nothing, and the API answers that with
`406 Not Acceptable`. A `Content-Type` that names a different media type gets
`415 Unsupported Media Type`. `GET` and `DELETE` don't check either header.
Responses always come back as `application/vnd.api+json`.

Request bodies are a JSON:API document, a `data` object carrying a `type` and
an `attributes` map. The bulk tag endpoints (`POST`, `PATCH` and `DELETE` on
`/tags`) take an array of those objects under `data` instead; see
[Tags](/docs/api/tags).

```json
{
  "data": {
    "type": "tags",
    "attributes": { "...": "..." }
  }
}
```

### Identifiers

Every resource id is a URL-safe string with a type prefix (`team_`, `key_`,
`tag_`, `ses_`, `vrf_`). The prefix identifies the resource type; treat the full
string as opaque.

### Errors

Errors come back as a JSON:API `errors` array. Each entry has an HTTP `status`
and a short `title`:

```json
{
  "errors": [
    { "status": "401", "title": "Unauthorized" }
  ]
}
```

The HTTP response status matches the `status` field. The statuses you'll see:

| Status | When                                                               |
|--------|-------------------------------------------------------------------|
| `400`  | The request body is missing or malformed (no `data.attributes`, or an out-of-range value). |
| `401`  | Missing, invalid, or revoked API key.                             |
| `404`  | The resource doesn't exist under your team.                    |
| `422`  | The request was well-formed but could not be processed (validation failed). |

A `422` from a tag write carries one error per failed attribute, with a
`detail` message and a `source.pointer` naming the field:

```json
{
  "errors": [
    {
      "status": "422",
      "title": "Unprocessable Entity",
      "detail": "should be at most 50 character(s)",
      "source": { "pointer": "/data/attributes/comment" }
    }
  ]
}
```

Other error bodies carry only `status` and `title`.

### Idempotency

The API has no idempotency keys, and every `POST` creates a new resource each
time it's called. Retrying a request that already succeeded (after a timeout or
a dropped connection) creates a duplicate: another batch of tags, another
subteam, or another verification.

So retry deliberately. When you get no response (a timeout or a dropped
connection), the outcome is unknown: the first attempt may have landed. Before
you retry a create, confirm on your side whether it did, or accept that a
duplicate may exist and reconcile it afterwards.

A response you did receive tells you what to do:

- `400`, `401`, `404` are definitive: the same request will fail the same way,
  so fix the request rather than retry it.
- `422` means nothing was recorded, so retrying the identical request is safe and
  won't create a duplicate. A `422` that keeps recurring means the request
  itself is wrong, so escalate it rather than keep retrying.
- `201` succeeded. Don't resend it, or you'll create a duplicate.

### Rate limiting

Requests are throttled per client IP at **600 requests per minute** across the
API. Exceeding the limit returns a plain-text `429 Too Many Requests` (not a
JSON:API error document), and no rate-limit headers are sent, so there's
nothing to inspect ahead of time: back off and retry after a pause. The limit
is generous for interactive traffic; batch work (like registering hundreds of
tags) should pace itself rather than burst.

### Pagination

[Tags](/docs/api/tags#list-tags) is the one collection with a list endpoint. It
takes `page` and `page_size` query parameters and reports where you are in a
`meta` object:

```json
{
  "data": ["..."],
  "meta": { "total_count": 250, "page": 1, "page_size": 100 }
}
```

`page` is 1-based and defaults to `1`. `page_size` defaults to `10`, with a
maximum of `100`; ask for more and you get 100 back. Walk the pages until you've
collected `total_count` records.

### What the API does not have (yet)

Keeping this explicit so you don't design around features that aren't there:

- **Tags are the only collection you can list.** You fetch every other resource
  one at a time by id: `GET /verifications/:id`, `GET /sessions/:id`, and so on
  (see the "Retrieve a…" section on each resource's page). Teams,
  verifications, sessions, keys, and webhooks have no list, search, or
  pagination. Lose one of those ids and you can't find the resource again.
- **Webhooks** push event notifications (e.g. when a tag is written) to a URL you
  register. They're the one place the platform calls *you*. See
  [Webhooks](/docs/api/webhooks). Scan verdicts still come back in the response
  to the verification request you made, not via a callback.
- **Scans are emulated in the dashboard, not the API.** On a sandbox team,
  tags are ready to scan when you create them, and you emulate scans from the
  dashboard (see [Sandbox](/docs/getting-started/sandbox)). On a production
  team, a verification only succeeds against a tag that's been written to a
  physical chip, one whose lifecycle has reached `configured` (see
  [Tags](/docs/api/tags)). A freshly provisioned production tag returns `404`
  until then.

So store each id at the moment you receive it. You can re-fetch the rest from
the retrieve endpoints and list tag ids back, but verdicts and session ids are
yours to keep when they arrive.

