Composio setup
3x talks to the ad platforms through Composio, which stores the OAuth tokens and runs the platform APIs as versioned tools. This is configured once per 3x deployment by the operator; tenants then connect their own ad accounts from the dashboard without ever seeing a Composio key.
1. Project API key
- In the Composio dashboard create a project for the deployment (e.g. "3x production").
- Settings → API keys → create a project key (
ak_…) and set it asCOMPOSIO_API_KEYon the API.
Keys are sent only as headers, never logged and never stored in the database. A user key (uak_…,
as used by the Composio CLI) also works for local development, but then COMPOSIO_PROJECT_ID must be
set, because a user key without project id reaches an empty default project.
2. One auth config per platform
| Platform | Toolkit | Auth config | Variable |
|---|---|---|---|
| Google Ads | googleads | Composio-managed OAuth is fine (scope adwords) | COMPOSIO_AUTH_CONFIG_GOOGLEADS |
| Meta Ads | metaads | not managed: your own Meta app (OAuth, ads_read,business_management) or API-key mode with a system-user token | COMPOSIO_AUTH_CONFIG_METAADS |
| LinkedIn Ads | linkedin_ads | not managed: your own LinkedIn app with the Advertising API product (and Conversions API with rw_conversions if you upload conversions through Composio) | COMPOSIO_AUTH_CONFIG_LINKEDIN_ADS |
| TikTok Ads | tiktok_ads | Composio-managed OAuth is fine for reporting | COMPOSIO_AUTH_CONFIG_TIKTOK_ADS |
Create them under Auth configs → New in the Composio dashboard and copy the ac_… ids. A platform
without an auth config is simply not offered in the dashboard; its connect route answers
503 not_configured.
LinkedIn and Meta app reviews take time (LinkedIn's Advertising API review often several weeks). Start them before you need the data – there is no fallback source for LinkedIn spend.
3. Pin toolkit versions
Composio versions its toolkits; a new version can change response shapes. 3x pins the versions it was tested with:
COMPOSIO_TOOLKIT_VERSIONS={"googleads":"20261014_00","metaads":"20261008_00","linkedin_ads":"20260826_00","tiktok_ads":"20261001_00"}Raise a version only after running the normalizer contract tests (npm test -w @3x/api) against a
recorded sample of the new version.
4. Webhook (connection health)
Create one webhook subscription in the Composio project for
composio.connected_account.expired and composio.connected_account.activated, pointing at
https://<api-host>/v1/webhooks/composioand set its signing secret as COMPOSIO_WEBHOOK_SECRET (a whsec_ prefix is accepted). 3x verifies
the Standard Webhooks signature (webhook-id, webhook-timestamp, webhook-signature), rejects
timestamps older than five minutes and ignores replays. Without the webhook, the daily ads.health
job still detects expired connections – just later.
5. Check
Open Settings → Integrations of a project in the dashboard and click Connect on a platform.
A redirect to Composio's hosted login means the key and the auth config work; 503 not_configured
names the missing variable. Starting a connection (POST /v1/projects/{project}/ad-connections)
requires a signed-in user with ads:write – secret keys cannot start an OAuth flow on someone's
behalf.
How connections work
- Organization-level connections belong to an organization (agencies: one MCC or Business Manager login). Every project of the organization can then select its own ad accounts from it.
- Project-level connections belong to one project.
- The connect link is generated on click and expires after 10 minutes. After Composio's hosted
OAuth, 3x verifies the callback against a one-time nonce, waits until the connection is
ACTIVE, discovers the reachable ad accounts and lets you select those that belong to the project. - Disconnecting revokes the Composio connection and stops syncing; the imported cost data stays until you delete it.