# Basketly API v1 — reference for AI agents

The complete Basketly public API reference in one Markdown file. HTML version: https://basketly.io/en/docs/api. OpenAPI 3.1 spec: https://app.basketly.io/api/v1/openapi.json. MCP server for agents: https://basketly.io/docs/mcp.md.

Base URL: `https://app.basketly.io/api/v1`

## Authentication

Every request sends an organisation key in the `Authorization: Bearer bsk_live_…` header. A key belongs to one organisation and sees only its data.

The full key is shown once, when it is created. We store only its hash, so it cannot be recovered: revoke a lost key and create a new one.

You can revoke a key in the panel at any time and give it an expiry date. A revoked key gets `401 key_revoked`, an expired one `401 key_expired`.

A key has scopes. A `:write` scope includes `:read` of the same area. `contacts:*`, `tasks:*` and `orders:read` return personal data. A missing scope answers `403 insufficient_scope` with `details.required_scope`.

The API and the MCP server are included in the Growth, Agency and Custom plans. On Free and Start the account owner gets a 5-day trial from creating the account. After it every request answers `402 plan_required`, new keys cannot be created and API webhooks are not delivered. Once you pick Growth or higher under “Plan & billing”, existing keys work again at once.

| Scope | Allows |
| --- | --- |
| `contacts:read` | Read people of the Klienci (Clients) page (names, phones, e-mails — personal data). |
| `contacts:write` | Create and change people: details, notes, tags, status, SMS block. |
| `events:read` | Read conversions and the status of their deliveries to Meta. |
| `events:write` | Send server-to-server conversions to the queue. |
| `tasks:read` | Open tasks (personal data). |
| `tasks:write` | Create, complete and postpone tasks. |
| `campaigns:read` | Campaign report. |
| `creatives:read` | Creative report. |
| `automations:read` | Automations with their last run and errors of the last day. |
| `integrations:read` | The integrations overview: states and key-based connections, without keys. |
| `orders:read` | Shop orders with lines and buyers (personal data: buyer names). |
| `products:read` | Products, their diagnosis and the product record. |
| `products:write` | Manual fixes of products. |
| `feeds:read` | Feeds, rules, flows, Google categories and previews. |
| `feeds:write` | Create and change feeds, save rules, flows and categories, download now. |
| `templates:read` | Image templates. |
| `diagnostics:read` | The last self-check. |
| `diagnostics:write` | Run the self-check. |
| `jobs:read` | Read background jobs. |
| `webhooks:read` | Read webhooks and their delivery history. |
| `webhooks:write` | Create, change, delete and test webhooks. |

## Errors

Every error has the same shape: `{"error": {"code", "message", "details?"}}`.

`code` is stable and never translated, so base your error handling on it. `message` is for humans, in the language of the `Accept-Language` header: Polish by default, `en` for English. `details` appears only when there is extra data, e.g. `required_scope`.

| Code | Meaning |
| --- | --- |
| `unauthorized` | Missing key, wrong format or unknown key. |
| `key_revoked` | The key was revoked. |
| `key_expired` | The key is past its expiry date. |
| `insufficient_scope` | The key lacks the required scope (`details.required_scope`). |
| `plan_required` | The organisation’s plan does not include the API: it needs Growth or higher after the 5-day trial. |
| `rate_limited` | Over 120 requests per minute for this key; wait `Retry-After` seconds. |
| `invalid_json` | The body is not valid JSON or has an unknown field. |
| `validation_error` | A field or parameter has an invalid value; the message says what to fix. |
| `invalid_cursor` | `cursor` is not from the previous page of the same list. |
| `not_found` | No such record in your organisation. |
| `route_not_found` | No such endpoint. |
| `conflict` | The operation conflicts with the record’s current state. |
| `contact_exists` | A person with this phone or e-mail already exists (`details.contact_id`). |
| `contact_merged` | The card was merged into another one (`details.merged_into`). |
| `event_conflict` | The event conflicts with one already accepted. |
| `source_paused` | The source is paused. |
| `limit_reached` | A limit was reached, e.g. 10 webhooks per organisation. |
| `idempotency_key_reused` | The same `Idempotency-Key` with a different request body. |
| `idempotency_in_progress` | A request with this `Idempotency-Key` is still running; retry shortly. |
| `payload_too_large` | Request body over 64 KB. |
| `internal_error` | An error on our side; retry later. |

## Pagination

Lists return `{"data": [...], "next_cursor", "has_more"}`. Set the page size with `limit` (1–100, default 25).

Fetch the next page by passing the previous response’s `next_cursor` as `cursor`. When `has_more` is `false`, the list is complete.

Records are ordered by `updated_at`, then id: `order=desc` newest first (default) or `order=asc`.

`updated_since` (ISO 8601) returns only records changed at or after that time. For incremental sync, store the time you start a sync, page through to the end, and use the stored time as `updated_since` for the next sync.

## Limits and formats

- Rate limit: 120 requests per minute per key. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.
- Over the limit you get `429 rate_limited` with a `Retry-After` header (seconds). Wait that long, then retry.
- A request body can be at most 64 KB, otherwise `413 payload_too_large`.
- Times in responses are ISO 8601 in UTC, e.g. `2026-10-08T10:00:00Z`.

## Idempotency

- Every `POST` accepts an `Idempotency-Key` header (1–255 visible ASCII characters). Use a value tied to the operation, e.g. a UUID stored with the record on your side.
- Repeating a request with the same key and the same body within 24 hours returns the stored response with `Idempotent-Replayed: true`, without running the operation again.
- The same key with a different body answers `422 idempotency_key_reused`. While the first request is still running you get `409 idempotency_in_progress`.
- 5xx and 429 responses are not stored, so after them you can safely retry with the same key.

## Endpoints

### Keys

The calling key.

#### `GET /me` — Describe the calling key

Organisation, key name, scopes and expiry. A safe first call to check a new key; needs no scope.

Scope: none

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `organization` | object |  |  |
| `organization.id` | string |  |  |
| `organization.name` | string |  |  |
| `key` | object |  |  |
| `key.id` | string |  |  |
| `key.name` | string |  |  |
| `key.prefix` | string |  |  |
| `key.scopes` | string[] |  |  |
| `key.expires_at` | string (date-time) \| null |  |  |
| `rate_limit` | object |  |  |
| `rate_limit.per_minute` | integer |  |  |

```bash
curl "https://app.basketly.io/api/v1/me" \
  -H "Authorization: Bearer $API_KEY"
```

### Contacts

People of the Klienci page.

#### `GET /contacts` — List contacts

People of the Klienci page (merged cards are left out). Filter by tag or exact e-mail or phone.

Scope: `contacts:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `limit` | query |  | Page size, 1–100 (default 25). |
| `cursor` | query |  | `next_cursor` of the previous page. |
| `order` | query |  | Sort by `updated_at` (then id): `desc` newest first (default) or `asc`. |
| `updated_since` | query |  | Only records changed at or after this ISO 8601 time. |
| `tag` | query |  | Tag. |
| `email` | query |  | Exact e-mail (matched by hash). |
| `phone` | query |  | Exact phone (matched by hash). |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | Contact[] | yes |  |
| `data[].id` | string |  | Contact id. |
| `data[].name` | string |  | Full name. |
| `data[].phone` | string |  | Phone in E.164 form, empty when unknown. |
| `data[].email` | string |  | E-mail, empty when unknown. |
| `data[].tags` | string[] |  | Tags. |
| `data[].sms_optout` | boolean |  | True when SMS to this person are blocked. |
| `data[].first_source` | string |  | Where the person first came from ("Zamówienie w sklepie", "Ręcznie", API…). |
| `data[].status_id` | string \| null |  | Internal status id (GET /contact-statuses), null when none. Never sent to Meta. |
| `data[].owner_id` | string \| null |  | User id of the person's owner (opiekun). |
| `data[].last_activity_at` | string (date-time) \| null |  | Last activity time. |
| `data[].created_at` | string (date-time) |  | Creation time. |
| `data[].updated_at` | string (date-time) |  | Last change; use with `updated_since`. |
| `data[].orders` | object |  | What the person bought in the shop. |
| `next_cursor` | string \| null | yes | Pass as `cursor` to get the next page; null on the last page. |
| `has_more` | boolean | yes | True when another page exists. |

```bash
curl "https://app.basketly.io/api/v1/contacts" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /contacts` — Create a contact

Adds a person like „Dodaj osobę” in the panel.

Scope: `contacts:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string |  | Full name (up to 200 characters). Max 200 characters. |
| `phone` | string |  | Phone with a country code, e.g. +48 600 857 482, or 9 digits (Poland). |
| `email` | string |  | E-mail address. Max 254 characters. |
| `tags` | string[] |  | Tags. |

Response 201:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Contact id. |
| `name` | string |  | Full name. |
| `phone` | string |  | Phone in E.164 form, empty when unknown. |
| `email` | string |  | E-mail, empty when unknown. |
| `tags` | string[] |  | Tags. |
| `sms_optout` | boolean |  | True when SMS to this person are blocked. |
| `first_source` | string |  | Where the person first came from ("Zamówienie w sklepie", "Ręcznie", API…). |
| `status_id` | string \| null |  | Internal status id (GET /contact-statuses), null when none. Never sent to Meta. |
| `owner_id` | string \| null |  | User id of the person's owner (opiekun). |
| `last_activity_at` | string (date-time) \| null |  | Last activity time. |
| `created_at` | string (date-time) |  | Creation time. |
| `updated_at` | string (date-time) |  | Last change; use with `updated_since`. |
| `orders` | object |  | What the person bought in the shop. |
| `orders.count` | integer |  | Bought, not cancelled shop orders. |
| `orders.value` | number |  | Their value in `currency`. |
| `orders.currency` | string |  | The currency most of the orders use. |
| `orders.first_at` | string (date-time) \| null |  | First order time. |
| `orders.last_at` | string (date-time) \| null |  | Last order time. |

```bash
curl -X POST "https://app.basketly.io/api/v1/contacts" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Anna Nowak",
    "phone": "+48600100200",
    "email": "anna@example.com",
    "tags": [
      "vip"
    ]
  }'
```

#### `GET /contacts/{id}` — Get a contact

A merged card answers 409 `contact_merged` with `details.merged_into`.

Scope: `contacts:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Contact id. |
| `name` | string |  | Full name. |
| `phone` | string |  | Phone in E.164 form, empty when unknown. |
| `email` | string |  | E-mail, empty when unknown. |
| `tags` | string[] |  | Tags. |
| `sms_optout` | boolean |  | True when SMS to this person are blocked. |
| `first_source` | string |  | Where the person first came from ("Zamówienie w sklepie", "Ręcznie", API…). |
| `status_id` | string \| null |  | Internal status id (GET /contact-statuses), null when none. Never sent to Meta. |
| `owner_id` | string \| null |  | User id of the person's owner (opiekun). |
| `last_activity_at` | string (date-time) \| null |  | Last activity time. |
| `created_at` | string (date-time) |  | Creation time. |
| `updated_at` | string (date-time) |  | Last change; use with `updated_since`. |
| `orders` | object |  | What the person bought in the shop. |
| `orders.count` | integer |  | Bought, not cancelled shop orders. |
| `orders.value` | number |  | Their value in `currency`. |
| `orders.currency` | string |  | The currency most of the orders use. |
| `orders.first_at` | string (date-time) \| null |  | First order time. |
| `orders.last_at` | string (date-time) \| null |  | Last order time. |

```bash
curl "https://app.basketly.io/api/v1/contacts/abc123" \
  -H "Authorization: Bearer $API_KEY"
```

#### `PATCH /contacts/{id}` — Update a contact

Changes name, phone, e-mail or tags; every change is a history line.

Scope: `contacts:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string |  | Full name. |
| `phone` | string |  | Phone; an empty string removes it (a phone or an e-mail must remain). |
| `email` | string |  | E-mail; an empty string removes it. |
| `tags` | string[] |  | Replaces all tags. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Contact id. |
| `name` | string |  | Full name. |
| `phone` | string |  | Phone in E.164 form, empty when unknown. |
| `email` | string |  | E-mail, empty when unknown. |
| `tags` | string[] |  | Tags. |
| `sms_optout` | boolean |  | True when SMS to this person are blocked. |
| `first_source` | string |  | Where the person first came from ("Zamówienie w sklepie", "Ręcznie", API…). |
| `status_id` | string \| null |  | Internal status id (GET /contact-statuses), null when none. Never sent to Meta. |
| `owner_id` | string \| null |  | User id of the person's owner (opiekun). |
| `last_activity_at` | string (date-time) \| null |  | Last activity time. |
| `created_at` | string (date-time) |  | Creation time. |
| `updated_at` | string (date-time) |  | Last change; use with `updated_since`. |
| `orders` | object |  | What the person bought in the shop. |
| `orders.count` | integer |  | Bought, not cancelled shop orders. |
| `orders.value` | number |  | Their value in `currency`. |
| `orders.currency` | string |  | The currency most of the orders use. |
| `orders.first_at` | string (date-time) \| null |  | First order time. |
| `orders.last_at` | string (date-time) \| null |  | Last order time. |

```bash
curl -X PATCH "https://app.basketly.io/api/v1/contacts/abc123" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Anna Nowak",
    "phone": "+48600100200",
    "email": "anna@example.com",
    "tags": [
      "vip"
    ]
  }'
```

#### `POST /contacts/{id}/notes` — Add a note

Adds a note, call or meeting to the person's history.

Scope: `contacts:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string |  | Entry kind. One of: `note`, `call`, `meeting`. Default `note`. |
| `text` | string | yes | Text, 1–5000 characters. Max 5000 characters. |
| `time` | string (date-time) |  | When it happened (default: now); not in the future. |

Response 201:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | History entry id. |
| `contact_id` | string |  | Contact id. |
| `kind` | string |  | Entry kind. |
| `text` | string |  | Text. |
| `time` | string (date-time) |  | Entry time. |

```bash
curl -X POST "https://app.basketly.io/api/v1/contacts/abc123/notes" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "call",
    "text": "Asked when order 1001 will ship."
  }'
```

#### `POST /contacts/{id}/tags` — Add or remove tags

Adds and removes tags without sending the whole list.

Scope: `contacts:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `add` | string[] |  | Tags to add. |
| `remove` | string[] |  | Tags to remove. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Contact id. |
| `name` | string |  | Full name. |
| `phone` | string |  | Phone in E.164 form, empty when unknown. |
| `email` | string |  | E-mail, empty when unknown. |
| `tags` | string[] |  | Tags. |
| `sms_optout` | boolean |  | True when SMS to this person are blocked. |
| `first_source` | string |  | Where the person first came from ("Zamówienie w sklepie", "Ręcznie", API…). |
| `status_id` | string \| null |  | Internal status id (GET /contact-statuses), null when none. Never sent to Meta. |
| `owner_id` | string \| null |  | User id of the person's owner (opiekun). |
| `last_activity_at` | string (date-time) \| null |  | Last activity time. |
| `created_at` | string (date-time) |  | Creation time. |
| `updated_at` | string (date-time) |  | Last change; use with `updated_since`. |
| `orders` | object |  | What the person bought in the shop. |
| `orders.count` | integer |  | Bought, not cancelled shop orders. |
| `orders.value` | number |  | Their value in `currency`. |
| `orders.currency` | string |  | The currency most of the orders use. |
| `orders.first_at` | string (date-time) \| null |  | First order time. |
| `orders.last_at` | string (date-time) \| null |  | Last order time. |

```bash
curl -X POST "https://app.basketly.io/api/v1/contacts/abc123/tags" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "add": [
      "vip"
    ],
    "remove": [
      "cold"
    ]
  }'
```

#### `POST /contacts/{id}/sms-optout` — Block or allow SMS

Keeps the „nie wysyłaj” list and every person with the same phone in step.

Scope: `contacts:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sms_optout` | boolean | yes | True blocks SMS to the person's phone (for every person sharing it), false allows them again. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Contact id. |
| `name` | string |  | Full name. |
| `phone` | string |  | Phone in E.164 form, empty when unknown. |
| `email` | string |  | E-mail, empty when unknown. |
| `tags` | string[] |  | Tags. |
| `sms_optout` | boolean |  | True when SMS to this person are blocked. |
| `first_source` | string |  | Where the person first came from ("Zamówienie w sklepie", "Ręcznie", API…). |
| `status_id` | string \| null |  | Internal status id (GET /contact-statuses), null when none. Never sent to Meta. |
| `owner_id` | string \| null |  | User id of the person's owner (opiekun). |
| `last_activity_at` | string (date-time) \| null |  | Last activity time. |
| `created_at` | string (date-time) |  | Creation time. |
| `updated_at` | string (date-time) |  | Last change; use with `updated_since`. |
| `orders` | object |  | What the person bought in the shop. |
| `orders.count` | integer |  | Bought, not cancelled shop orders. |
| `orders.value` | number |  | Their value in `currency`. |
| `orders.currency` | string |  | The currency most of the orders use. |
| `orders.first_at` | string (date-time) \| null |  | First order time. |
| `orders.last_at` | string (date-time) \| null |  | Last order time. |

```bash
curl -X POST "https://app.basketly.io/api/v1/contacts/abc123/sms-optout" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "sms_optout": true
  }'
```

#### `GET /contacts/search` — Search contacts with panel filters

The Klienci list with its filters: values of one filter are alternatives (comma list), filters combine. Every bad value is a 400. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `contacts:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `q` | query |  | Name, e-mail or phone. |
| `page` | query |  | Page, 1 = first. |
| `per_page` | query |  | People per page: 10, 25 (default), 50 or 100. |
| `owner` | query |  | me, any, none or member ids. |
| `status` | query |  | Status ids or none. |
| `tag` | query |  | Tag. |
| `source` | query |  | First source name. |
| `buyer` | query |  | yes = has a bought shop order, no = none. |
| `orders_min` | query |  | At least this many bought orders. |
| `orders_max` | query |  | At most this many bought orders. |
| `spent_min` | query |  | Spent at least. |
| `spent_max` | query |  | Spent at most. |
| `last_order` | query |  | Last purchase within. |
| `inactive_buyer` | query |  | Bought before but not in the last N days. |
| `first_order` | query |  | First purchase within. |
| `product` | query |  | Bought this catalogue id (repeatable). |
| `created` | query |  | today, 7d, 30d or 90d (or created_from / created_to days). |
| `activity` | query |  | Last activity: today, 7d, 30d, 90d (or activity_from / activity_to). |
| `inactive_days` | query |  | No activity for at least this many days. |
| `tasks` | query |  | open, overdue or none. |
| `phone` | query |  | yes or no. |
| `email` | query |  | yes or no. |
| `optout` | query |  | yes or no. |
| `duplicate` | query |  | yes or no. |
| `sort` | query |  | activity, activity_asc, created, created_asc, spent, spent_asc, orders, orders_asc, aov (average order), aov_asc, last_order, last_order_asc, first_order or first_order_asc. Ascending purchase sorts put people without orders last. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object[] |  | People: id, name, status, owner, tags, last activity, added (created), orders (count, value, first and last), shops (source kinds of bought orders), duplicate (a possible duplicate hint), open_tasks and overdue_tasks (members who see tasks). |
| `per_page` | integer |  | People per page. |
| `total` | integer |  | All matching people. |
| `page` | integer |  | Page number. |

```bash
curl "https://app.basketly.io/api/v1/contacts/search" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /contact-statuses` — List statuses

The organisation's internal statuses. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `contacts:read`

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `statuses` | object[] |  | Statuses: id, name, archived, sort, people. |

```bash
curl "https://app.basketly.io/api/v1/contact-statuses" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /contacts/{id}/status` — Set the status

Sets an internal status (a history line). Never sent to Meta; status automations start. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `contacts:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | yes | Status id, or "" to clear. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string |  | Status id ("" = none). |
| `owner` | string |  | Owner id. |

```bash
curl -X POST "https://app.basketly.io/api/v1/contacts/abc123/status" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "purchase"
  }'
```

#### `GET /contacts/{id}/history` — Contact history

Orders, calls, notes, statuses, tasks and SMS of the person. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `contacts:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `page` | query |  | Page, 1 = newest. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object[] |  | History lines: kind, time, actor, summary, details. |
| `page` | integer |  | Page number (`?page=`). |
| `more` | boolean |  | An older page exists. |

```bash
curl "https://app.basketly.io/api/v1/contacts/abc123/history" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /segments` — List segments

Saved customer segments (Klienci filters with a name) and the ready-made shop segments, each with the number of people the member may see and, for saved ones, the Meta audience and Klaviyo list state. A segment's `filters` are /contacts/search parameters. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `contacts:read`

Response 200:


```bash
curl "https://app.basketly.io/api/v1/segments" \
  -H "Authorization: Bearer $API_KEY"
```

### Conversions

Conversions sent to Meta and their deliveries.

#### `GET /events` — List events

Conversions with their deliveries; payloads are never returned.

Scope: `events:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `limit` | query |  | Page size, 1–100 (default 25). |
| `cursor` | query |  | `next_cursor` of the previous page. |
| `order` | query |  | Sort by `updated_at` (then id): `desc` newest first (default) or `asc`. |
| `updated_since` | query |  | Only records changed at or after this ISO 8601 time. |
| `source` | query |  | Source id. |
| `status` | query |  | Your status. |
| `external_id` | query |  | Your record id. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | Event[] | yes |  |
| `data[].id` | string |  | Event id. |
| `data[].source_id` | string |  | Source id. |
| `data[].external_id` | string |  | Your record id. |
| `data[].status` | string |  | Your status. |
| `data[].meta_event_id` | string |  | Meta deduplication id. |
| `data[].event_time` | string (date-time) \| null |  | Event time. |
| `data[].value` | number \| null |  | Value. |
| `data[].currency` | string |  | Currency. |
| `data[].deliveries` | Delivery[] |  | Deliveries of this event. |
| `data[].created_at` | string (date-time) |  | Creation time. |
| `data[].updated_at` | string (date-time) |  | Last change. |
| `next_cursor` | string \| null | yes | Pass as `cursor` to get the next page; null on the last page. |
| `has_more` | boolean | yes | True when another page exists. |

```bash
curl "https://app.basketly.io/api/v1/events" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /events` — Send a conversion

Server-to-server conversion ingest. The source's active flows map `status` to Meta events and queue one delivery per dataset; dedupe rules are unchanged. Answers 202: queued, not yet sent.

Scope: `events:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `source` | string | yes | Id of a Google Sheets or shop source of your organisation (Połączenia → source details). |
| `external_id` | string | yes | Your id of the record (order number…), max 128 characters. Max 128 characters. |
| `status` | string | yes | Your status mapped to a Meta event by the source's flow, e.g. QUALIFIED or purchase. Max 80 characters. |
| `consent` | boolean | yes | Must be true: you confirm a legal basis for sending the data. |
| `event_time` | integer \| string (date-time) |  | Unix seconds or ISO 8601; at most 7 days old. |
| `event_id` | string |  | Meta deduplication id; generated from source + external_id + status when omitted. |
| `email` | string |  | Customer e-mail (hashed with SHA-256 before storage). |
| `phone` | string |  | Customer phone in E.164 (hashed). |
| `lead_id` | string |  | Meta lead id of the customer, when you have one; sent to Meta as customer information. |
| `user_id` | string |  | Your customer id (hashed as external_id). |
| `fbp` | string |  | _fbp cookie. |
| `fbc` | string |  | _fbc cookie. |
| `event_source_url` | string |  | Page URL of the conversion. |
| `tracking_id` | string |  | Collector tracking id (shop sources). |
| `client_ip_address` | string |  | Customer IP. |
| `client_user_agent` | string |  | Customer user agent. |
| `value` | number |  | Conversion value (required for Purchase). |
| `currency` | string |  | ISO 4217 currency (required with value). |
| `content_ids` | string[] |  | Product ids. |

Response 202:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `event_id` | string |  | Basketly event id. |
| `deliveries_created` | integer |  | New deliveries queued for Meta datasets. |
| `duplicates` | integer |  | Deliveries skipped as duplicates. |
| `sending_enabled` | boolean |  | False when the server does not send to Meta (queued only). |

```bash
curl -X POST "https://app.basketly.io/api/v1/events" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "src_abc123",
    "external_id": "order-1001",
    "status": "purchase",
    "consent": true,
    "event_time": "2026-10-08T10:00:00Z",
    "email": "anna@example.com",
    "phone": "+48600100200",
    "value": 1200,
    "currency": "PLN"
  }'
```

#### `GET /events/{id}` — Get an event

One conversion with the status of each delivery.

Scope: `events:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Event id. |
| `source_id` | string |  | Source id. |
| `external_id` | string |  | Your record id. |
| `status` | string |  | Your status. |
| `meta_event_id` | string |  | Meta deduplication id. |
| `event_time` | string (date-time) \| null |  | Event time. |
| `value` | number \| null |  | Value. |
| `currency` | string |  | Currency. |
| `deliveries` | Delivery[] |  | Deliveries of this event. |
| `deliveries[].id` | string |  | Delivery id. |
| `deliveries[].event_id` | string |  | Basketly event id. |
| `deliveries[].target_id` | string |  | Meta receiver (dataset connection) id. |
| `deliveries[].dataset_id` | string |  | Meta dataset (pixel) id. |
| `deliveries[].event_name` | string |  | Meta event name, e.g. Purchase, AddToCart. |
| `deliveries[].meta_event_id` | string |  | event_id sent to Meta. |
| `deliveries[].mode` | string |  | `test` or `live`. |
| `deliveries[].status` | string |  | Delivery status. One of: `pending`, `sending`, `retry`, `accepted`, `failed`, `cancelled`. |
| `deliveries[].attempts` | integer |  | Attempts so far. |
| `deliveries[].last_error` | string |  | Last error, empty when none. |
| `deliveries[].accepted_at` | string (date-time) \| null |  | When Meta accepted it (receipt, not proof of attribution). |
| `deliveries[].next_attempt_at` | string (date-time) \| null |  | Next attempt. |
| `deliveries[].created_at` | string (date-time) |  | Creation time. |
| `deliveries[].updated_at` | string (date-time) |  | Last change. |
| `created_at` | string (date-time) |  | Creation time. |
| `updated_at` | string (date-time) |  | Last change. |

```bash
curl "https://app.basketly.io/api/v1/events/abc123" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /deliveries` — List deliveries

Delivery status to Meta, e.g. `status=failed&updated_since=…` to follow failures.

Scope: `events:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `limit` | query |  | Page size, 1–100 (default 25). |
| `cursor` | query |  | `next_cursor` of the previous page. |
| `order` | query |  | Sort by `updated_at` (then id): `desc` newest first (default) or `asc`. |
| `updated_since` | query |  | Only records changed at or after this ISO 8601 time. |
| `status` | query |  | Delivery status. |
| `event` | query |  | Event id. |
| `target` | query |  | Receiver id. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | Delivery[] | yes |  |
| `data[].id` | string |  | Delivery id. |
| `data[].event_id` | string |  | Basketly event id. |
| `data[].target_id` | string |  | Meta receiver (dataset connection) id. |
| `data[].dataset_id` | string |  | Meta dataset (pixel) id. |
| `data[].event_name` | string |  | Meta event name, e.g. Purchase, AddToCart. |
| `data[].meta_event_id` | string |  | event_id sent to Meta. |
| `data[].mode` | string |  | `test` or `live`. |
| `data[].status` | string |  | Delivery status. One of: `pending`, `sending`, `retry`, `accepted`, `failed`, `cancelled`. |
| `data[].attempts` | integer |  | Attempts so far. |
| `data[].last_error` | string |  | Last error, empty when none. |
| `data[].accepted_at` | string (date-time) \| null |  | When Meta accepted it (receipt, not proof of attribution). |
| `data[].next_attempt_at` | string (date-time) \| null |  | Next attempt. |
| `data[].created_at` | string (date-time) |  | Creation time. |
| `data[].updated_at` | string (date-time) |  | Last change. |
| `next_cursor` | string \| null | yes | Pass as `cursor` to get the next page; null on the last page. |
| `has_more` | boolean | yes | True when another page exists. |

```bash
curl "https://app.basketly.io/api/v1/deliveries" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /conversions/balance` — Purchase balance

Day by day: the shop's bought orders, purchase events Basketly accepted and, per destination (each Meta receiver, GA4, TikTok, Google Ads, Microsoft Ads, Pinterest, Klaviyo — Klaviyo gets only purchases with an e-mail), purchases accepted, failed and waiting, with the most common failure reasons. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `events:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `days` | query |  | Period: 7, 14, 30 (default) or 90 days. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/conversions/balance" \
  -H "Authorization: Bearer $API_KEY"
```

### Tasks

Tasks.

#### `GET /tasks` — List tasks

Open tasks by due time. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `tasks:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `scope` | query |  | Whose work: mine, user (with user) or all. |
| `user` | query |  | Member id for scope=user. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object[] |  | Tasks by due time. |
| `counts` | object |  | Counts per section (overdue, today, later, none). |
| `truncated` | boolean |  | More than 500 open tasks. |

```bash
curl "https://app.basketly.io/api/v1/tasks" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /contacts/{id}/tasks` — Create a task

Adds a task to the person, like „Dodaj zadanie” on the card. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `tasks:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | yes | Task title. Max 200 characters. |
| `note` | string |  | Note. |
| `due_date` | string |  | Due day YYYY-MM-DD (organisation's zone); without it no due date. |
| `due_time` | string |  | Due time HH:MM; without it the end of the day. |
| `assignee` | string |  | Member id (default: the person's owner or the key's member). |

Response 201:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | New record id. |

```bash
curl -X POST "https://app.basketly.io/api/v1/contacts/abc123/tasks" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "example"
  }'
```

#### `POST /tasks/{id}/done` — Complete a task

Closes the task, optionally with a history entry. Send `{}` for none. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `tasks:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `entry` | object |  | Optional history entry written with the task (e.g. the call that closed it). |
| `entry.kind` | string | yes | Entry kind. One of: `note`, `call`, `meeting`. |
| `entry.text` | string | yes | Text. |

Response 204.

```bash
curl -X POST "https://app.basketly.io/api/v1/tasks/abc123/done" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "entry": {
      "kind": "call",
      "text": "Asked when order 1001 will ship."
    }
  }'
```

#### `POST /tasks/{id}/postpone` — Postpone a task

Moves the due time by days or to a date. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `tasks:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `days` | integer |  | Move by 1–30 days. Range 1–30. |
| `due_date` | string |  | Or a new day YYYY-MM-DD. |
| `due_time` | string |  | With due_date: time HH:MM. |

Response 204.

```bash
curl -X POST "https://app.basketly.io/api/v1/tasks/abc123/postpone" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "days": 1,
    "due_date": "example",
    "due_time": "example"
  }'
```

### Integrations

Where the shop sells, advertises and sends its catalogue.

#### `GET /integrations` — List integrations

Every place the shop connects, by category, with its state; connecting stays in the panel. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `integrations:read`

Response 200:


```bash
curl "https://app.basketly.io/api/v1/integrations" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /getting-started` — Getting started

The organisation's setup checklist: shop connected, Meta receiver, a running flow to Meta, a product feed, a Meta ad account read, other platforms (optional) — each with its state counted from the organisation's records and the panel address where it is done. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `integrations:read`

Response 200:


```bash
curl "https://app.basketly.io/api/v1/getting-started" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /map` — Connection map

The whole shop setup read-only: where data comes from (shop sources, feeds, logins), what Basketly does with it (orders, products, conversions, segments, automations, templates) and where it goes (Meta receivers, GA4/TikTok/Pinterest/Klaviyo, Meta audiences and Klaviyo lists, feed formats, API, agents), with the health of every node and edge. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `integrations:read`

Response 200:


```bash
curl "https://app.basketly.io/api/v1/map" \
  -H "Authorization: Bearer $API_KEY"
```

### Orders

Shop orders with their lines and buyers.

#### `GET /orders` — List orders

Shop orders of a period (days in the organisation's zone, default the last 30), bought by default, with the Zamówienia filters and sorts. Values of one filter mean any of them, filters combine; a wrong value is a 400. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `orders:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD. |
| `to` | query |  | Last day YYYY-MM-DD (range up to 366 days). |
| `status` | query |  | Which orders: bought (default), unpaid, cancelled or all; a comma list means any of them. |
| `contact` | query |  | Orders of this person (all dates unless from/to are given). |
| `q` | query |  | Order number fragment, or the buyer's full e-mail or phone (matched by hash). |
| `source` | query |  | Comma list of shop source ids (facets.sources). |
| `shop_status` | query |  | The shop's own status name; repeat for several (facets.shop_statuses). |
| `status_type` | query |  | Comma list of new, open, closed, cancelled. |
| `paid` | query |  | The shop's paid flag. |
| `total_min` | query |  | Order value at least (in the order's currency). |
| `total_max` | query |  | Order value at most. |
| `platform` | query |  | Comma list of ad source platforms: meta, google_ads, microsoft_ads, tiktok, pinterest, openai_ads, email, comparison, other, direct, unknown (unknown includes orders without one). |
| `campaign` | query |  | Campaign key (a campaign id, or utm:<utm_campaign> — keys come from ad_facets); repeat for several. |
| `ad_source` | query |  | yes = a known ad platform, no = direct or unknown. |
| `meta_send` | query |  | Comma list of accepted, failed, waiting, none: what happened to the purchase sent to Meta. |
| `product` | query |  | A line whose name contains this text, or whose code, EAN or catalogue id equals it. |
| `country` | query |  | Comma list of delivery country codes. |
| `sort` | query |  | Order of the list. |
| `per_page` | query |  | Orders per page. |
| `page` | query |  | Page, 1 = first. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `orders` | object[] |  | Orders: id, number, placed_at, status, status_type (new, open, closed, cancelled), paid, bought, cancelled, currency, total, shipping, items, city, buyer {id, name}, source_name, source_kind (shoper, idosell, baselinker, woocommerce, shopify…), lines (line count), first_product {name, variant, content_id, quantity, product (record id or empty), image}, sends ([{destination meta \| ga4 \| tiktok \| pinterest \| klaviyo \| openai_ads \| google_ads \| microsoft_ads, status accepted \| failed \| waiting \| skipped}] — the worst outcome per destination), ad_source. `ad_source` = Basketly's own last-click reading of where the order came from, never a platform's attribution: platform (meta \| google_ads \| microsoft_ads \| tiktok \| pinterest \| openai_ads \| email \| comparison \| other \| direct \| unknown), method (click_id \| utm \| ids \| feed \| referrer \| none), link (server = tracking_id in the order, browser = thank-you page linkOrder, shop = the shop's landing data, none), deduced, campaign / adset / ad {id, name}, resolved (ids \| names \| empty), utm {source, medium, campaign, content, term, id}, click_at, clicks (click ids of the visit: fbc, gclid, gbraid, wbraid, msclkid, ttclid, ttp, epik, oppref, obref), referrer (host), landing (path), first_touch, feed {id, name, channel, product} (the served feed and catalogue product of the tagged link the visit landed on). |
| `ad_facets` | object |  | Orders of the selection (before the platform / campaign filters) per ad platform (`platforms`: platform, orders) and per campaign (`campaigns`: platform, key, name, orders; at most 100). |
| `facets` | object |  | The period's orders (all states, before the other filters) per shop (`sources`: id, name, kind, orders), shop status (`shop_statuses`: status, status_type, orders) and delivery country (`countries`: country, orders); at most 100 each. |
| `page_size` | integer |  | Orders per page (per_page). |
| `total` | integer |  | All matching orders. |
| `summary` | object |  | Bought orders, unpaid, cancelled, items, revenue per currency, average basket, buyers, new and returning buyers. |

```bash
curl "https://app.basketly.io/api/v1/orders" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /orders/{id}` — Get an order

One order with its lines, buyer, ad source and the destinations its purchase went to. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `orders:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/orders/abc123" \
  -H "Authorization: Bearer $API_KEY"
```

### Products

Products with their diagnosis and manual fixes.

#### `GET /products` — List products

Active products of the organisation or of one feed, 50 per page (or per_page), with their diagnosis, origin, Google category, fixes, the period's results and a sales trend. Values of one filter OR, filters AND. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `products:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `feed` | query |  | Only products of this feed. |
| `q` | query |  | Title, SKU, exact EAN or exact catalogue id. |
| `origin` | query |  | Comma list of shop source ids or feed ids the products come from (facets.origins). |
| `category` | query |  | Shop category, exact; repeat for several (facets.categories). |
| `brand` | query |  | Brand, exact; repeat for several (facets.brands). |
| `availability` | query |  | Comma list of in_stock, out_of_stock, preorder, backorder, unknown. |
| `price_min` | query |  | Lowest price a shopper pays (the sale price when there is one). |
| `price_max` | query |  | Highest price a shopper pays. |
| `score_min` | query |  | Lowest quality score 0–100. |
| `score_max` | query |  | Highest quality score 0–100. |
| `issue` | query |  | Comma list of problem codes; any of them matches. |
| `status` | query |  | Comma list of worst problems: error, warning, clean. |
| `fixes` | query |  | Comma list: auto (Basketly's automatic fixes repair a problem), manual (a person typed a fix), none. |
| `google` | query |  | Comma list: set (leaves the feeds with a Google category: a person's, its own or a sure automatic one), review (automatic, confidence under 60), none (Basketly picks one by the title when building the feed). |
| `exposure` | query |  | Comma list: none = not shown in catalogue ads in the period, low = under 100 impressions (not shown included), shown. |
| `sales` | query |  | yes = bought in the period, no = not bought. |
| `sort` | query |  | title (default), title_desc, score (worst first), score_desc, price, stock, issues, synced, or the period's impressions, clicks, ctr, units, revenue, views — most first; `_asc` = least first. |
| `days` | query |  | Statistics period: 7, 14, 30 (default) or 90 days. |
| `per_page` | query |  | Products per page: 10, 25, 50 (default) or 100. |
| `facets` | query |  | 1 adds `facets`: origins, categories, brands, availability, google and fixes with product counts. |
| `page` | query |  | Page, 1 = first. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `products` | object[] |  | Products: id, external_id, title, price, sale_price, availability, stock, score 0–100, worst problem, issues, errors, warnings, origin_name, origin_kind, google {id, path, by person\|own\|auto\|title\|none, confidence, review — an automatic or by-title pick under 60 to check}, fixes {auto, manual}, stats of the period, trend (bought units per day, per week over 30 days). |
| `total` | integer |  | All matching products. |
| `summary` | object |  | Counts of the selection: errors, warnings, average score, problems by code. |
| `per_page` | integer |  | Products per page. |
| `facets` | object |  | With facets=1: origins (id, name, kind, products), categories and brands (name, products; top 500), availability, google (set, review, none) and fixes (auto, manual) counts of the whole selection. |

```bash
curl "https://app.basketly.io/api/v1/products" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /products/performance` — Catalogue performance

Products not shown, barely shown and shown in catalogue ads, best sellers and most shown without a sale. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `products:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `days` | query |  | Statistics period: 7, 14, 30 (default) or 90 days. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/products/performance" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /products/{product}/stats` — Product results

One product in ads (Meta per-product breakdown), in the shop (Basketly script) and in sales (orders), per day. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `products:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `product` | path | yes | Product record id. |
| `days` | query |  | Statistics period: 7, 14, 30 (default) or 90 days. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/products/abc123/stats" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /products/{product}` — Get a product

The product record of the panel. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `products:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `product` | path | yes | Product record id. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/products/abc123" \
  -H "Authorization: Bearer $API_KEY"
```

#### `PUT /products/{product}/overrides` — Set manual fixes

Values typed by hand win over fixes, rules and flows in every feed. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `products:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `product` | path | yes | Product record id. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `overrides` | object | yes | Attribute → value typed by hand; they win in every feed and survive the next read. "" removes the attribute. Replaces all manual fixes (send the ones to keep). `id` cannot be changed. |

Response 200:


```bash
curl -X PUT "https://app.basketly.io/api/v1/products/abc123/overrides" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "overrides": {}
  }'
```

#### `GET /guidelines` — List guidelines

Vendor rules for product data with their sources and how many products break each. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `products:read`

Response 200:


```bash
curl "https://app.basketly.io/api/v1/guidelines" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /products/{product}/title-suggestions` — Suggest titles

Asks the AI (MiniMax) for 1–3 better titles following the title guidelines; only the product's own attributes are sent, every proposal is rechecked by Basketly's title checks. Nothing is saved. Uses the organisation's daily AI budget (429 when spent, also over 30 calls per hour). Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `products:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `product` | path | yes | Product record id. |

Response 200:


```bash
curl -X POST "https://app.basketly.io/api/v1/products/abc123/title-suggestions" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /meta-catalog/status` — Catalogue status of products

For up to 500 content ids: review status, availability, visibility and product set membership in the chosen Meta catalogues, with the catalogue reason the product gets no delivery. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `products:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `content_ids` | query | yes | Comma list of content ids (catalogue retailer_id), at most 500. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/meta-catalog/status" \
  -H "Authorization: Bearer $API_KEY"
```

### Feeds

Product feeds, their rules, flows and Google categories.

#### `POST /products/unseen-flow` — Feed of products not shown

Creates a flow feed of the products catalogue ads showed fewer than `below` times in the last `days` days. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `days` | integer | yes | Period: 7, 14, 30 or 90 days. One of: `7`, `14`, `30`, `90`. |
| `below` | number | yes | Impressions under which a product counts as barely shown (≥ 1). |

Response 200:


```bash
curl -X POST "https://app.basketly.io/api/v1/products/unseen-flow" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "days": 7,
    "below": 1
  }'
```

#### `GET /feeds` — List feeds

Every feed with its state and output address. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:read`

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `feeds` | object[] |  | Feeds as in GET /feeds/{id} without fields, plus `category_coverage` {covered, products}: products that leave the feed with a Google category (as on the Kategorie Google tab), null when it cannot be counted. |

```bash
curl "https://app.basketly.io/api/v1/feeds" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /feeds` — Create a feed

Like „Dodaj feed”: from an address (download queued), a shop, other feeds or as a flow (starts with the chosen feeds joined in the output, or with a whole canvas). Uploading a file stays in the panel. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | 2–120 characters. |
| `input` | string | yes | Where products come from. One of: `url`, `source`, `feeds`, `flow`. |
| `url` | string |  | input=url: the feed address (http/https). |
| `source` | string |  | input=source: a connected shop source id. |
| `refresh_hours` | integer |  | input=url: download every 0 (never), 1, 3, 6, 12 or 24 hours. One of: `0`, `1`, `3`, `6`, `12`, `24`. |
| `parents` | string[] |  | input=feeds or flow: 1–10 feeds with their own products, in priority order. |
| `graph` | object |  | input=flow: a whole canvas (as in PUT /feeds/{id}/flow) instead of parents. |
| `output` | string |  | Served format: "" Google/Meta XML, ceneo, csv, openai (ChatGPT Ads CSV). One of: ``, `ceneo`, `csv`, `openai`. |

Response 200:


```bash
curl -X POST "https://app.basketly.io/api/v1/feeds" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Anna Nowak",
    "input": "url",
    "url": "https://example.com/webhooks"
  }'
```

#### `GET /feeds/{id}` — Get a feed

A feed with its attributes; a flow with its canvas. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/feeds/abc123" \
  -H "Authorization: Bearer $API_KEY"
```

#### `PATCH /feeds/{id}` — Update a feed

Name, address, download interval, automatic fixes, parents, output format, link tagging (fields of link_tagging left out stay). Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string |  | 2–120 characters. |
| `url` | string |  | input=url only. |
| `refresh_hours` | integer |  | input=url only. One of: `0`, `1`, `3`, `6`, `12`, `24`. |
| `autofix` | boolean |  | Automatic fixes on or off (feeds with rules). |
| `parents` | string[] |  | input=feeds only. |
| `output` | string |  | Served format (a flow's output block follows). One of: ``, `ceneo`, `csv`, `openai`. |
| `link_tagging` | LinkTagging |  |  |
| `link_tagging.enabled` | boolean |  | Tag served product links (the last step before serving). New feeds start on; feeds made before the setting stay off until switched on. |
| `link_tagging.preset` | string |  | marker = only Basketly's bk_f/bk_pid; meta, google, ceneo = channel parameters; custom = your own. A named preset without `params` takes its own parameters. One of: `marker`, `meta`, `google`, `ceneo`, `custom`. |
| `link_tagging.params` | string |  | Query template added to links, e.g. utm_source=facebook&utm_content={id}. Placeholders {id}, {item_group_id}, {brand}, {product_type}, {feed} (the feed's name as a slug) are filled from the product and URL-encoded; any other {…} or {{…}} is a channel macro (Google ValueTrack, Meta) served untouched. bk_f/bk_pid are reserved. Parameters a link already has are never replaced. |
| `link_tagging.ads_redirect` | boolean |  | Google: the parameters go only into ads_redirect (Shopping ad clicks; built from link when the product has none), link and mobile_link get only the marker. Ignored for Ceneo. |

Response 200:


```bash
curl -X PATCH "https://app.basketly.io/api/v1/feeds/abc123" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Anna Nowak",
    "url": "https://example.com/webhooks"
  }'
```

#### `POST /feeds/{id}/link-tagging/preview` — Preview link tagging

Tags one served product's links with a draft setting (fields left out = the saved ones). Nothing is saved. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean |  | Tag served product links (the last step before serving). New feeds start on; feeds made before the setting stay off until switched on. |
| `preset` | string |  | marker = only Basketly's bk_f/bk_pid; meta, google, ceneo = channel parameters; custom = your own. A named preset without `params` takes its own parameters. One of: `marker`, `meta`, `google`, `ceneo`, `custom`. |
| `params` | string |  | Query template added to links, e.g. utm_source=facebook&utm_content={id}. Placeholders {id}, {item_group_id}, {brand}, {product_type}, {feed} (the feed's name as a slug) are filled from the product and URL-encoded; any other {…} or {{…}} is a channel macro (Google ValueTrack, Meta) served untouched. bk_f/bk_pid are reserved. Parameters a link already has are never replaced. |
| `ads_redirect` | boolean |  | Google: the parameters go only into ads_redirect (Shopping ad clicks; built from link when the product has none), link and mobile_link get only the marker. Ignored for Ceneo. |

Response 200:


```bash
curl -X POST "https://app.basketly.io/api/v1/feeds/abc123/link-tagging/preview" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "preset": "marker",
    "params": "example",
    "ads_redirect": true
  }'
```

#### `PUT /feeds/{id}/rules` — Save feed rules

Replaces the rules of a feed (not a flow); the served feed uses them at once. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `rules` | object[] | yes | Ordered rules: id, name, off, match (""/all/any), when [{field, op, value, to}], then and else [{type, field, value, with, from}]. Replaces all rules. Refused for flows. |

Response 200:


```bash
curl -X PUT "https://app.basketly.io/api/v1/feeds/abc123/rules" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      {}
    ]
  }'
```

#### `PUT /feeds/{id}/flow` — Save a feed flow

Replaces the canvas of a flow; the served feed and its format follow at once. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `graph` | object | yes | The canvas {nodes: [{id, type, label, config, x, y}], edges: [{from, port, to}]}. Node types: input {feed, processed}, filter and branch {match, when}, change {actions}, autofix, categories, output {format}. Ports: out; branch yes/no. Exactly one output; every node on a path from an input to the output; no loops; each feed once. A refused canvas answers 400 with `node_id`. |

Response 200:


```bash
curl -X PUT "https://app.basketly.io/api/v1/feeds/abc123/flow" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "graph": {}
  }'
```

#### `POST /feeds/{id}/preview` — Preview a feed

Runs unsaved rules or an unsaved canvas over every product and returns what would change. Nothing is saved. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `rules` | object[] |  | Rules to try (feeds with rules). |
| `graph` | object |  | A canvas to try (flows). |

Response 200:


```bash
curl -X POST "https://app.basketly.io/api/v1/feeds/abc123/preview" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      {}
    ],
    "graph": {}
  }'
```

#### `POST /feeds/{id}/fetch` — Download now

Queues a download of the feed (feeds made from feeds and flows: of their inputs). Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Response 204.

```bash
curl -X POST "https://app.basketly.io/api/v1/feeds/abc123/fetch" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b"
```

#### `GET /feeds/{id}/categories` — Feed categories

Shop categories with the Google category each gets — a person's, chosen automatically with its confidence, or by each product's title — and suggestions. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/feeds/abc123/categories" \
  -H "Authorization: Bearer $API_KEY"
```

#### `PUT /feeds/{id}/categories` — Save feed categories

Saves the Google category and own name per shop category; entries without auto are a person's and win over the automatic choice. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `categories` | object[] | yes | [{name, google, own, auto, confidence, broad}] — shop category, Google category number (0 = none), own name ("" = keep); without `auto` the Google category is a person's choice and is never changed automatically; with `auto: true` it stays Basketly's to recalculate. Replaces the whole list. |

Response 200:


```bash
curl -X PUT "https://app.basketly.io/api/v1/feeds/abc123/categories" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "categories": [
      {}
    ]
  }'
```

#### `PATCH /feeds/{id}/categories` — Change some feed categories

Changes only the named rows: a person's Google category, confirming the automatic one, back to automatic, own name. Answers with the changed rows and the summary of all rows. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `changes` | object[] | yes | 1–500 rows to change: [{name, google?, confirm?, restore?, own?}] — `name` = shop category as GET gives it; one of `google` (a Google category id: a person's choice, wins from then on), `confirm: true` (the current automatic choice becomes a person's) or `restore: true` (back to Basketly's automatic choice, chosen again), and/or `own` (own name, "" removes it). Other rows stay as they are. |

Response 200:


```bash
curl -X PATCH "https://app.basketly.io/api/v1/feeds/abc123/categories" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "changes": [
      {}
    ]
  }'
```

#### `GET /google-categories` — Search Google categories

Finds categories of Google's product taxonomy by Polish or English words or by id (for the `google` of feed categories). Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `q` | query |  | Words or a category id, up to 100 characters. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/google-categories" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /google-taxonomy` — Google taxonomy version

Which version of Google's product taxonomy Basketly uses, how many categories it has and when Google was last checked (weekly by itself). Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:read`

Response 200:


```bash
curl "https://app.basketly.io/api/v1/google-taxonomy" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /google-taxonomy/refresh` — Check Google taxonomy now

Queues a background job that downloads Google's taxonomy (Polish and English) and puts a valid newer version in use. Owner or admin of the organisation only. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:write`

Response 202:


```bash
curl -X POST "https://app.basketly.io/api/v1/google-taxonomy/refresh" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /feeds/{id}/categories/auto` — Recalculate automatic categories

Chooses the Google category of every shop category a person has not chosen again (Basketly does it by itself after every product read; this redoes the existing choices too). A person's choices stay. Answers like GET. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `feeds:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Response 200:


```bash
curl -X POST "https://app.basketly.io/api/v1/feeds/abc123/categories/auto" \
  -H "Authorization: Bearer $API_KEY"
```

### Image templates

Image templates for catalogue ads.

#### `GET /templates` — List image templates

Templates a feed rule or flow block can draw product images with. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `templates:read`

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `templates` | object[] |  | Templates: id, name, format, updated. |

```bash
curl "https://app.basketly.io/api/v1/templates" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /templates/performance` — Template results

Ad results of the products of feeds that use each template. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `templates:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/templates/performance" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /templates/{id}` — Get an image template

One template with its layers. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `templates:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/templates/abc123" \
  -H "Authorization: Bearer $API_KEY"
```

### Reports

Campaign and creative numbers.

#### `GET /campaigns/report` — Campaign report

Spend, purchases, purchase value, cost per purchase and ROAS per campaign, ad set or ad — the numbers of the Kampanie page.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (range up to 366 days). |
| `level` | query |  | Row level. |
| `campaign` | query |  | Only this campaign (for adset/ad level). |
| `adset` | query |  | Only this ad set (ad level). |
| `ad_account` | query |  | Only this Meta ad account (act_… or the bare number); the shop's totals then come only when it is the organisation's only account. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string |  | First day (organisation time zone). |
| `to` | string |  | Last day. |
| `level` | string |  | Row level. One of: `campaign`, `adset`, `ad`. |
| `totals` | object[] |  | Totals per currency: `current` and the `previous` period of the same length. Without a campaign/adset filter they also carry the shop's bought orders of the period (`shop_orders`, `shop_revenue`) and `mer` = shop revenue / spend. Totals and rows also carry `baskety_orders`, `baskety_revenue`, `baskety_currency`: bought orders whose ad source (Basketly's last-click reading of the buyer's visits) is a Meta campaign / ad set / ad of the row — a different count from Meta's purchases, never added to them. |
| `rows` | array Each row also names its Meta ad account: `ad_account_id` and `ad_account_name` (empty until read). |  | One row per campaign / ad set / ad with `status` (Meta's effective status), `optimization` (what its ad sets optimize for: `label` in Polish — Meta's standard events named in Polish, e.g. „Zdarzenie: Zakup (zbiór …)” — `event` and, for an event goal, `event_name` = Meta's technical name such as Purchase, `short` = the goal in words without the dataset for a badge, e.g. „Zakup”) and `metrics`: spend, purchases, purchase_value, cost_per_purchase, roas (purchases and their value as Meta reports them); delivery summed over the period's days — impressions, clicks (all clicks), cpm (spend / impressions × 1000), link_clicks, ctr (link clicks / impressions, a fraction), cpc (spend / link click; link figures count only days read with link clicks, null before), video plays_3s, thruplays, p25, p50, p75, p100, hook_rate (3 s plays / impressions) and hold_rate (ThruPlays / 3 s plays; null for images); costs and ratios are null when the divisor is 0. Reach and frequency are not offered (daily reach cannot be added up). Plus `baskety_orders` / `baskety_revenue` from the orders' ad source. Totals carry the same delivery fields. Każdy wiersz podaje też konto reklamowe Mety: `ad_account_id` i `ad_account_name` (puste, dopóki nie odczytane). |
| `syncs` | object[] |  | Spend sync state per ad account: `ad_account_id` (act_…), `ad_account_name` (the account's name in Meta; empty until the next spend read stores it), `currency`, `last_success`, `last_error` and `job` while it waits or is read. |
| `comparison` | object |  | How the totals compare with the previous period: {mode whole\|same_hour, until, previous_until, spend_without_today, spend_to, previous_spend_to}. A period that ends today compares Meta's figures and MER over whole days (today left out of both periods) and the shop's and Basketly's orders up to the same hour; each total's `compare` holds the current figures to set against `previous` (`current` still counts today). |
| `shops` | string[] |  | Kinds of the connected shops (shoper, idosell, baselinker…) whose orders give the shop's numbers. |

```bash
curl "https://app.basketly.io/api/v1/campaigns/report" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /creatives/report` — Creative report

Metrics per creative group with weekly momentum and the fatigue rule — the numbers of the Kreacje page.

Scope: `creatives:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD. |
| `to` | query |  | Last day YYYY-MM-DD. |
| `format` | query |  | Creative format. |
| `campaign` | query |  | Meta campaign id. |
| `ad_account` | query |  | Ad account id. |
| `tag` | query |  | Tag filter `dimension:value` (hook, creator, offer). |
| `weeks` | query |  | Weeks of momentum, 4–8. |
| `channel` | query |  | meta (default) = Meta's creative groups; google_ads, microsoft_ads, tiktok, pinterest or openai_ads = that platform's ads (and Google's assets) with the platform's figures — the answer is then a ChannelCreatives object. |
| `active` | query |  | 1 = only active ads. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string |  | First day. |
| `to` | string |  | Last day. |
| `weeks` | array[] |  | Week ranges used for momentum. |
| `rows` | array `ad_accounts` lists the Meta ad account ids of the group and `ad_account_names` their names in the same order (empty until read). |  | One row per creative group (same video, image or creative) with metrics, weekly trend, fatigue verdict and tags. `baskety_orders` / `baskety_revenue` (in `baskety_currency`): bought shop orders of the period whose ad source (Basketly's last-click reading of the buyer's visits) is one of the creative's ads — a different count from Meta's purchases, never added to them. `ad_accounts` to identyfikatory kont reklamowych Mety grupy, a `ad_account_names` ich nazwy w tej samej kolejności (puste, dopóki nie odczytane). |
| `totals` | object[] |  | Totals per currency. |
| `compare_totals` | object[] |  | The current totals to set against `previous_totals`: when the period ends today, the same creatives over whole days without today (Meta reports per day); otherwise equal to `totals`. Empty for a period of today only (nothing to compare). |
| `comparison` | object |  | How the totals compare with the previous period: {mode whole\|same_hour, until, previous_until, spend_without_today, spend_to, previous_spend_to}; previous_totals then leave out the matching last day. |
| `previous_totals` | object[] |  | Totals per currency of the same filtered creatives over the period of the same length just before `from` (the comparison under the Kreacje summary; when the period ends today, without its last day — see `comparison`). |
| `syncs` | object[] |  | Spend sync state per ad account: `ad_account_id` (act_…), `ad_account_name` (the account's name in Meta; empty until the next spend read stores it), `currency`, `last_success`, `last_error` and `job` while it waits or is read. |

```bash
curl "https://app.basketly.io/api/v1/creatives/report" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/sales` — Sales report

The shop's sales of a period next to the previous one: orders, revenue in the main currency, average order, buyers new/returning, Meta spend, purchases, ROAS, MER, spend per order, purchases accepted for sending, Google Ads / Microsoft Ads spend as each reports it, per day, the top 10 products and `verdict` — one sentence about the period (start here when reading Raporty). Sources are never mixed. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/sales" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/channels` — Channels and the shop

Every channel of the period next to the shop: blended spend, MER and cost per order over all channels, each channel's figures as its platform reports them (never added across platforms), Basketly's last-click orders per source (shares add up to 100%), the sum of the platforms' own conversions next to the shop's orders, per day revenue and spend per channel — Raporty → Sprzedaż. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/channels" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/product-channels` — Products × channels

Every product against every channel that shows it — Raporty → Kanały × produkty: per channel impressions, clicks, spend, CTR and the platform's own per-product conversions where the platform reports them (Meta does not: null), Basketly's last-click orders containing the product, units per 100 clicks and the channel's impression share; the shop's units, revenue, views, carts, stock and days of cover; best channel, spend split and source split. Never adds platform conversions together. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |
| `channel` | query |  | Only products with figures in this channel; spend and orders sorting read it. |
| `sort` | query |  | Sort order (add _asc for ascending). |
| `page` | query |  | Page from 1. |
| `per_page` | query |  | Rows per page: 10, 25, 50 or 100. |
| `content_id` | query |  | One product by its catalogue id. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/product-channels" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /channels/report` — Channel report

Without `channel` every channel's totals (Kampanie → Wszystkie kanały); with `channel` that channel's campaigns (ad groups, ads where read) with the platform's figures and Basketly's last-click orders — one vocabulary for Meta, Google Ads, Microsoft Ads, TikTok and Pinterest. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |
| `channel` | query |  | One paid channel; empty = every channel's totals. |
| `level` | query |  | Row level. |
| `parent` | query |  | The campaign of ad groups / the ad group of ads. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/channels/report" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /manual-spend` — Costs outside ad platforms

Costs the team typed in (comparison sites, e-mail tool, agency, other) per month. Each month's amount is spread evenly over its days and counted in the blended spend, MER and cost per order (GET /reports/channels: lines with `manual`), never as a platform's figures. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Response 200:


```bash
curl "https://app.basketly.io/api/v1/manual-spend" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /manual-spend` — Add a cost outside ad platforms

Adds one month's cost. Nothing is sent anywhere; only the blended spend and MER change. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `channel` | string |  | comparison, email, agency or other. One of: `comparison`, `email`, `agency`, `other`. |
| `name` | string |  | What the cost is, up to 80 characters (required for other). |
| `month` | string |  | YYYY-MM. |
| `amount` | number |  | More than 0, at most 1 000 000 000. |
| `currency` | string |  | ISO code (default: the shop's main currency). |
| `note` | string |  | Up to 500 characters. |

Response 201:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Entry id. |
| `channel` | string |  | Kind of cost: comparison (comparison sites), email (e-mail tool), agency, other. One of: `comparison`, `email`, `agency`, `other`. |
| `name` | string |  | What the cost is (required for other). |
| `month` | string |  | Month YYYY-MM; the amount is spread evenly over its days. |
| `amount` | number |  | The month's amount. |
| `currency` | string |  | ISO currency code. |
| `note` | string |  | Note. |
| `created` | string (date-time) |  | Created. |
| `updated` | string (date-time) |  | Changed. |

```bash
curl -X POST "https://app.basketly.io/api/v1/manual-spend" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Anna Nowak",
    "currency": "PLN"
  }'
```

#### `PATCH /manual-spend/{id}` — Change a cost outside ad platforms

Changes any field of one entry. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `channel` | string |  | comparison, email, agency or other. One of: `comparison`, `email`, `agency`, `other`. |
| `name` | string |  | What the cost is. |
| `month` | string |  | YYYY-MM. |
| `amount` | number |  | The month's amount. |
| `currency` | string |  | ISO code. |
| `note` | string |  | Note. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Entry id. |
| `channel` | string |  | Kind of cost: comparison (comparison sites), email (e-mail tool), agency, other. One of: `comparison`, `email`, `agency`, `other`. |
| `name` | string |  | What the cost is (required for other). |
| `month` | string |  | Month YYYY-MM; the amount is spread evenly over its days. |
| `amount` | number |  | The month's amount. |
| `currency` | string |  | ISO currency code. |
| `note` | string |  | Note. |
| `created` | string (date-time) |  | Created. |
| `updated` | string (date-time) |  | Changed. |

```bash
curl -X PATCH "https://app.basketly.io/api/v1/manual-spend/abc123" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Anna Nowak",
    "currency": "PLN"
  }'
```

#### `DELETE /manual-spend/{id}` — Remove a cost outside ad platforms

Removes one entry for good. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `deleted` | string | yes | Id of the removed entry. |

```bash
curl -X DELETE "https://app.basketly.io/api/v1/manual-spend/abc123" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/insights` — Report conclusions

Every rule-based conclusion of the period (sales, product trajectories, product magnets, stock, tagged feeds) with the numbers behind it and a suggested action, most important first. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |
| `channel` | query |  | Only conclusions about this channel (the `channels` counts still cover every conclusion). |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/insights" \
  -H "Authorization: Bearer $API_KEY"
```

#### `PUT /reports/insights/{key}` — Mark a conclusion

Marks one conclusion for the key's creator only, like the ⋯ menu on Raporty: done, snoozed until a day, dismissed (does not apply) or open again. Other members keep seeing it; repeating the same state changes nothing. It changes no organisation data, so the read scope of the reports is enough. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `key` | path | yes | The conclusion's `key` from GET /reports/insights. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `state` | string | yes | done, snoozed, dismissed, or open to bring the conclusion back. One of: `open`, `done`, `snoozed`, `dismissed`. |
| `until` | string (date) |  | snoozed only: the day the conclusion comes back, YYYY-MM-DD (tomorrow up to a year; default 7 days from today). |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string |  | The conclusion's key. |
| `state` | string |  | The state now. One of: `open`, `done`, `snoozed`, `dismissed`. |
| `until` | string |  | The day a snooze ends, or empty. |

```bash
curl -X PUT "https://app.basketly.io/api/v1/reports/insights/abc123" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "open"
  }'
```

#### `GET /reports/products` — Product trajectories

Each product's impressions, clicks and spend in Meta catalogue ads next to its units and revenue from shop orders, day by day (week by week above 31 days), with trends, week-over-week change, pattern and conclusions — every product with impressions, units or views in the period (up to 5000), searched, filtered, sorted and paged on the server (`total`, `listed`, `matched`, `capped`). Clicks and sales are side by side, not attributed. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |
| `bucket` | query |  | day or week (default: day up to 31 days, week above). |
| `q` | query |  | Text in the product's title or catalogue id; searches every product of the period, not only the first page. |
| `pattern` | query |  | Only these patterns (repeat or separate with commas); `patterns` in the answer still counts every product. |
| `sort` | query |  | Sort order (add _asc for ascending). |
| `page` | query |  | Page from 1. |
| `per_page` | query |  | Products per page, 1–500. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/products" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/products/detail` — Product trajectory

One product's trajectory, every day of the period, with its conclusions. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | query | yes | Catalogue product id (content_id). |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/products/detail" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/magnets` — Product magnets

Products people look at or click but rarely buy while the same people buy other products — from linked visits of the shop script and from orders of catalogue ads. Co-occurrence, not proof of cause. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/magnets" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/unseen` — Products not shown

Products in the catalogue (a Basketly feed Meta reads, or every active product without one), available and without Meta errors, that Meta catalogue ads did not show in the period — with the reason, days in the feed, shop demand and stock value. One page of the list; summary and conclusions cover the whole catalogue. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |
| `reason` | query |  | Comma list: not_in_feed, excluded_by_rule, out_of_stock, meta_error_check, meta_rejected, meta_pending, pending_sync, not_in_active_set, not_picked. |
| `exposure` | query |  | Comma list of none, low, shown (default none,low). |
| `eligible` | query |  | yes or no. |
| `feed` | query |  | Feed id: products served or left out by it. |
| `category` | query |  | Shop category, exact (repeatable). |
| `brand` | query |  | Brand, exact (repeatable). |
| `availability` | query |  | Comma list: in_stock, out_of_stock, preorder, backorder, unknown. |
| `sales` | query |  | yes or no: units sold in the period. |
| `views` | query |  | yes or no: shop views in the period. |
| `q` | query |  | Title, SKU or catalogue id. |
| `sort` | query |  | stock_value (default), units, views, revenue, impressions, days_in_feed, last_impression, title; _asc reverses. |
| `page` | query |  | Page, from 1. |
| `per_page` | query |  | 10, 25 (default), 50 or 100. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/unseen" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/stock-ads` — Sold out but shown

Products with impressions and spend on days the shop said out of stock (from the availability changes of the nightly product reads), the catalogue's lag, and products back in stock for at least 3 days without an impression since. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/stock-ads" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/concentration` — Delivery concentration

How unevenly Meta spreads catalogue-ad impressions over the products of a scope (all, ad account, campaign, ad set or ad): rank table, top shares, Gini, HHI and effective products, weekly series, before/after sell-outs of top products and conclusions. Meta purchases only for the whole scope, never per product. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `scope` | query |  | all (default), account, campaign, adset or ad. |
| `id` | query |  | Id of the ad account (act_…), campaign, ad set or ad; required unless scope=all. The answer's `options` list them. |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/concentration" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/stock` — Stock signals

Which products run out before a restock can arrive: velocity of the last 7/14/28 days, days of cover, stockout date, reorder point, suggested quantity, level against the lead time and demand from catalogue ads and the shop script. Stock is the shop's number at the last read, plus Basketly's labelled estimate; the forecast is linear. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |
| `level` | query |  | Comma list: sold_out, critical, warning, watch, ok, overstock, thin. |
| `rising` | query |  | 1 = only products with rising clicks or carts. |
| `in_ads` | query |  | 1 = only products with impressions in the last 7 days. |
| `brand` | query |  | Only this brand. |
| `category` | query |  | Category contains this text. |
| `source` | query |  | Shop kind or feed. |
| `lead_time` | query |  | What-if lead time in days (1–180); default: the organisation's setting. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/stock" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/feed-orders` — Orders from tagged feeds

Orders whose visit landed on a tagged catalogue link, per feed and per landing product, with orders per 100 clicks of the feed's products. Basketly's own reading from the shop script (needs link tagging and the script), not Meta's purchases. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/feed-orders" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /reports/product-sets` — Product set health

Per product set of the chosen Meta catalogues: product count, share in stock, approved, rejected and pending, spend of the ad sets promoting it and warnings; plus Meta's catalogue diagnostics. Needs the optional catalogue connection (catalog_management); without it `connected` is false and the lists are empty. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD (up to 366 days). |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/reports/product-sets" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /meta-catalog` — Meta catalogue connection

Whether the organisation connected a Meta catalogue (optional), which catalogues it chose and how their last read went. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Response 200:


```bash
curl "https://app.basketly.io/api/v1/meta-catalog" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /campaigns/products` — Products in catalogue ads

Per-product results of a campaign, ad group or ad from the platform's per-product report (Meta by default; Google Ads, Microsoft Ads, TikTok with `platform`), with the product's shop sales. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `campaigns:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query |  | First day YYYY-MM-DD (default: 30 days ago). |
| `to` | query |  | Last day YYYY-MM-DD. |
| `platform` | query |  | meta (default), google_ads, microsoft_ads or tiktok. |
| `campaign` | query |  | Only this campaign. |
| `adgroup` | query |  | Only this ad group (Google Ads, Microsoft Ads). |
| `ad` | query |  | Only this ad. |

Response 200:


```bash
curl "https://app.basketly.io/api/v1/campaigns/products" \
  -H "Authorization: Bearer $API_KEY"
```

### Automations

The organisation's automations and their state.

#### `GET /automations` — List automations

The organisation's automations: name, on/off, trigger type, version, author, last run, failed steps of the last 24 hours, runs, sends and failed steps of the last 30 days, runs in progress and what each can send. Editing and switching stay in the panel. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `automations:read`

Response 200:


```bash
curl "https://app.basketly.io/api/v1/automations" \
  -H "Authorization: Bearer $API_KEY"
```

### Self-check

The organisation's self-check.

#### `GET /diagnostics` — Last self-check

The last self-check run with every check. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `diagnostics:read`

Response 200:


```bash
curl "https://app.basketly.io/api/v1/diagnostics" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /diagnostics/run` — Run the self-check

Like „Sprawdź teraz”: queues a run (or starts a waiting one at once). Read the result with GET /diagnostics. Runs as the member who created the key: their role applies, `scope=mine` means their own work.

Scope: `diagnostics:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Response 202:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `job` | object |  | The job (number, status). |

```bash
curl -X POST "https://app.basketly.io/api/v1/diagnostics/run" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b"
```

### Background jobs

Background work.

#### `GET /jobs` — List background jobs

Order reads, spend syncs, sheet syncs and other background work of the organisation.

Scope: `jobs:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `limit` | query |  | Page size, 1–100 (default 25). |
| `cursor` | query |  | `next_cursor` of the previous page. |
| `order` | query |  | Sort by `updated_at` (then id): `desc` newest first (default) or `asc`. |
| `updated_since` | query |  | Only records changed at or after this ISO 8601 time. |
| `status` | query |  | Job status. |
| `kind` | query |  | Job kinds, comma-separated (the `kind` field, e.g. `shoper_orders,feed_fetch`). |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | Job[] | yes |  |
| `data[].number` | integer |  | Job number within the organisation. |
| `data[].kind` | string |  | Job kind, e.g. ad_spend, shoper_orders, sheets_sync. |
| `data[].label` | string |  | Description. |
| `data[].status` | string |  | Status. One of: `queued`, `running`, `done`, `failed`, `cancelled`. |
| `data[].priority` | string |  | `user` or `background`. |
| `data[].trigger` | string |  | What started it. |
| `data[].progress` | object |  | `done` count and `text`. |
| `data[].attempts` | integer |  | Attempts. |
| `data[].error` | string |  | Error text of a failed job. |
| `data[].summary` | string |  | Result summary. |
| `data[].run_after` | string (date-time) \| null |  | Not before. |
| `data[].started_at` | string (date-time) \| null |  | Start. |
| `data[].finished_at` | string (date-time) \| null |  | End. |
| `data[].created` | string (date-time) |  | Creation time. |
| `data[].updated_at` | string (date-time) |  | Last change. |
| `next_cursor` | string \| null | yes | Pass as `cursor` to get the next page; null on the last page. |
| `has_more` | boolean | yes | True when another page exists. |

```bash
curl "https://app.basketly.io/api/v1/jobs" \
  -H "Authorization: Bearer $API_KEY"
```

#### `GET /jobs/{number}` — Get a job

One job by its number.

Scope: `jobs:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `number` | path | yes | Job number. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `number` | integer |  | Job number within the organisation. |
| `kind` | string |  | Job kind, e.g. ad_spend, shoper_orders, sheets_sync. |
| `label` | string |  | Description. |
| `status` | string |  | Status. One of: `queued`, `running`, `done`, `failed`, `cancelled`. |
| `priority` | string |  | `user` or `background`. |
| `trigger` | string |  | What started it. |
| `progress` | object |  | `done` count and `text`. |
| `attempts` | integer |  | Attempts. |
| `error` | string |  | Error text of a failed job. |
| `summary` | string |  | Result summary. |
| `run_after` | string (date-time) \| null |  | Not before. |
| `started_at` | string (date-time) \| null |  | Start. |
| `finished_at` | string (date-time) \| null |  | End. |
| `created` | string (date-time) |  | Creation time. |
| `updated_at` | string (date-time) |  | Last change. |

```bash
curl "https://app.basketly.io/api/v1/jobs/42" \
  -H "Authorization: Bearer $API_KEY"
```

### Webhooks

Events pushed to your server.

#### `GET /webhooks` — List webhooks

Webhook subscriptions of the organisation (up to 10).

Scope: `webhooks:read`

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | Webhook[] | yes |  |
| `data[].id` | string |  | Webhook id. |
| `data[].url` | string |  | HTTPS endpoint that receives POSTs. |
| `data[].description` | string |  | Your note. |
| `data[].events` | string[] |  | Subscribed events. One of: `contact.created`, `delivery.failed`. |
| `data[].active` | boolean |  | False pauses deliveries. |
| `data[].last_status` | integer |  | HTTP status of the last attempt (0 = no answer). |
| `data[].last_error` | string |  | Last error. |
| `data[].last_delivery_at` | string (date-time) \| null |  | Last attempt. |
| `data[].consecutive_failures` | integer |  | Failed attempts in a row. |
| `data[].created_at` | string (date-time) |  | Creation time. |
| `data[].updated_at` | string (date-time) |  | Last change. |
| `next_cursor` | string \| null | yes | Pass as `cursor` to get the next page; null on the last page. |
| `has_more` | boolean | yes | True when another page exists. |

```bash
curl "https://app.basketly.io/api/v1/webhooks" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /webhooks` — Create a webhook

Subscribes an HTTPS URL to events. The signing `secret` is returned only here.

Scope: `webhooks:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | HTTPS URL (no credentials, no private or local addresses). Max 2000 characters. |
| `events` | string[] | yes | Events to receive. One of: `contact.created`, `delivery.failed`. |
| `description` | string |  | Your note. Max 200 characters. |

Response 201:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Webhook id. |
| `url` | string |  | HTTPS endpoint that receives POSTs. |
| `description` | string |  | Your note. |
| `events` | string[] |  | Subscribed events. One of: `contact.created`, `delivery.failed`. |
| `active` | boolean |  | False pauses deliveries. |
| `last_status` | integer |  | HTTP status of the last attempt (0 = no answer). |
| `last_error` | string |  | Last error. |
| `last_delivery_at` | string (date-time) \| null |  | Last attempt. |
| `consecutive_failures` | integer |  | Failed attempts in a row. |
| `created_at` | string (date-time) |  | Creation time. |
| `updated_at` | string (date-time) |  | Last change. |
| `secret` | string | yes | Signing secret (`whsec_…`). Shown only in this response — store it. |

```bash
curl -X POST "https://app.basketly.io/api/v1/webhooks" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks",
    "events": [
      "contact.created",
      "delivery.failed"
    ]
  }'
```

#### `GET /webhooks/{id}` — Get a webhook

One subscription with its last result.

Scope: `webhooks:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Webhook id. |
| `url` | string |  | HTTPS endpoint that receives POSTs. |
| `description` | string |  | Your note. |
| `events` | string[] |  | Subscribed events. One of: `contact.created`, `delivery.failed`. |
| `active` | boolean |  | False pauses deliveries. |
| `last_status` | integer |  | HTTP status of the last attempt (0 = no answer). |
| `last_error` | string |  | Last error. |
| `last_delivery_at` | string (date-time) \| null |  | Last attempt. |
| `consecutive_failures` | integer |  | Failed attempts in a row. |
| `created_at` | string (date-time) |  | Creation time. |
| `updated_at` | string (date-time) |  | Last change. |

```bash
curl "https://app.basketly.io/api/v1/webhooks/abc123" \
  -H "Authorization: Bearer $API_KEY"
```

#### `PATCH /webhooks/{id}` — Update a webhook

Changes URL, events, description or pauses it; resuming resets the failure count.

Scope: `webhooks:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Request body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string |  | New URL. |
| `events` | string[] |  | New event list. One of: `contact.created`, `delivery.failed`. |
| `description` | string |  | Your note. |
| `active` | boolean |  | Pause or resume. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Webhook id. |
| `url` | string |  | HTTPS endpoint that receives POSTs. |
| `description` | string |  | Your note. |
| `events` | string[] |  | Subscribed events. One of: `contact.created`, `delivery.failed`. |
| `active` | boolean |  | False pauses deliveries. |
| `last_status` | integer |  | HTTP status of the last attempt (0 = no answer). |
| `last_error` | string |  | Last error. |
| `last_delivery_at` | string (date-time) \| null |  | Last attempt. |
| `consecutive_failures` | integer |  | Failed attempts in a row. |
| `created_at` | string (date-time) |  | Creation time. |
| `updated_at` | string (date-time) |  | Last change. |

```bash
curl -X PATCH "https://app.basketly.io/api/v1/webhooks/abc123" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks",
    "events": [
      "contact.created",
      "delivery.failed"
    ],
    "active": false
  }'
```

#### `DELETE /webhooks/{id}` — Delete a webhook

Removes the subscription and its queued deliveries.

Scope: `webhooks:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |

Response 204.

```bash
curl -X DELETE "https://app.basketly.io/api/v1/webhooks/abc123" \
  -H "Authorization: Bearer $API_KEY"
```

#### `POST /webhooks/{id}/test` — Send a test ping

Queues a signed `ping` event to check your endpoint and signature verification.

Scope: `webhooks:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Response 202:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `delivery_id` | string |  | Queued delivery of a `ping` event. |

```bash
curl -X POST "https://app.basketly.io/api/v1/webhooks/abc123/test" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b"
```

#### `POST /webhooks/{id}/rotate-secret` — Rotate the signing secret

Replaces the signing secret at once; the new `secret` is returned only here. Deliveries still queued are signed with the new secret.

Scope: `webhooks:write`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `Idempotency-Key` | header |  | Unique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with `Idempotent-Replayed: true`. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string |  | Webhook id. |
| `url` | string |  | HTTPS endpoint that receives POSTs. |
| `description` | string |  | Your note. |
| `events` | string[] |  | Subscribed events. One of: `contact.created`, `delivery.failed`. |
| `active` | boolean |  | False pauses deliveries. |
| `last_status` | integer |  | HTTP status of the last attempt (0 = no answer). |
| `last_error` | string |  | Last error. |
| `last_delivery_at` | string (date-time) \| null |  | Last attempt. |
| `consecutive_failures` | integer |  | Failed attempts in a row. |
| `created_at` | string (date-time) |  | Creation time. |
| `updated_at` | string (date-time) |  | Last change. |
| `secret` | string | yes | Signing secret (`whsec_…`). Shown only in this response — store it. |

```bash
curl -X POST "https://app.basketly.io/api/v1/webhooks/abc123/rotate-secret" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b"
```

#### `GET /webhooks/{id}/deliveries` — List webhook deliveries

Recent attempts (kept 30 days), newest first.

Scope: `webhooks:read`

Parameters:

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | Record id. |
| `limit` | query |  | Page size, 1–100 (default 25). |
| `cursor` | query |  | `next_cursor` of the previous page. |
| `order` | query |  | Sort by `updated_at` (then id): `desc` newest first (default) or `asc`. |
| `updated_since` | query |  | Only records changed at or after this ISO 8601 time. |

Response 200:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | WebhookDelivery[] | yes |  |
| `data[].id` | string |  | Delivery id (header X-Baskety-Delivery). |
| `data[].webhook_id` | string |  | Webhook id. |
| `data[].event` | string |  | Event type. |
| `data[].event_id` | string |  | Event id (`evt_…`, the same for every webhook of one event). |
| `data[].status` | string |  | Status. One of: `pending`, `sending`, `retry`, `delivered`, `failed`, `cancelled`. |
| `data[].attempts` | integer |  | Attempts. |
| `data[].response_status` | integer |  | HTTP status of the last attempt. |
| `data[].last_error` | string |  | Last error. |
| `data[].next_attempt_at` | string (date-time) \| null |  | Next attempt. |
| `data[].delivered_at` | string (date-time) \| null |  | Delivered at. |
| `data[].created_at` | string (date-time) |  | Creation time. |
| `data[].updated_at` | string (date-time) |  | Last change. |
| `next_cursor` | string \| null | yes | Pass as `cursor` to get the next page; null on the last page. |
| `has_more` | boolean | yes | True when another page exists. |

```bash
curl "https://app.basketly.io/api/v1/webhooks/abc123/deliveries" \
  -H "Authorization: Bearer $API_KEY"
```

## Webhooks

A webhook sends a JSON `POST` to your HTTPS URL when something changes in the organisation. An organisation can have up to 10 webhooks. The signing secret (`whsec_…`) is returned only in the response that creates the webhook. The same webhooks can be added, tested, paused and removed in the panel, on the API page under Webhooks, with the delivery history of each.

Delivery is at least once: the same event can arrive more than once, so skip any `id` you have already handled. Payloads carry ids and states only, never names, phones or e-mails; fetch details with `GET`.

### `contact.created`

A person was added (shop order, manual, API, automation).

```json
{
  "contact_id": "abc123",
  "first_source": "Zamówienie w sklepie"
}
```

### `delivery.failed`

A conversion delivery to Meta failed for good.

```json
{
  "delivery_id": "d123",
  "event_id": "e123",
  "target_id": "t123",
  "dataset_id": "123456789",
  "event_name": "Purchase",
  "mode": "live",
  "error": "Połącz konto Meta ponownie lub sprawdź token."
}
```

- Answer with any 2xx code within 15 seconds. Store the event first and do longer work later.
- Any other answer, or none, is retried: up to 8 attempts in total, after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h.
