# API reference

> Every endpoint, generated from the OpenAPI document. Base URL: https://taseli.co.il/api/v1.

Source: https://taseli.co.il/en/developers/reference

## Workspace

The business a key opens.

### The business this key opens

`GET /workspace`

| Returns | Type |  |
| --- | --- | --- |
| `id` | string |  |
| `name` | string |  |
| `slug` | string | In the app's links. |
| `currency` | string |  |
| `plan` | `free` `starter` `pro` `agency` |  |
| `accounts` | integer | Connected ad accounts. |

curl:

```bash
curl https://taseli.co.il/api/v1/workspace \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch("https://taseli.co.il/api/v1/workspace", {
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": {
    "id": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
    "name": "Dan Plumbing",
    "slug": "dan-plumbing",
    "currency": "ILS",
    "plan": "pro",
    "accounts": 2
  }
}
```

## Accounts

The connected Google, Meta and TikTok ad accounts, and their numbers.

### List ad accounts

`GET /accounts`

| Query | Type |  |
| --- | --- | --- |
| `limit` | integer | How many, from 1 to 100. Default `50`. |
| `cursor` | string | nextCursor from the previous page. |

| Returns | Type |  |
| --- | --- | --- |
| `id` | string |  |
| `name` | string |  |
| `platform` | `google` `meta` `tiktok` |  |
| `externalId` | string | The platform's own id. |
| `currency` | string |  |
| `timezone` | string | The account's time zone: its metrics' days end here. |
| `status` | `active` `syncing` `needs_reconnect` `suspended` |  |
| `readOnly` | boolean | Nothing changes in it, even approved. |
| `goal` | `leads` `sales` `calls` `traffic` |  |
| `target` | object |  |
| `target.kind` | `cpa` `roas` |  |
| `target.value` | number | Money for cpa, a ratio for roas. |
| `target.set` | boolean | False: nobody set a target; value is the account's own cost per result standing in. |
| `lastSyncAt` | string |  |
| `budget` | object |  |
| `budget.monthly` | number |  |
| `budget.spentMtd` | number | Spent this month. |
| `budget.forecast` | number | Where the month ends at this pace. |
| `budget.set` | boolean | False: no monthly budget set; monthly is the pace estimate. |
| `last28` | object |  |
| `last28.cost` | number |  |
| `last28.conversions` | number |  |
| `last28.value` | number | The conversion value the platform reports. 0 when it reports none. |
| `last28.clicks` | integer |  |
| `last28.impressions` | integer |  |
| `total` | integer | How many in the whole list. |
| `nextCursor` | string or null |  |

curl:

```bash
curl https://taseli.co.il/api/v1/accounts \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch("https://taseli.co.il/api/v1/accounts", {
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": [
    {
      "id": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
      "name": "Dan Plumbing Search",
      "platform": "google",
      "externalId": "123-456-7890",
      "currency": "ILS",
      "timezone": "Asia/Jerusalem",
      "status": "active",
      "readOnly": false,
      "goal": "leads",
      "target": {
        "kind": "cpa",
        "value": 85,
        "set": true
      },
      "lastSyncAt": "2026-10-20T08:12:00.000Z",
      "budget": {
        "monthly": 6000,
        "spentMtd": 2140.5,
        "forecast": 5890,
        "set": true
      },
      "last28": {
        "cost": 5320.1,
        "conversions": 61,
        "value": 0,
        "clicks": 1840,
        "impressions": 41200
      }
    }
  ],
  "total": 1,
  "nextCursor": null
}
```

### One ad account

`GET /accounts/{id}`

| Path | Type |  |
| --- | --- | --- |
| `id` | string | The account's Taseli id. Required. |

| Returns | Type |  |
| --- | --- | --- |
| `id` | string |  |
| `name` | string |  |
| `platform` | `google` `meta` `tiktok` |  |
| `externalId` | string | The platform's own id. |
| `currency` | string |  |
| `timezone` | string | The account's time zone: its metrics' days end here. |
| `status` | `active` `syncing` `needs_reconnect` `suspended` |  |
| `readOnly` | boolean | Nothing changes in it, even approved. |
| `goal` | `leads` `sales` `calls` `traffic` |  |
| `target` | object |  |
| `target.kind` | `cpa` `roas` |  |
| `target.value` | number | Money for cpa, a ratio for roas. |
| `target.set` | boolean | False: nobody set a target; value is the account's own cost per result standing in. |
| `lastSyncAt` | string |  |
| `budget` | object |  |
| `budget.monthly` | number |  |
| `budget.spentMtd` | number | Spent this month. |
| `budget.forecast` | number | Where the month ends at this pace. |
| `budget.set` | boolean | False: no monthly budget set; monthly is the pace estimate. |
| `last28` | object |  |
| `last28.cost` | number |  |
| `last28.conversions` | number |  |
| `last28.value` | number | The conversion value the platform reports. 0 when it reports none. |
| `last28.clicks` | integer |  |
| `last28.impressions` | integer |  |

curl:

```bash
curl "https://taseli.co.il/api/v1/accounts/$ACCOUNT_ID" \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch(`https://taseli.co.il/api/v1/accounts/${ACCOUNT_ID}`, {
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": {
    "id": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
    "name": "Dan Plumbing Search",
    "platform": "google",
    "externalId": "123-456-7890",
    "currency": "ILS",
    "timezone": "Asia/Jerusalem",
    "status": "active",
    "readOnly": false,
    "goal": "leads",
    "target": {
      "kind": "cpa",
      "value": 85,
      "set": true
    },
    "lastSyncAt": "2026-10-20T08:12:00.000Z",
    "budget": {
      "monthly": 6000,
      "spentMtd": 2140.5,
      "forecast": 5890,
      "set": true
    },
    "last28": {
      "cost": 5320.1,
      "conversions": 61,
      "value": 0,
      "clicks": 1840,
      "impressions": 41200
    }
  }
}
```

### Daily metrics, or totals by campaign

`GET /accounts/{id}/metrics`

As synced from the platform once a day, in the account's currency and time zone. A day not synced yet is missing, never zero. At most 400 days per call. by=campaign gives the 200 campaigns that spent the most in the period; for one row per campaign per day, call it once per day.

| Path | Type |  |
| --- | --- | --- |
| `id` | string | The account's Taseli id. Required. |

| Query | Type |  |
| --- | --- | --- |
| `from` | string | First day, YYYY-MM-DD. Default: 29 days before to. |
| `to` | string | Last day, YYYY-MM-DD. Default: today (UTC). |
| `by` | `day` `campaign` | One row per day, or totals per campaign. Default `day`. |

| Returns | Type |  |
| --- | --- | --- |
| `account` | string |  |
| `currency` | string |  |
| `from` | string |  |
| `to` | string |  |
| `days` | object[] | Optional. |
| `days[].date` | string |  |
| `days[].cost` | number |  |
| `days[].conversions` | number |  |
| `days[].value` | number |  |
| `days[].clicks` | integer |  |
| `days[].impressions` | integer |  |
| `totals` | object | Optional. |
| `totals.cost` | number |  |
| `totals.conversions` | number |  |
| `totals.value` | number |  |
| `totals.clicks` | integer |  |
| `totals.impressions` | integer |  |
| `totals.costPerConversion` | number or null |  |
| `totals.ctr` | number or null |  |
| `campaigns` | object[] | Optional. |
| `campaigns[].id` | string | The campaign's Taseli id. |
| `campaigns[].name` | string |  |
| `campaigns[].cost` | number |  |
| `campaigns[].conversions` | number |  |
| `campaigns[].value` | number |  |
| `campaigns[].clicks` | integer |  |
| `campaigns[].impressions` | integer |  |
| `campaigns[].costPerConversion` | number or null |  |

curl:

```bash
curl "https://taseli.co.il/api/v1/accounts/$ACCOUNT_ID/metrics?from=2026-10-01&to=2026-10-20&by=day" \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch(`https://taseli.co.il/api/v1/accounts/${ACCOUNT_ID}/metrics?from=2026-10-01&to=2026-10-20&by=day`, {
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": {
    "account": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
    "currency": "ILS",
    "from": null,
    "to": null,
    "days": [
      {
        "date": "2026-10-19",
        "cost": 182.4,
        "conversions": 3,
        "value": 0,
        "clicks": 64,
        "impressions": 1390
      }
    ],
    "totals": {
      "cost": 182.4,
      "conversions": 3,
      "value": 0,
      "clicks": 64,
      "impressions": 1390,
      "costPerConversion": 60.8,
      "ctr": 4.6
    }
  }
}
```

## Proposals

Changes Taseli prepared, ran, watched or undid. The owner approves them in Taseli; approvalUrl is the link. Taseli keeps the latest 200 here.

### List proposals by state

`GET /proposals`

| Query | Type |  |
| --- | --- | --- |
| `status` | `waiting` `active` `done` `all` | waiting: awaiting approval, most valuable first. active: approved, executing or monitoring. done: kept, rolled_back, rejected, expired or failed. all. The last three are newest first. Default `waiting`. |
| `account` | string | An account's id, name or platform id. |
| `limit` | integer | How many, from 1 to 100. Default `20`. |
| `cursor` | string | nextCursor from the previous page. |
| `language` | `he` `ar` `en` | Language of titles and texts. Default `he`. |

| Returns | Type |  |
| --- | --- | --- |
| `id` | string |  |
| `number` | integer | The # the owner sees. |
| `status` | `awaiting_approval` `approved` `executing` `monitoring` `kept` `rolled_back` `rejected` `expired` `failed` |  |
| `kind` | string | What it changes, for example budget_change, negative_keywords_add, entity_pause, bid_target_change, campaign_create. |
| `source` | string | Who prepared it: audit, anomaly, rule, playbook, agent (Taseli's chat), mcp (an outside AI) or user. |
| `account` | string | The account's name. |
| `campaign` | string or null |  |
| `title` | string |  |
| `why` | string |  |
| `monthlyValue` | number | What Taseli expects it to be worth a month, in the account's currency. |
| `risk` | `low` `medium` `high` |  |
| `confidence` | `high` `medium` `low` |  |
| `changes` | object[] |  |
| `changes[].what` | string | The campaign, keyword or ad. |
| `changes[].field` | string |  |
| `changes[].before` | string |  |
| `changes[].after` | string |  |
| `createdAt` | string |  |
| `decidedAt` | string or null |  |
| `decidedVia` | string or null |  |
| `executedAt` | string or null |  |
| `watch` | object or null |  |
| `watch.day` | integer | Day of the watch. |
| `watch.days` | integer | Length of the watch. |
| `rolledBackAt` | string or null |  |
| `approvalUrl` | string | Where the owner approves it, in Taseli. |
| `total` | integer | How many in the whole list. |
| `nextCursor` | string or null |  |

curl:

```bash
curl "https://taseli.co.il/api/v1/proposals?status=waiting&language=en" \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch("https://taseli.co.il/api/v1/proposals?status=waiting&language=en", {
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": [
    {
      "id": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
      "number": 214,
      "status": "awaiting_approval",
      "kind": "negative_keywords_add",
      "source": "audit",
      "account": "Dan Plumbing Search",
      "campaign": "Emergency plumber",
      "title": "Block 12 searches that spend without leads",
      "why": "They took ₪640 in 28 days with no lead.",
      "monthlyValue": 680,
      "risk": "low",
      "confidence": "high",
      "changes": [
        {
          "what": "Emergency plumber",
          "field": "Blocked searches",
          "before": "48",
          "after": "60"
        }
      ],
      "createdAt": "2026-10-20T08:12:00.000Z",
      "decidedAt": null,
      "decidedVia": null,
      "executedAt": null,
      "watch": null,
      "rolledBackAt": null,
      "approvalUrl": "https://taseli.co.il/en/app/dan-plumbing/proposals/0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90"
    }
  ],
  "total": 1,
  "nextCursor": null
}
```

### One proposal in full

`GET /proposals/{id}`

Up to 50 changes; the list shows 5 per proposal. Among the workspace's latest 200.

| Path | Type |  |
| --- | --- | --- |
| `id` | string | The proposal's id. Required. |

| Query | Type |  |
| --- | --- | --- |
| `language` | `he` `ar` `en` | Language of titles and texts. Default `he`. |

| Returns | Type |  |
| --- | --- | --- |
| `id` | string |  |
| `number` | integer | The # the owner sees. |
| `status` | `awaiting_approval` `approved` `executing` `monitoring` `kept` `rolled_back` `rejected` `expired` `failed` |  |
| `kind` | string | What it changes, for example budget_change, negative_keywords_add, entity_pause, bid_target_change, campaign_create. |
| `source` | string | Who prepared it: audit, anomaly, rule, playbook, agent (Taseli's chat), mcp (an outside AI) or user. |
| `account` | string | The account's name. |
| `campaign` | string or null |  |
| `title` | string |  |
| `why` | string |  |
| `monthlyValue` | number | What Taseli expects it to be worth a month, in the account's currency. |
| `risk` | `low` `medium` `high` |  |
| `confidence` | `high` `medium` `low` |  |
| `changes` | object[] |  |
| `changes[].what` | string | The campaign, keyword or ad. |
| `changes[].field` | string |  |
| `changes[].before` | string |  |
| `changes[].after` | string |  |
| `createdAt` | string |  |
| `decidedAt` | string or null |  |
| `decidedVia` | string or null |  |
| `executedAt` | string or null |  |
| `watch` | object or null |  |
| `watch.day` | integer | Day of the watch. |
| `watch.days` | integer | Length of the watch. |
| `rolledBackAt` | string or null |  |
| `approvalUrl` | string | Where the owner approves it, in Taseli. |

curl:

```bash
curl "https://taseli.co.il/api/v1/proposals/$PROPOSAL_ID?language=en" \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch(`https://taseli.co.il/api/v1/proposals/${PROPOSAL_ID}?language=en`, {
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": {
    "id": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
    "number": 214,
    "status": "awaiting_approval",
    "kind": "negative_keywords_add",
    "source": "audit",
    "account": "Dan Plumbing Search",
    "campaign": "Emergency plumber",
    "title": "Block 12 searches that spend without leads",
    "why": "They took ₪640 in 28 days with no lead.",
    "monthlyValue": 680,
    "risk": "low",
    "confidence": "high",
    "changes": [
      {
        "what": "Emergency plumber",
        "field": "Blocked searches",
        "before": "48",
        "after": "60"
      }
    ],
    "createdAt": "2026-10-20T08:12:00.000Z",
    "decidedAt": null,
    "decidedVia": null,
    "executedAt": null,
    "watch": null,
    "rolledBackAt": null,
    "approvalUrl": "https://taseli.co.il/en/app/dan-plumbing/proposals/0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90"
  }
}
```

## Activity

The latest 200 things that happened in the accounts.

### What happened in the accounts, newest first

`GET /activity`

| Query | Type |  |
| --- | --- | --- |
| `limit` | integer | How many, from 1 to 100. Default `50`. |
| `cursor` | string | nextCursor from the previous page. |
| `language` | `he` `ar` `en` | Language of titles and texts. Default `he`. |

| Returns | Type |  |
| --- | --- | --- |
| `id` | string |  |
| `at` | string |  |
| `account` | string | The account's name. |
| `actor` | `taseli` `user` `external` `google` |  |
| `kind` | `executed` `kept` `rolled_back` `approved` `rejected` `external_change` `sync` `alert` `access_changed` |  |
| `text` | string |  |
| `detail` | string or null |  |
| `entity` | string or null |  |
| `via` | string or null |  |
| `proposalId` | string or null |  |
| `total` | integer | How many in the whole list. |
| `nextCursor` | string or null |  |

curl:

```bash
curl "https://taseli.co.il/api/v1/activity?limit=20&language=en" \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch("https://taseli.co.il/api/v1/activity?limit=20&language=en", {
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": [
    {
      "id": "a-48213",
      "at": "2026-10-20T08:12:00.000Z",
      "account": "Dan Plumbing Search",
      "actor": "taseli",
      "kind": "executed",
      "text": "Blocked 12 searches.",
      "detail": null,
      "entity": "Emergency plumber",
      "via": null,
      "proposalId": null
    }
  ],
  "total": 1,
  "nextCursor": null
}
```

## Creatives

Each picture and video with its own last-30-day results.

### Pictures and videos with their results and verdict

`GET /creatives`

Highest spend first, from the 300 highest. No cursor: shown and of say how much of the list came back. An account filter leaves out creatives that belong to no account (uploads, generated pictures). With nothing to show, the answer is only a note.

| Query | Type |  |
| --- | --- | --- |
| `account` | string | An account's id, name or platform id. |
| `verdict` | `good` `ok` `weak` `untested` `unused` `new` | Only this verdict. |
| `kind` | `image` `video` | Only pictures or videos. |
| `limit` | integer | How many, from 1 to 100. Default `15`. |
| `language` | `he` `ar` `en` | Language of titles and texts. Default `he`. |

| Returns | Type |  |
| --- | --- | --- |
| `window` | string | Optional. |
| `verdicts` | object | How many of each verdict. Optional. |
| `note` | string | A caveat on the numbers, or the only field when there is nothing to show. |
| `shown` | integer | Optional. |
| `of` | integer | Optional. |
| `creatives` | object[] | Optional. |
| `creatives[].id` | string |  |
| `creatives[].title` | string |  |
| `creatives[].kind` | `image` `video` |  |
| `creatives[].shows` | string | What's in it. Optional. |
| `creatives[].seconds` | integer | A video's length. Optional. |
| `creatives[].account` | string | The account's name, or the source of a creative with no account. Optional. |
| `creatives[].verdict` | `good` `ok` `weak` `untested` `unused` `new` | Among the account's own creatives. Optional. |
| `creatives[].why` | string | Why it's weak. Optional. |
| `creatives[].live` | boolean | Seen in the last 3 days. |
| `creatives[].runsOn` | string[] | The placements it runs on. Optional. |
| `creatives[].spend` | string | Formatted with its currency. Optional. |
| `creatives[].results` | number | Optional. |
| `creatives[].impressions` | number | Optional. |
| `creatives[].ctr` | number | Percent. Optional. |
| `creatives[].perResult` | string | Formatted with its currency. Optional. |
| `creatives[].offer` | string | Whether it matches the current offer. Optional. |
| `creatives[].offerWhy` | string | Optional. |
| `creatives[].watched` | object | For videos: the hook, the offer, voice, music, on-screen words and the start of the transcript. Optional. |

curl:

```bash
curl "https://taseli.co.il/api/v1/creatives?verdict=weak&limit=50&language=en" \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch("https://taseli.co.il/api/v1/creatives?verdict=weak&limit=50&language=en", {
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": {
    "window": "last 30 days",
    "verdicts": {
      "good": 2,
      "weak": 1
    },
    "note": "Per-creative results on Meta are a lower bound; spend, impressions and clicks are exact.",
    "shown": 1,
    "of": 3,
    "creatives": [
      {
        "id": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
        "title": "Plumber at the door, 24/7",
        "kind": "image",
        "account": "Dan Plumbing Meta",
        "verdict": "good",
        "live": true,
        "spend": "₪412.00",
        "results": 9,
        "impressions": 15210,
        "ctr": 1.8,
        "perResult": "₪45.78"
      }
    ]
  }
}
```

## Competitors

Where the feature is on for the business. Otherwise 404 not_enabled.

### Competitors Taseli watches

`GET /competitors`

| Query | Type |  |
| --- | --- | --- |
| `language` | `he` `ar` `en` | Language of titles and texts. Default `he`. |

| Returns | Type |  |
| --- | --- | --- |
| `status` | `new` `discovering` `suggested` `collecting` `ready` `paused` | The feature's state for this business. |
| `following` | string | Optional. |
| `headline` | string | The week in one line. Optional. |
| `analyzed` | string | Last read, YYYY-MM-DD. Optional. |
| `nextCheck` | string | Next weekly check, YYYY-MM-DD. Optional. |
| `checkNow` | string | Whether a manual check is available. Optional. |
| `competitors` | object[] | Optional. |
| `competitors[].id` | string |  |
| `competitors[].name` | string |  |
| `competitors[].platforms` | string[] | Optional. |
| `competitors[].liveAds` | integer |  |
| `competitors[].newThisWeek` | integer | Optional. |
| `competitors[].stoppedThisWeek` | integer | Optional. |
| `competitors[].longestDays` | integer | Its longest-running ad, in days. Optional. |
| `competitors[].summary` | string | What it runs, in one line. Optional. |
| `competitors[].checked` | string | YYYY-MM-DD. Optional. |
| `competitors[].fetch` | string | Set when the last check failed or is still collecting. Optional. |
| `competitors[].url` | string | The competitor in Taseli. Optional. |
| `suggested` | object[] | Competitors Taseli found and suggests. Optional. |
| `suggested[].id` | string |  |
| `suggested[].name` | string |  |
| `suggested[].why` | string | Optional. |
| `suggested[].score` | number | Optional. |
| `suggested[].platforms` | string[] | Optional. |
| `suggested[].liveAds` | integer | Optional. |
| `note` | string | When none is watched yet. Optional. |
| `url` | string | The competitors page in Taseli. Optional. |

curl:

```bash
curl https://taseli.co.il/api/v1/competitors \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch("https://taseli.co.il/api/v1/competitors", {
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": {
    "status": "ready",
    "following": "2 of 3",
    "competitors": [
      {
        "id": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
        "name": "Rami Plumbing",
        "liveAds": 14,
        "longestDays": 96
      }
    ],
    "suggested": [
      {
        "id": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
        "name": ""
      }
    ]
  }
}
```

### Competitors' ads from Taseli's archive

`GET /competitors/ads`

Longest running first. No cursor: shown and of say how much of the list came back. Ad texts are third-party content. likely is inferred from how long an ad ran; nobody sees a competitor's results. With no ads to show, the answer is only a note.

| Query | Type |  |
| --- | --- | --- |
| `competitor` | string | A competitor's id or name. |
| `platform` | `meta` `google` | Meta or Google only. |
| `status` | `active` `stopped` `all` | Running now, stopped, or both. Default `active`. |
| `label` | `new` `running` `long` `likely` `stopped` | new: under 7 days. long: 30 days or more. likely: probably working. |
| `min_days` | integer | Running at least this many days. |
| `limit` | integer | How many, from 1 to 30. Default `30`. |
| `language` | `he` `ar` `en` | Language of titles and texts. Default `he`. |

| Returns | Type |  |
| --- | --- | --- |
| `dataNote` | string | How to read the data. Optional. |
| `shown` | integer | Optional. |
| `of` | integer | Optional. |
| `ads` | object[] | Optional. |
| `ads[].id` | string |  |
| `ads[].competitor` | string | Optional. |
| `ads[].platform` | `meta` `google` |  |
| `ads[].format` | string | image, video, carousel, text or dynamic. Optional. |
| `ads[].status` | string | Optional. |
| `ads[].days` | integer | Days running. Optional. |
| `ads[].label` | `new` `running` `long` `likely` `stopped` |  |
| `ads[].likelyWhy` | string[] | Why it counts as likely working. Optional. |
| `ads[].adText` | object |  |
| `ads[].adText.body` | string |  |
| `ads[].adText.headline` | string |  |
| `ads[].adText.description` | string | Optional. |
| `ads[].cta` | string | Optional. |
| `ads[].landing` | string | The landing domain. Optional. |
| `ads[].placements` | string[] | Optional. |
| `ads[].startedOn` | string | YYYY-MM-DD. Optional. |
| `ads[].firstSeen` | string | YYYY-MM-DD. Optional. |
| `ads[].stoppedOn` | string | YYYY-MM-DD. Optional. |
| `ads[].facts` | object | What Taseli read from the ad: offer, price, discount, hook, angles. Optional. |
| `ads[].thumb` | string | A thumbnail link, valid for about an hour. Optional. |
| `ads[].source` | string | The public ad library's link. Optional. |
| `ads[].url` | string | The ad in Taseli. Optional. |
| `note` | string | When there are no ads to show. Optional. |

curl:

```bash
curl "https://taseli.co.il/api/v1/competitors/ads?label=likely&language=en" \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch("https://taseli.co.il/api/v1/competitors/ads?label=likely&language=en", {
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": {
    "shown": 1,
    "of": 14,
    "ads": [
      {
        "id": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
        "competitor": "Rami Plumbing",
        "platform": "meta",
        "format": "image",
        "status": "active",
        "days": 96,
        "label": "likely",
        "adText": {
          "body": "A plumber at your door in 30 minutes.",
          "headline": "24/7 plumber"
        },
        "landing": "rami-plumbing.example"
      }
    ]
  }
}
```

## Webhooks

Owners and admins only, with a personal key that isn't read-only (a connected app's token can't manage webhooks).

### List webhooks

`GET /webhooks`

| Returns | Type |  |
| --- | --- | --- |
| `id` | string |  |
| `url` | string |  |
| `events` | string[] |  |
| `language` | `he` `ar` `en` |  |
| `active` | boolean | False: it sends nothing. See pausedReason. |
| `pausedReason` | `key` `member` or null |  |
| `failures` | integer | Failed attempts in a row. At 25 it switches off. |
| `lastStatus` | integer or null |  |
| `lastError` | string or null |  |
| `lastDeliveryAt` | string or null |  |
| `createdAt` | string |  |

curl:

```bash
curl https://taseli.co.il/api/v1/webhooks \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch("https://taseli.co.il/api/v1/webhooks", {
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": [
    {
      "id": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
      "url": "https://crm.example.com/hooks/taseli",
      "events": [
        ""
      ],
      "language": "en",
      "active": true,
      "pausedReason": null,
      "failures": 0,
      "lastStatus": 200,
      "lastError": null,
      "lastDeliveryAt": null,
      "createdAt": "2026-10-20T08:12:00.000Z"
    }
  ]
}
```

### Create a webhook

`POST /webhooks`

Up to 5 per workspace. The response carries the signing secret once; it isn't shown again (delete and recreate to replace it). Not idempotent: after a timeout, list webhooks before retrying. Deliveries are signed with HMAC-SHA256; reject signatures older than 300 seconds. See /developers/webhooks.

| Body | Type |  |
| --- | --- | --- |
| `url` | string | A public https address on the default port. |
| `events` | `proposal.created` `change.applied` `change.undone` `verdict.recorded`[] | Default: all of them. Optional. |
| `language` | `he` `ar` `en` | Language of titles and texts in the events. Optional. |

| Returns | Type |  |
| --- | --- | --- |
| `id` | string |  |
| `url` | string |  |
| `events` | string[] |  |
| `language` | `he` `ar` `en` |  |
| `active` | boolean | False: it sends nothing. See pausedReason. |
| `pausedReason` | `key` `member` or null |  |
| `failures` | integer | Failed attempts in a row. At 25 it switches off. |
| `lastStatus` | integer or null |  |
| `lastError` | string or null |  |
| `lastDeliveryAt` | string or null |  |
| `createdAt` | string |  |
| `secret` | string | Shown once. |

curl:

```bash
curl -X POST https://taseli.co.il/api/v1/webhooks \
  -H "Authorization: Bearer $TASELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://crm.example.com/hooks/taseli","language":"en"}'
```

JavaScript:

```js
const res = await fetch("https://taseli.co.il/api/v1/webhooks", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"url":"https://crm.example.com/hooks/taseli","language":"en"}),
});
const { data } = await res.json();
```

Response 201:

```json
{
  "data": {
    "id": "0b6f3c1e-7a42-4c55-9b1d-2f6e8a1c4d90",
    "url": "https://crm.example.com/hooks/taseli",
    "events": [
      ""
    ],
    "language": "en",
    "active": true,
    "pausedReason": null,
    "failures": 0,
    "lastStatus": 200,
    "lastError": null,
    "lastDeliveryAt": null,
    "createdAt": "2026-10-20T08:12:00.000Z",
    "secret": "whsec_G7pQ0mYcVx2u4Tn8Zk1Rb6Ld3Ha9Ws5Je0Fy7Ci2Mo4"
  }
}
```

### Delete a webhook

`DELETE /webhooks/{id}`

| Path | Type |  |
| --- | --- | --- |
| `id` | string | The webhook's id. Required. |

Returns `204` with no body.

curl:

```bash
curl -X DELETE "https://taseli.co.il/api/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch(`https://taseli.co.il/api/v1/webhooks/${WEBHOOK_ID}`, {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
console.log(res.status); // 204
```

### Send one signed webhook.test now

`POST /webhooks/{id}/test`

Answers what the receiver answered. A test that lands switches back on a webhook that failures switched off. One paused for its key or its maker stays paused.

| Path | Type |  |
| --- | --- | --- |
| `id` | string | The webhook's id. Required. |

| Returns | Type |  |
| --- | --- | --- |
| `delivered` | boolean | A 2xx came back. |
| `status` | integer | The receiver's status. 0: none. |
| `error` | string or null |  |

curl:

```bash
curl -X POST "https://taseli.co.il/api/v1/webhooks/$WEBHOOK_ID/test" \
  -H "Authorization: Bearer $TASELI_KEY"
```

JavaScript:

```js
const res = await fetch(`https://taseli.co.il/api/v1/webhooks/${WEBHOOK_ID}/test`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TASELI_KEY}`,
  },
});
const { data } = await res.json();
```

Response 200:

```json
{
  "data": {
    "delivered": true,
    "status": 200,
    "error": null
  }
}
```
