# Operation tokens for your app

> Let users of **your own app** open and close products over Bluetooth, even where the
> site has no network. Your backend creates a short-lived **operation token** for one
> product; your app hands it to the product through the Parklio SDK. This guide covers the
> backend side in full, and the app side as a flow.

# Operation tokens for your mobile app

## Prerequisites

- An API token, on your backend only; see [Authentication](/api/authentication). Never
  ship it in the app.
- Products paired with your account. A Gateway isn't needed for this path.
- Access to the Parklio SDK for your app (Android, iOS): it's shared under contract.
  Contact [info@parklio.com](mailto:info@parklio.com). See [SDK](/sdk).

## The flow

1. A user asks your app for access to a product, for example when their booking starts.
2. Your app asks **your** backend, which checks the user is allowed.
3. Your backend creates an operation token for that product and a short window, and
   returns it to the app.
4. The app, through the SDK, sends the token's open or close command to the product over
   Bluetooth. The product checks the signature and the window itself; no network is
   needed at the product.
5. When the window ends, the token stops working.

## 1. Create a token, just in time

Call [`POST /v2/operation-tokens`](/api/endpoints/operation-tokens-create) with the
product and a window. Times are ISO-8601 in UTC.

**curl**

```bash title="Create an operation token"
curl -s -X POST https://api.parklio.com/v2/operation-tokens \
  -H "Authorization: Bearer $PARKLIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": 123,
    "start_time": "2026-03-02T08:00:00.000Z",
    "end_time": "2026-03-02T10:00:00.000Z"
  }'
```

**Node.js**

```ts title="create-token.ts"
export async function createOperationToken(productId: number, minutes: number) {
  const start = new Date();
  const end = new Date(start.getTime() + minutes * 60_000);
  const res = await fetch('https://api.parklio.com/v2/operation-tokens', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.PARKLIO_API_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      product_id: productId,
      start_time: start.toISOString(),
      end_time: end.toISOString(),
    }),
  });
  const body = await res.json();
  if (!body.success) throw new Error(`${body.errors[0].code}: ${body.errors[0].message}`);
  return body.data;
}
```

**Python**

```python title="create_token.py"
import os
from datetime import datetime, timedelta, timezone

import httpx

def create_operation_token(product_id, minutes):
    start = datetime.now(timezone.utc)
    end = start + timedelta(minutes=minutes)
    body = httpx.post(
        "https://api.parklio.com/v2/operation-tokens",
        headers={"Authorization": f"Bearer {os.environ['PARKLIO_API_TOKEN']}"},
        json={
            "product_id": product_id,
            "start_time": start.isoformat(),
            "end_time": end.isoformat(),
        },
    ).json()
    if not body["success"]:
        error = body["errors"][0]
        raise RuntimeError(f"{error['code']}: {error['message']}")
    return body["data"]
```

The response (HTTP 200):

```json title="Response"
{
  "success": true,
  "data": {
    "id": 9001,
    "product_id": 123,
    "start_time": "2026-03-02T08:00:00.000Z",
    "end_time": "2026-03-02T10:00:00.000Z",
    "status": "active",
    "used_count": 0,
    "is_unlimited": 0,
    "token_open": { "token": "<hex>", "status_key": "<hex>", "challenge_key": "<hex>" },
    "token_close": { "token": "<hex>", "status_key": "<hex>", "challenge_key": "<hex>" },
    "token_status": { "token": "<hex>", "status_key": "<hex>", "challenge_key": "<hex>" },
    "product": { "id": 123, "device_id": "B1234567" },
    "created_at": "2026-03-02T07:59:58.000Z",
    "updated_at": "2026-03-02T07:59:58.000Z"
  }
}
```

Return `token_open`, `token_close` and `token_status`, unchanged, plus the product's
`device_id` and `end_time`, to your app. The SDK documentation explains how the app uses
them.

## 2. Keep windows short

**Revoking a token doesn't reach the device.** The window is signed into the token, and
the product checks it on its own. Blocking a token
([`PATCH /v2/operation-tokens/{id}`](/api/endpoints/operation-tokens-update) with
`{"status": "blocked"}`) or deleting it only changes Parklio's records; a copy the app
already holds keeps working until `end_time`.

So the window **is** your revocation:

- Create tokens when the user needs access, not in advance.
- Make the window as short as your use case allows: the length of a booking, or a few
  minutes for a single "open now".
- Create a new token to extend access; a token can't be changed.
- **Don't use `"is_unlimited": true`.** It sets `end_time` to 2038-01-01, and such a
  token can never be revoked on the device.
- `start_time` may be in the past; `end_time` must be in the future.

## 3. Store nothing long-lived

- Don't keep tokens in your database once delivered, and don't let the app cache them
  beyond `end_time`. Create a new one each time.
- Keep the API token on your backend. The app should only ever see operation tokens.
- Use HTTPS between your app and backend, and authenticate the user before you create a
  token for them: whoever holds a token can operate the product until it expires.

## 4. Check the device clock

Time windows need the product's clock to be set. If a product reports
`product_error.rtcc_invalid` = `1` (see [Monitor devices](/guides/monitor-devices)),
its clock isn't set and time-limited tokens fail until it is. Parklio sets the clock
when a Gateway sees the product; on a site with no Gateway, contact
[support](/resources/support).

## What the API tracks, and what it doesn't

- `status` becomes `expired` (checked hourly) after `end_time`, `blocked` after you
  block it, and `invalid` when the product is removed from its lot or paired again.
- `used_count` **doesn't** count uses over Bluetooth: the product doesn't report them to
  Parklio. Log opens in your app if you need them.
- [`GET /v2/operation-tokens`](/api/endpoints/operation-tokens-find-all) lists tokens,
  filterable by `product_id` and `status`.
- Operation tokens need no plan feature and no Gateway.

## Verify it worked

1. `POST /v2/operation-tokens` returns `status: "active"` and the three commands.
2. Your app, near the product, sends `token_open`: the product opens (on a barrier, the
   arm lowers).
3. After `end_time`, the same command is refused by the product.

## Common errors

| HTTP | Code | Cause | Fix |
| --- | --- | --- | --- |
| 403 | `402` | Neither `end_time` nor `is_unlimited`. | Send an `end_time`. |
| 403 | `403` | `end_time` is in the past. | Use a future `end_time`. |
| 403 | `405` | `end_time` is before `start_time`. | Fix the window. |
| 403 | `404` | A time after 2038. | Use an earlier time. |
| 403 | `206` | The product isn't paired. | Pair it with the Connect app. |
| 404 | `6` | No such product, or your token can't see it. | Check the id and the product's lot. |
| — | — | The product refuses the token. | Check the window, the device clock (`rtcc_invalid`), and that the product wasn't paired again since the token was created. |

## Next

- [SDK](/sdk)
- [Access and keys](/concepts/access-and-keys): the other ways to share access.
