> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corsair.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Adyntel plugin

> Ad intelligence across Meta, Google, LinkedIn and TikTok - ad libraries, keyword searches, paid versus organic keywords.

Use **Adyntel** through Corsair: one client, typed API calls.

Adyntel needs two things, not one: an API key and the email of the account that owns it. Both go in the JSON body of every call, and a key without its matching email is a 401. Sign up at platform.adyntel.com/auth/sign-up for 50 free credits, then copy the key from the Integrations page of the platform dashboard. Store it as `api_key` and the account email as `email` in the account config.

Things to know before you call:

* **Every call costs credits.** One credit per scrape, one page = one credit. `google.shoppingStatus` is free. `extract_text` (Google and LinkedIn), `impression_data` (LinkedIn) and `influencer_ads` (TikTok) each add one credit on top. A 204 ("no ads found") is not charged.
* **A 204 is a real answer, and each endpoint reports it differently.** Adyntel answers 204 with no body when it finds nothing, and charges nothing for it. The plugin turns that into an empty result rather than an error. How you read it depends on the endpoint: `meta.ads` and `meta.adSearch` leave `results` absent, `tiktok.ads` leaves `brand_ads` absent, `tiktok.keywordSearch` leaves `data` absent, and `google.shopping` and `google.shoppingStatus` leave `ads` and `id` absent. Separately, `google.shopping` without `shopping: true` answers with an `id` for a queued scrape; that is not an empty result, so poll it with `google.shoppingStatus`. A failed body read is not a 204 and throws. On `google.ads` and `linkedin.ads` a 204 can also mean the scrape passed Adyntel's 60 second sync limit, and the status alone can't tell that from "no ads". When you need a complete answer from either, send `webhook_url` so the scrape runs as a job and the results are POSTed to you.
* **`company_domain` is the bare domain.** Send `clay.com`, never `https://clay.com` or `www.clay.com`. Adyntel does not reject a URL; it finds nothing and answers 204.
* **Paging differs by endpoint.** `meta.ads`, `meta.adSearch`, `google.ads`, `linkedin.ads` and `linkedin.keywordSearch` return a `continuation_token` that is `null` on the last page; send it back on the next call. `tiktok.ads` uses `continuation_tokens` instead, one token for brand ads and one per influencer. `tiktok.keywordSearch` has no paging input at all: it returns `has_more` and a `search_id`, but the API exposes no parameter for requesting the next page, so a caller gets the first page only. On Google, a country filter is scoped to one call: send `country_code` again with every token, because the token does not carry it.
* **`all_ads` is a one-way door.** On `google.ads`, `linkedin.ads` and `meta.ads` it follows the token to the end, costs one credit per page, and the docs state there is no way to stop it. It needs a `webhook_url`, and the schema rejects the call without one rather than letting Adyntel fail it. `meta.adSearch` exposes `all_ads` without a webhook input, so its pages are collected by Adyntel and only the final result is returned; it takes no `webhook_url`.
* **Webhooks are yours to receive.** Setting `webhook_url` makes Adyntel answer with a `jobId` and POST the results to that URL later. This plugin does not host that receiver; the response carries `jobId` or `jobIds` so you can tell a queued job from a result.
* **Two filters reject rather than ignore.** On Meta, an unrecognised `language` is a 422 before anything is scraped. On Google, Adyntel treats an unrecognised `media_type` as `all`, so the schema accepts only `text`, `image`, `video` and `all` and a typo fails before the call instead of widening the result.
* **Country support differs per endpoint.** Meta accepts one code or an array of up to 20 (the array needs `webhook_url` and costs one credit per country). Google accepts one per call. TikTok's keyword search accepts European codes only; the schema rejects `US` rather than letting the API answer with nothing.
* **TikTok wraps its answers.** `tiktok.keywordSearch` and `tiktok.adDetails` return `{ code, data }` and can send a non-zero `code` with a 200. The plugin throws an `AdyntelAPIError` on a non-zero code so a failure cannot read as "no ads"; its `providerCode` holds the code, and it is not retried because the scrape already ran.

**What you get:**

* 11 typed API operations

## Setup

**1. Install**

```bash npm theme={null}
npm install corsair @corsair-dev/adyntel better-sqlite3
npm install --save-dev @types/better-sqlite3
```

```bash yarn theme={null}
yarn add corsair @corsair-dev/adyntel better-sqlite3
yarn add --dev @types/better-sqlite3
```

```bash pnpm theme={null}
pnpm install corsair @corsair-dev/adyntel better-sqlite3
pnpm install --save-dev @types/better-sqlite3
```

```bash bun theme={null}
bun add corsair @corsair-dev/adyntel better-sqlite3
bun add --dev @types/better-sqlite3
```

**2. Add the plugin**

```ts corsair.ts theme={null}
import Database from 'better-sqlite3';
import { createCorsair } from 'corsair';
import { adyntel } from '@corsair-dev/adyntel';

export const corsair = createCorsair({
	plugins: [
		adyntel(),
	],
	database: new Database('corsair.db'),
	kek: process.env.CORSAIR_KEK!,
	multiTenancy: true,
	hub: {
		projectApiKey: process.env.CORSAIR_API_KEY!,
		signingSecret: process.env.CORSAIR_SIGNING_SECRET!,
	},
});
```

With `multiTenancy: true`, scope calls with `corsair.withTenant(id)`. See [Quick start](https://docs.corsair.dev/quick-start.md) for KEK + Hub keys, and [Multi-tenancy](https://docs.corsair.dev/concepts/multi-tenancy.md) for account isolation.

**3. Choose authentication**

> [tab] API Key (Recommended)
>
> No setup required yet. When you make your first request as a tenant, Corsair prompts for the API key.
>
> ```ts theme={null}
> adyntel()
> ```
>
> More: [API Key](https://docs.corsair.dev/concepts/api-key)

**4. Connect a tenant**

Mint a connect link and send the tenant to it. Hub hosts the page and delivers the result to your app — see [Connect / OAuth](https://docs.corsair.dev/management/connect).

```ts theme={null}
const { connectUrl } = await corsair.manage.connect.createLink({
	plugin: 'adyntel',
	tenantId: 'acme',
});
// redirect the user's browser to connectUrl
```

## Example API calls

**List a company's Google ads**

```ts theme={null}
const tenant = corsair.withTenant('acme');
await tenant.adyntel.api.google.ads({ company_domain: 'clay.com' });
```

**Compare paid and organic keywords for a domain**

```ts theme={null}
const tenant = corsair.withTenant('acme');
await tenant.adyntel.api.keywords.paidVsOrganic({ company_domain: 'lokalise.com' });
```

See the full list on the [API](https://docs.corsair.dev/plugins/adyntel/api.md) page.

## What's next

> [card] [API reference](https://docs.corsair.dev/plugins/adyntel/api.md)
>
> Every `adyntel.api.*` operation with input and output types.

> [card] [Connect / OAuth](https://docs.corsair.dev/management/connect)
>
> createLink, Hub delivery, and tenant connect flows.

> [card] [Use with agents](https://docs.corsair.dev/mcp-adapters/mcp-adapters)
>
> Expose this plugin's operations as MCP tools.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.