# API

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


With the verify API, your own systems read and write the tags, products, and
verifications of your team. A warehouse system writes batch numbers to tags
as pallets ship, and a reporting tool pulls the week's scans into a report.
The API is server to server: a backend sends a request with an API key and
gets a JSON:API document back. You create keys under
**[API Keys](/dashboard/api-keys)** on the dashboard.

<div class="docs-cards">
  <a class="docs-card" href="/docs/api/authentication" data-icon="identification">
    <strong>Authentication</strong>
    <span>Create a key and send it as a bearer token with every request.</span>
  </a>
  <a class="docs-card" href="/docs/api/tags" data-icon="tag">
    <strong>Tags</strong>
    <span>Read and write the values of every tag, one at a time or in bulk.</span>
  </a>
  <a class="docs-card" href="/docs/api/products" data-icon="cube">
    <strong>Products</strong>
    <span>List your products and change what a scan shows.</span>
  </a>
  <a class="docs-card" href="/docs/api/verifications" data-icon="check-circle">
    <strong>Verifications</strong>
    <span>Every scan of your tags, with its status and the tag behind it.</span>
  </a>
</div>

## Conventions

Every endpoint follows the same rules, so after your first request the rest
look the same.

### Base URL

```
https://verify.tagbase.io/api
```

All paths in this reference are relative to that base. The path carries no
version. A [custom domain](/docs/your-domain/external-verification) changes
nothing here: the API answers on `verify.tagbase.io` for every team.

### Content type

Send `Content-Type: application/vnd.api+json` on a request with a body.
`Accept` is optional; a response always comes back as
`application/vnd.api+json`. If you send `Accept`, use that media type or
`application/json`. Another value gets `406 Not Acceptable`.

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

A request body is one JSON:API document: a `data` member holding a resource
with a `type`, an `id`, and an `attributes` map. The
[bulk update](/docs/api/tags#update-many-tags) sends an array of those under
`data`.

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

### Identifiers

Every resource id is a URL-safe string with a type prefix: `key_` for an API
key, `tag_` for a tag, `prd_` for a product, and `vrf_` for a verification.
The prefix names the resource type; treat the rest as opaque. The ids match
the ones in the dashboard's URLs, so an id you copy from the address bar works
in a request.

Behind the prefix sits a UUID in a compact encoding. The API accepts the raw
UUID in a path too, so `GET /tags/d52352c7-502b-4c8a-83dc-61e742d9457f` and
`GET /tags/tag_TKWk9WD8BiXrurBwjeZ5NW` read the same tag. Every response
carries the prefixed form. Store that one.

### 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 will
see:

| Status | Meaning |
| --- | --- |
| `200` | The request succeeded. The body holds the resource, or the page you asked for. |
| `400` | The body is malformed: no `data.attributes` object, or a bulk update with an empty array or more than 500 entries. |
| `401` | Missing, invalid, or revoked API key. See [Authentication](/docs/api/authentication). |
| `404` | No such resource in your team. A resource of another team and a malformed id get the same answer. |
| `422` | The request was well formed, but a value failed validation. Nothing changed. |
| `429` | Too many requests from your address. See [Rate limiting](#rate-limiting). |

A `422` names the value that failed. Each entry carries a `detail` with the
reason and a `source.pointer`, a
[JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901) into the document you
sent:

```json
{
  "errors": [
    {
      "status": "422",
      "title": "Unprocessable Entity",
      "detail": "can't be blank",
      "source": { "pointer": "/data/attributes/data/batch" }
    }
  ]
}
```

The pointer follows the shape of your request. In a single update it starts
at `/data/attributes`, so a missing category field `batch` reads
`/data/attributes/data/batch`. In a bulk update it starts at the entry's
position, counted from 0, so the same field on the second entry reads
`/data/1/attributes/data/batch`. A `404` in a bulk update points at the entry
too, `/data/1`, so you know which tag was unknown.

### Idempotency

The API creates nothing. It reads with `GET` and writes with `PATCH`, and both
are safe to repeat. A `PATCH` replaces each attribute you send as a whole, so
the same body sent twice leaves the resource in the same state as the body
sent once. When a request times out or the connection drops, send it again.

The [bulk update](/docs/api/tags#update-many-tags) is all or nothing. When one
entry fails, no tag changes, and the error points at that entry. Fix the entry
and send the whole batch again.

When you did get a response, act on its status:

- `400`, `401`, `404`: the same request fails the same way. Fix the request
  instead of repeating it.
- `422`: nothing changed. Fix the value the pointer names, then send again.
- `429`: wait, then send again.

### Rate limiting

The API accepts about **600 requests a minute** from one client address. Past
that it answers `429 Too Many Requests` as plain text. The body is not a
JSON:API error document, and the response carries no rate-limit headers, so
there is nothing to inspect ahead of time: back off and retry after a pause. The limit is generous for
interactive traffic. A sync of a large catalogue should pace itself, and the
[bulk update](/docs/api/tags#update-many-tags) changes up to 500 tags in one
request, so it stays inside the budget.

### Pagination

Every list endpoint 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` counts from 1 and defaults to `1`. `page_size` defaults to `10`, with a
maximum of `100`; ask for more and you get 100 back. `total_count` counts the
rows that match your filters, so a request with `page_size=1` is a cheap way
to count. Walk the pages until you have collected `total_count` records.

Each list has a fixed order:

| List | Order |
| --- | --- |
| [Tags](/docs/api/tags#list-tags) | Most recently changed first. A new tag counts as changed. |
| [Products](/docs/api/products#list-products) | By title. |
| [Verifications](/docs/api/verifications#list-verifications) | Newest first. |

### Code samples

Each request in this reference comes as cURL, JavaScript, PHP, and Elixir.
The JavaScript samples use `fetch`. The PHP samples use a
[Guzzle](https://docs.guzzlephp.org) client, `$client = new GuzzleHttp\Client()`.
The Elixir samples use [Req](https://hexdocs.pm/req). Every sample reads the
key from the `TAGBASE_API_KEY` environment variable.

Next: create a key and send it with every request.
[Authentication](/docs/api/authentication) covers both.

