# Products

List your products and change what a scan shows.


A **product** is the item you protect, such as a bottle of Lonafen, together
with what a customer sees after a scan. Each product owns a set of
[tags](/docs/api/tags), one per physical item. This API lists the products of
your team, reads one, and changes the title, description, and images it shows.

Every request needs an [API key](/docs/api/authentication). A key is limited
to the products of its own team.

You still create a product, mint more tags, assign a category, and change
where a scan lands on the [dashboard](/docs/dashboard/products). Documents stay
there too. This API returns the product's
[category](/docs/dashboard/categories) so you can see which group it belongs to.

## Use cases

- **Keep an ERP catalogue in sync.** Your product names and descriptions live
  in an ERP. Push a change to the product with one
  [update](#update-a-product), and every tag that shows the product's title
  shows the new one on its next scan.
- **Refresh images.** Product photos move to a new CDN, or a pack design
  changes. Write the new `image_urls` from the system that hosts them, and the
  verification page shows the new ones.

## Fields

A product is a JSON:API resource of type `products`.

| Field | Type | Notes |
| --- | --- | --- |
| `id` | string | `prd_`-prefixed. The same id the dashboard's product URLs carry. |
| `title` | string | The product's title, max 50 characters. |
| `description` | string or `null` | The product's description. |
| `image_urls` | array of strings | The images the verification page shows, as URLs, in order. |
| `verification_type` | string | `internal` when TAGBASE hosts the verification page, `external` when a scan redirects to [your own domain](/docs/your-domain/external-verification). Read only. |
| `tag_count` | integer | How many tags the product has. Read only. |
| `website` | string or `null` | The product's own page, as a URL, when the product has one. Read only. |
| `category` | object or `null` | The category the product belongs to, `{ "id", "name" }`, or `null` when it belongs to none. Read only. A product belongs to one category at most. |
| `type` | `null` | Unused. Always `null`. |

The `relationships` object is empty. Documents and links stay on the
dashboard.

### A tag inherits from its product

A tag with no title or description of its own shows the product's, so an edit
here shows on every such tag at once. The [Tags](/docs/api/tags) endpoints
read and write the values of one tag.

## List products

```
GET /api/products
```

Lists the products of the team, ordered by title. Paging follows the shared
[pagination rules](/docs/api/introduction#pagination).

### Query parameters

| Parameter | Type | Notes |
| --- | --- | --- |
| `page` | integer | The page, counting from 1. Defaults to `1`. |
| `page_size` | integer | Products per page. Defaults to `10`, maximum `100`. |

<!-- tabs -->
```bash cURL
curl "https://verify.tagbase.io/api/products?page=1&page_size=100" \
  -H "Authorization: Bearer $TAGBASE_API_KEY" \
  -H "Accept: application/vnd.api+json"
```
```js
const res = await fetch(
  "https://verify.tagbase.io/api/products?page=1&page_size=100",
  {
    headers: {
      "Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
      "Accept": "application/vnd.api+json",
    },
  },
);
const { data, meta } = await res.json();
```
```php
$response = $client->get("https://verify.tagbase.io/api/products", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
        "Accept"        => "application/vnd.api+json",
    ],
    "query" => ["page" => 1, "page_size" => 100],
]);
$page = json_decode((string) $response->getBody(), true);
```
```elixir
page =
  Req.get!("https://verify.tagbase.io/api/products",
    params: [page: 1, page_size: 100],
    headers: [
      {"authorization", "Bearer #{System.fetch_env!("TAGBASE_API_KEY")}"},
      {"accept", "application/vnd.api+json"}
    ]
  ).body
```
<!-- /tabs -->

### Response: `200 OK`

A `data` array of products, each in the shape of
[Retrieve a product](#retrieve-a-product), plus a `meta` object that
describes the page:

```json
{
  "data": [
    {
      "type": "products",
      "id": "prd_4fm3xUcsDEUK77WDBMed6U",
      "attributes": {
        "title": "Lonafen",
        "description": "Sublingual fentanyl spray by Zanna Therapeutics.",
        "image_urls": ["https://cdn.zanna.example/lonafen/front.jpg"],
        "verification_type": "internal",
        "tag_count": 2000,
        "website": "https://zanna.example/lonafen",
        "category": { "id": "tpl_7kQ2mN4pR8sT1uV6wX9yZa", "name": "Pharmaceuticals" },
        "type": null
      },
      "relationships": {}
    }
  ],
  "meta": { "total_count": 1, "page": 1, "page_size": 100 }
}
```

### Errors

| Status | When |
| --- | --- |
| `401` | Missing, invalid, or revoked key. |

## Retrieve a product

```
GET /api/products/:id
```

<!-- tabs -->
```bash cURL
curl https://verify.tagbase.io/api/products/prd_4fm3xUcsDEUK77WDBMed6U \
  -H "Authorization: Bearer $TAGBASE_API_KEY" \
  -H "Accept: application/vnd.api+json"
```
```js
const res = await fetch(
  "https://verify.tagbase.io/api/products/prd_4fm3xUcsDEUK77WDBMed6U",
  {
    headers: {
      "Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
      "Accept": "application/vnd.api+json",
    },
  },
);
const product = await res.json();
```
```php
$response = $client->get("https://verify.tagbase.io/api/products/prd_4fm3xUcsDEUK77WDBMed6U", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
        "Accept"        => "application/vnd.api+json",
    ],
]);
$product = json_decode((string) $response->getBody(), true);
```
```elixir
product =
  Req.get!("https://verify.tagbase.io/api/products/prd_4fm3xUcsDEUK77WDBMed6U",
    headers: [
      {"authorization", "Bearer #{System.fetch_env!("TAGBASE_API_KEY")}"},
      {"accept", "application/vnd.api+json"}
    ]
  ).body
```
<!-- /tabs -->

### Response: `200 OK`

```json
{
  "data": {
    "type": "products",
    "id": "prd_4fm3xUcsDEUK77WDBMed6U",
    "attributes": {
      "title": "Lonafen",
      "description": "Sublingual fentanyl spray by Zanna Therapeutics.",
      "image_urls": ["https://cdn.zanna.example/lonafen/front.jpg"],
      "verification_type": "internal",
      "tag_count": 2000,
      "website": "https://zanna.example/lonafen",
      "category": { "id": "tpl_7kQ2mN4pR8sT1uV6wX9yZa", "name": "Pharmaceuticals" },
      "type": null
    },
    "relationships": {}
  }
}
```

### Errors

| Status | When |
| --- | --- |
| `401` | Missing, invalid, or revoked key. |
| `404` | No such product in your team. A product of another team and a malformed id get the same answer. |

## Update a product

```
PATCH /api/products/:id
```

Writes `title`, `description`, or `image_urls`. Send any of them, in any
combination. An attribute you send replaces the stored value as a whole, and
an attribute you leave out stays as it is. So send the full `image_urls` array
you want the product to show, with the URLs that did not change. The API
ignores any other attribute in the body.

### Request

One resource under `data`, with the product's `id`:

```json
{
  "data": {
    "type": "products",
    "id": "prd_4fm3xUcsDEUK77WDBMed6U",
    "attributes": {
      "title": "Lonafen 400 µg",
      "image_urls": [
        "https://cdn.zanna.example/lonafen/front.jpg",
        "https://cdn.zanna.example/lonafen/back.jpg"
      ]
    }
  }
}
```

<!-- tabs -->
```bash cURL
curl https://verify.tagbase.io/api/products/prd_4fm3xUcsDEUK77WDBMed6U \
  -X PATCH \
  -H "Authorization: Bearer $TAGBASE_API_KEY" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{ "data": { "type": "products", "id": "prd_4fm3xUcsDEUK77WDBMed6U", "attributes": { "title": "Lonafen 400 µg", "image_urls": [ "https://cdn.zanna.example/lonafen/front.jpg", "https://cdn.zanna.example/lonafen/back.jpg" ] } } }'
```
```js
const res = await fetch(
  "https://verify.tagbase.io/api/products/prd_4fm3xUcsDEUK77WDBMed6U",
  {
    method: "PATCH",
    headers: {
      "Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
      "Content-Type": "application/vnd.api+json",
    },
    body: JSON.stringify({
      data: {
        type: "products",
        id: "prd_4fm3xUcsDEUK77WDBMed6U",
        attributes: {
          title: "Lonafen 400 µg",
          image_urls: [
            "https://cdn.zanna.example/lonafen/front.jpg",
            "https://cdn.zanna.example/lonafen/back.jpg",
          ],
        },
      },
    }),
  },
);
const product = await res.json();
```
```php
$response = $client->patch("https://verify.tagbase.io/api/products/prd_4fm3xUcsDEUK77WDBMed6U", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
        "Content-Type"  => "application/vnd.api+json",
    ],
    "json" => [
        "data" => [
            "type"       => "products",
            "id"         => "prd_4fm3xUcsDEUK77WDBMed6U",
            "attributes" => [
                "title"      => "Lonafen 400 µg",
                "image_urls" => [
                    "https://cdn.zanna.example/lonafen/front.jpg",
                    "https://cdn.zanna.example/lonafen/back.jpg",
                ],
            ],
        ],
    ],
]);
$product = json_decode((string) $response->getBody(), true);
```
```elixir
product =
  Req.patch!("https://verify.tagbase.io/api/products/prd_4fm3xUcsDEUK77WDBMed6U",
    headers: [
      {"authorization", "Bearer #{System.fetch_env!("TAGBASE_API_KEY")}"},
      {"content-type", "application/vnd.api+json"}
    ],
    json: %{
      data: %{
        type: "products",
        id: "prd_4fm3xUcsDEUK77WDBMed6U",
        attributes: %{
          title: "Lonafen 400 µg",
          image_urls: [
            "https://cdn.zanna.example/lonafen/front.jpg",
            "https://cdn.zanna.example/lonafen/back.jpg"
          ]
        }
      }
    }
  ).body
```
<!-- /tabs -->

### Response: `200 OK`

The updated product, in the shape of
[Retrieve a product](#retrieve-a-product).

### Errors

| Status | When |
| --- | --- |
| `400` | The body has no `data.attributes` object. |
| `401` | Missing, invalid, or revoked key. |
| `404` | No such product in your team. |
| `422` | A value failed validation. `source.pointer` names it, and `detail` says why. Nothing changed. |

A `422` for a title over 50 characters looks like this:

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

For an entry in `image_urls` that is not an `http://` or `https://` URL, the
pointer is `/data/attributes/image_urls`.

Next: read the scans of your products with the
[Verifications](/docs/api/verifications) endpoints.

