# Owlect REST API

> Read and change a user's Owlect collections and items over HTTPS. Every request carries an OAuth access token the user granted to your app, and sees only that user's data.

- Base URL: `https://owlect.app/api/v1`
- OpenAPI 3.1 spec: https://owlect.app/api/v1/openapi.json
- v1 · JSON · OAuth 2.1 + PKCE · OpenAPI 3.1 · 11 endpoints
- Human-readable version: https://owlect.app/developers/api

## Quick start

1. **Register your app.** POST your redirect URI to the registration endpoint and keep the client_id it returns.
2. **Get a token.** Send the user to the authorize URL, they sign in with Google and click Allow, then exchange the code for an access token.
3. **Call the API.** Send Authorization: Bearer <token> with every request. Start with GET /account.

## Authentication

Owlect is an OAuth 2.1 authorization server. Apps are public clients that prove themselves with PKCE; there is no client secret to leak.

### 1. Register a client

Redirect URIs must be https, or http on localhost. Registration is open and rate limited.

```bash
curl -X POST https://owlect.app/api/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name": "My app", "redirect_uris": ["https://myapp.example/callback"]}'
```

### 2. Send the user to authorize

Use response_type=code, a PKCE S256 code_challenge, your redirect_uri, a state value and scope="read create update delete" (ask only for what you need). The user signs in with Google, sees a consent screen naming your redirect host, and ticks what to allow: read and create are pre-selected. The token response's scope says what was granted.

```text
https://owlect.app/oauth/authorize?response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=https%3A%2F%2Fmyapp.example%2Fcallback
  &code_challenge=CODE_CHALLENGE&code_challenge_method=S256
  &scope=read%20create%20update%20delete&state=STATE
```

### 3. Exchange the code

POST the code and your code_verifier to the token endpoint. You get an access token (1 hour) and a refresh token (30 days), which rotates on every use: always store the newest one.

```bash
curl -X POST https://owlect.app/api/oauth/token \
  -d grant_type=authorization_code \
  -d code=CODE -d client_id=CLIENT_ID \
  -d redirect_uri=https://myapp.example/callback \
  -d code_verifier=CODE_VERIFIER
```

### 4. Call the API

Send Authorization: Bearer <access_token>. When it expires, use grant_type=refresh_token for a new pair.

```bash
curl -X POST https://owlect.app/api/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=REFRESH_TOKEN -d client_id=CLIENT_ID
```

### Scopes

- `read`: See your collections and items. Always on.
- `create`: Add collections and items. On by default.
- `update`: Change collections and items. Off unless you tick it.
- `delete`: Delete collections and items. Off unless you tick it.

There are no personal API keys yet: every token comes from a user approving your app, and the user can revoke it at any time in Settings -> Connected apps.

## Endpoints

All endpoints take and return JSON. Ids are UUIDs.

## Endpoints: Account

### GET /api/v1/account

`getAccount`: Plan and usage, the Free vs Plus plan table (limits, features, price), this token's access, remaining daily write/delete budgets, batch sizes, the subscription (status, renewal or end date, manage URL) and links to the in-app pages. Call it first; reading it spends no budget.

No parameters.

## Endpoints: Collections

### GET /api/v1/collections

`listCollections`: All of the user's collections, most recently updated first.

No parameters.

### POST /api/v1/collections

`createCollection`: Creates a collection. Built-in types get their standard fields unless fieldDefinitions is sent. The custom type needs Owlect Plus.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string, 1-100 | yes | Collection name. |
| `type` | body | dolls \| board_games \| coins \| stamps \| music \| pokemon_cards \| sneakers \| retro_games \| funko_pop \| lego \| comic_books \| books \| watches \| cars \| hot_wheels \| custom | yes | Collection type. Decides the default fields and the lookups used in the app. |
| `description` | body | string, max 500 | no | Shown on the collection page. |
| `fieldDefinitions` | body | array of { key, label, type, options?, required? } | no | Custom fields. Omit to use the type's standard fields. |

### GET /api/v1/collections/{collectionId}

`getCollection`: One collection with its custom field definitions. Read these before writing customFieldValues.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `collectionId` | path | uuid | yes | Id of one of the user's collections (from the collection list). |

### PATCH /api/v1/collections/{collectionId}

`updateCollection`: Changes the name, description or field definitions. Fields you do not send stay as they are.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `collectionId` | path | uuid | yes | Collection to change. |
| `name` | body | string, 1-100 | no | New name. |
| `description` | body | string, max 500 | no | New description. |
| `fieldDefinitions` | body | array of { key, label, type, options?, required? } | no | Replaces the whole field list. |

### DELETE /api/v1/collections/{collectionId}

`deleteCollection`: Deletes the collection and every item in it. confirmName must match the collection's name exactly.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `collectionId` | path | uuid | yes | Collection to delete. |
| `confirmName` | query | string (the collection's exact name) | yes | Must equal the collection's name exactly. |

## Endpoints: Items

### GET /api/v1/collections/{collectionId}/items

`searchItems`: Items in a collection, newest first, with an optional name search and for-sale filter. Paginated with a cursor.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `collectionId` | path | uuid | yes | Collection to search. |
| `query` | query | string, max 200 | no | Case-insensitive match on the item name. |
| `forSale` | query | boolean | no | true for items listed for sale, false for the rest. |
| `limit` | query | integer 1-50, default 20 | no | Items per page. |
| `cursor` | query | string (nextCursor from the previous page) | no | nextCursor from the previous page. Omit it for the first page. |

### POST /api/v1/collections/{collectionId}/items

`createItems`: Adds up to 50 items in one request. Custom field values are validated first; if any item is invalid, nothing is saved.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `collectionId` | path | uuid | yes | Collection to add to. |
| `items` | body | array, 1-50 items | yes | The items to create. |
| `items[].name` | body | string, 1-200 | yes | Item name. |
| `items[].description` | body | string, max 1000 | no | Free-text notes. |
| `items[].quantity` | body | integer 1-999 | no | How many copies you own. |
| `items[].customFieldValues` | body | object: field key -> string \| number \| boolean \| null | no | Values keyed by the collection's field keys (read the collection first to get them). Unknown keys are rejected with the list of valid ones. |
| `items[].forSale` | body | boolean | no | Lists the item for sale. |
| `items[].salePrice` | body | integer (whole currency units) \| null | no | Asking price in whole units, e.g. 40 for $40. |
| `items[].saleCurrency` | body | currency code, e.g. USD, EUR, UAH | no | Currency of the sale price. Defaults to USD. |
| `items[].completeness` | body | complete \| incomplete \| partial \| sealed \| unknown \| null | no | Whether the item is complete, sealed and so on. |
| `items[].barcode` | body | string, max 64 \| null | no | EAN, UPC or ISBN. |
| `items[].coverUrl` | body | https URL \| null | no | Link to a cover image. Stored as a link, not uploaded. |

### DELETE /api/v1/collections/{collectionId}/items

`deleteItems`: Deletes up to 25 items from the collection.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `collectionId` | path | uuid | yes | Collection the items belong to. |
| `ids` | query | comma-separated uuids | yes | Comma-separated ids of the items to delete. |

### GET /api/v1/items/{itemId}

`getItem`: One item with every field.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `itemId` | path | uuid | yes | Id of an item (from listing or searching a collection). |

### PATCH /api/v1/items/{itemId}

`updateItem`: Changes only the fields you send. customFieldValues merge by key and null clears a field.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `itemId` | path | uuid | yes | Item to change. |
| `name` | body | string, 1-200 | no | New name. |
| `description` | body | string, max 1000 | no | Free-text notes. |
| `quantity` | body | integer 1-999 | no | How many copies you own. |
| `customFieldValues` | body | object: field key -> string \| number \| boolean \| null | no | Values keyed by the collection's field keys (read the collection first to get them). Unknown keys are rejected with the list of valid ones. |
| `forSale` | body | boolean | no | Lists the item for sale. |
| `salePrice` | body | integer (whole currency units) \| null | no | Asking price in whole units, e.g. 40 for $40. |
| `saleCurrency` | body | currency code, e.g. USD, EUR, UAH | no | Currency of the sale price. Defaults to USD. |
| `completeness` | body | complete \| incomplete \| partial \| sealed \| unknown \| null | no | Whether the item is complete, sealed and so on. |
| `barcode` | body | string, max 64 \| null | no | EAN, UPC or ISBN. |
| `coverUrl` | body | https URL \| null | no | Link to a cover image. Stored as a link, not uploaded. |

## Examples

Replace $TOKEN with an access token and the ids with real ones.

### List collections

```bash
curl https://owlect.app/api/v1/collections \
  -H "Authorization: Bearer $TOKEN"
```

### Add items to a collection

```bash
curl -X POST https://owlect.app/api/v1/collections/COLLECTION_ID/items \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"items": [
    {"name": "Wingspan", "customFieldValues": {"game_type": "Base Game"}},
    {"name": "Scythe", "quantity": 2}
  ]}'
```

### Search items that are for sale

```bash
curl "https://owlect.app/api/v1/collections/COLLECTION_ID/items?forSale=true&limit=20" \
  -H "Authorization: Bearer $TOKEN"
```

### List an item for sale

```bash
curl -X PATCH https://owlect.app/api/v1/items/ITEM_ID \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"forSale": true, "salePrice": 40, "saleCurrency": "USD"}'
```

## Errors

Errors use one JSON shape and a meaningful HTTP status. `code` is stable; `message` is for people and may change.

```json
{
  "error": {
    "code": "invalid_input",
    "message": "Unknown field \"publsher\". This collection's fields are: ...",
    "details": {
      "validKeys": [
        "publisher",
        "players"
      ]
    }
  }
}
```

- `401 unauthorized`: No token, or it is invalid, expired or revoked. Refresh it or send the user through authorization again.
- `403 insufficient_scope`: The token only has the read scope.
- `403 plan_limit`: The user's plan limit was reached. details has the limit and an upgradeUrl.
- `404 not_found`: No such collection or item for this user.
- `409 confirmation_required`: confirmName did not match the collection's name.
- `413 payload_too_large`: The request body is over 1 MB. Send fewer items per request.
- `422 invalid_input`: The input failed validation, or the body had a key the endpoint does not know (details.unknownKeys). details.validKeys lists a collection's field keys when a custom field was unknown.
- `429 rate_limited`: Too many requests. Wait for the Retry-After header.
- `503 unavailable`: A temporary problem on Owlect's side. Retry shortly.
- `500 internal`: An unexpected error. Retry; if it keeps happening, contact support.

## Pagination

GET /collections/{collectionId}/items returns { items, nextCursor }. Pass nextCursor back as ?cursor= for the next page; it is null on the last page. Pages are stable while you write.

## Limits

Limits apply per connected app (grant). The REST API and the MCP server share them.

- 120 requests per minute.
- 500 write requests per day, of which 20 may be deletes.
- Per user, across all their connected apps: 1000 write requests and 40 deletes per day.
- Up to 50 items per create request and 25 per delete request.
- The user's plan limits for items and collections apply as in the app.
- Every authenticated response carries RateLimit-Policy (the per-minute quota, e.g. "minute";q=120;w=60) and RateLimit (what is left and the seconds until it resets, e.g. "minute";r=57;t=18), following the IETF RateLimit header fields draft. A 429 also carries Retry-After. The daily budgets are in GET /account.

## Custom GPTs (ChatGPT Actions)

A Custom GPT can use the Owlect API as an Action.

OpenAPI spec URL: https://owlect.app/api/v1/openapi.json

1. In the GPT editor, open Actions -> Create new action -> Import from URL, and paste the OpenAPI spec URL.
2. Under Authentication choose OAuth. Authorization URL: https://owlect.app/oauth/authorize. Token URL: https://owlect.app/api/oauth/token. Scope: read create update delete (the user still chooses on the consent screen). Token exchange method: Default (POST request).
3. Enter the client ID and client secret Owlect gives you for the GPT, then copy the callback URL ChatGPT shows so it can be registered.

GPT Actions use a client secret, which Owlect issues per GPT on request. Email support@owlect.app with the GPT's callback URL.

## Versioning

This is v1, and the version is part of the path. Adding endpoints, optional parameters or response fields is not a breaking change; anything that would break a client goes into a new version. Before an endpoint or a version is removed, it responds with a Deprecation header (RFC 9745) and a Sunset header (RFC 8594) with the removal date for at least 6 months, and the change is announced on the Updates page. Nothing is deprecated today.
