Documentation · Platforms.

Connect Meta and Google Ads to Admaxxer — without App Review.

Admaxxer's paste-token model is the v1 default. You bring your own credentials, we encrypt them with AES-256-GCM, monitor expiry on a daily cron, and rotate keys without breaking existing connections. No Facebook App Review. No multi-week Google Ads developer-token wait. Connect in five minutes and ship revenue attribution today.

Open Integrations Back to docs

Overview — why paste-token, why now

Most attribution and ad-ops tools require a Meta App Review pass, a Google Ads standard-access developer token, or both. That gate is a 4–12 week back-and-forth before you ship a single feature. The reviewer wants a recorded demo, screenshots, a privacy policy, a data-deletion flow, and proof that your callbacks are reachable. By the time you pass, your roadmap has shifted twice. Admaxxer skips that loop entirely with a paste-token model: you generate credentials in your own developer accounts and paste them into Integrations. Admaxxer holds them encrypted, rotates them, monitors expiry, and calls the public APIs on your behalf. Account safety is rule #1 — we respect every documented rate limit and back off aggressively rather than risk a flagged ad account or a revoked developer token.

The trade-off is real. With a paste-token model, the user is responsible for generating the token in the first place. We compensate by making the generation step the simplest possible UX: short instructions, a copy-paste field, a live GET /me validator that fails before persistence. For users who already manage tokens (developers, agencies, in-house ad-ops teams), this is faster than OAuth. For users who don't, the OAuth path is supported as well — we don't force a single flow.

The v1 model has three properties worth calling out:

This document covers the full lifecycle — connect, encrypt, monitor, rotate, disconnect — and the safeguards that make it production-grade. If you're integrating with Admaxxer's API, the connection model also affects the public REST surface (see Developer / REST API).

Meta Ads connection

Two paths are supported, and both end with a live GET /me Graph API call before the credential is persisted — invalid tokens fail fast at connect time, not on the next cron run.

Path A — OAuth (primary, Apr 2026+)

The OAuth path runs the Facebook OAuth dialog with PKCE and a signed state parameter. After the user approves, we exchange the short-lived authorization code for a short-lived user token, then immediately exchange that for a long-lived (~60 day) user token. The flow ends with an account picker where the user chooses one or more ad accounts to wire up. PKCE is required because Admaxxer is a public-facing app; the signed state blocks CSRF on the redirect. The signed-state value embeds the originating workspace ID and a short-lived nonce, so a stolen redirect can't be replayed against a different workspace.

Account picker behavior: if the OAuth user has access to multiple ad accounts, we list them all with their currency, status, and timezone, and let the user select one or many. Each selected account becomes a separate adPlatformConnection row. The OAuth path doesn't require the user to know any IDs upfront — Facebook returns them, we display them, the user picks.

Path B — Paste long-lived token (legacy/fallback)

Users who already manage long-lived user tokens (e.g. issued from Meta for Developers' Graph Explorer or from a System User) can paste the access token plus the ad-account ID directly. This bypasses the OAuth round-trip entirely — useful for agencies provisioning multiple workspaces from a single Business Manager, or for engineering-led integrations that don't want to ship an OAuth UI. The paste flow runs the same GET /me validator before persistence, so an invalid token fails the same way it would in OAuth.

System User tokens are particularly recommended for agencies and long-lived deployments. They don't expire on the same ~60-day cadence and they survive password changes by individual users. Generation requires a Meta Business account with admin privileges; the Marketing API system-user docs walk through the steps.

Required permissions and limits

Meta Ads — required scopes, API version, and rate limits
ItemValue
OAuth scopesads_management, ads_read
Graph API versionv21.0
Rate limit~200 calls/hour/user-token (auto-backoff on 17/32/613)
Multi-account ceiling≤ 10 active connections / workspace (META_MAX_CONNECTIONS_PER_WORKSPACE)
Liveness checkGET /me before persistence

Source of truth — internal + allowlisted external

Internal: /docs/ADMAXXER-META-SERVICE.md. The only external Meta documentation we consult is the four-URL allowlist: Marketing API overview, Get started, Business SDK, and v25.0 reference. Field names, enum values, edge shapes, and endpoint paths come from the v25.0 reference specifically — this prevents drift across Meta API version semantics.

Google Ads connection

Google Ads requires an OAuth2 refresh-token exchange. The required fields are: developer_token, oauth2_client_id, oauth2_client_secret, refresh_token, and customer_id (10 digits, no dashes). For agencies and MCC accounts, also pass login_customer_id so requests resolve through the manager hierarchy. The required scope is https://www.googleapis.com/auth/adwords — no broader Google API access is requested.

GAQL — Google Ads Query Language

Admaxxer batches campaign, ad-group, and keyword reads via GAQL. GAQL is Google's SQL-flavored query language for the Ads API; it lets you select fields from a logical resource (campaign, ad_group, keyword_view, etc.) with WHERE filters and segments. A representative query:

SELECT campaign.id, campaign.name
FROM campaign
WHERE campaign.status != 'REMOVED'

All cost values come back as cost_micros; we divide by 1,000,000 to get the account currency (USD for most US accounts, native currency otherwise). Conversion values, click counts, and impression counts are first-class GAQL fields — no second roundtrip required. We always select cost_micros rather than constructing a derived "spend" field because the micros version is the canonical Google representation; rounding to dollars happens in the presentation layer.

For richer reporting (date-segmented insights, cross-account roll-ups, keyword performance over a window), Admaxxer issues batched GAQL queries against the relevant resource (campaign, ad_group, keyword_view) and joins the results in our own pipeline. We don't rely on Google's reporting API for derived metrics — every metric used in our dashboards is computed from the raw GAQL columns, which means we can re-derive history any time a definition changes.

Token cache, refresh policy, and quotas

Google Ads — token cache, refresh, and quota
ItemValue
Access-token cache50 min in-memory, per connection
Refresh policyOn 401, exactly one refresh attempt before failing the request
Daily quota15,000 ops / day (default standard access)
REST versionv18
Required scopehttps://www.googleapis.com/auth/adwords

Source of truth — internal + external

Internal: /docs/ADMAXXER-GOOGLE-SERVICE.md. External: Google Ads API getting started. GAQL queries validated against the Google-published GAQL reference; we always remember to divide cost_micros by 1e6 — that's the most common mistake when integrating Google Ads.

Token storage — AES-256-GCM with scrypt key derivation

Every credential — Meta access token, Google refresh token, OAuth secret, MCC login_customer_id — is encrypted with AES-256-GCM before it touches the database. AES-256-GCM is the industry-standard authenticated encryption mode; it gives us confidentiality (AES-256) and integrity (GCM tag) in a single primitive. Encryption keys are derived from a high-entropy passphrase via scrypt, which is deliberately memory-hard and CPU-bound; a leaked database alone never coughs up a usable key. An attacker would need both the encrypted column and the live process's encryption key — and the key never gets persisted.

Ciphertext is packed into a single column in the format iv:tag:ciphertext, base64-encoded. The IV is fresh per encryption call (12 bytes, drawn from crypto.randomBytes); the GCM tag (16 bytes) authenticates the ciphertext (any tampering causes decryption to throw). Reading a token always means decrypting on the way out — there is no plaintext column anywhere in the schema. Even backup snapshots only see encrypted bytes.

Plaintext lifetime

Plaintext only exists in memory during the lifetime of an outbound call to Meta or Google. It is never logged (the logger is configured to redact known credential field names), never serialized to disk, never sent to a third party, and never returned over a public Admaxxer API. Process restarts wipe memory; the only way back to plaintext is via the encryption key. We've audited the codebase to confirm there is no console.log of a token, no JSON serialization that includes a token, and no error path that surfaces a token in a stack trace.

Why not field-level encryption in Postgres?

Postgres has pgcrypto extensions for column-level encryption. We chose application-level AES-256-GCM instead for two reasons: (1) the encryption key never leaves the application process, so an admin with read access to the database can't decrypt — even with a SQL console; (2) we can rotate keys without an in-place column rewrite (see Key Source & Rotation below). The cost is one extra hop in application code, which is negligible compared to the network round-trip to Meta or Google.

Key source and rotation

The primary encryption key is the ENCRYPTION_KEY environment variable. As a fallback, Admaxxer derives a deterministic key from SESSION_SECRET so an unset primary doesn't crash the boot path. Production deployments must set both — the fallback is for boot-resilience, not a production posture.

Dual-key decrypt

Decryption tries the primary key first. If it fails, it falls back to the SESSION_SECRET-derived key. This means a key rotation never breaks existing connections — both keys work until you re-encrypt. New encryptions always use the primary key, so the fallback path retires itself naturally as data ages.

Re-encryption tool

The script scripts/re-encrypt-credentials.ts is dry-run by default; it prints what would change without writing. Pass --apply to commit. Run after every key rotation to migrate old-key ciphertext to new-key ciphertext. The tool is idempotent — running it twice on the same data is safe.

Token expiry monitoring

Long-lived Meta user tokens last ~60 days. A daily cron job at 09:00 UTC scans every active connection. Any token whose expires_at falls inside a 7-day window triggers a Resend email reminder — once per 24 hours per connection, so users aren't bombarded if the cron fires multiple times.

Once a token has expired, calls into Meta will fail with a typed error. The connection enters credentials_unreadable state, the UI surfaces a yellow banner, and the user is prompted to reconnect. The cron is forgiving but never silent: every transition into a failed state generates exactly one email.

Decrypt failure handling

If decryption fails — for example, because ENCRYPTION_KEY changed without re-encryption — a typed DecryptError is thrown by the token resolver and surfaced as HTTP 422 credentials_unreadable. The UI catches this and prompts the user to reconnect. There is no silent fallback to a stale state; we'd rather a clean error than a confusing partial failure.

Disconnect lifecycle — soft-delete with audit

Disconnect is a soft-delete with three guarantees: (a) we attempt to revoke on the platform side, (b) we tombstone the encrypted token so no one can resurrect it, and (c) we keep the row for audit history.

  1. Best-effort revoke (Meta). We call DELETE /{user_id}/permissions on Meta's Graph API. Failures (e.g. token already invalid) don't block the rest of the flow — we treat it as "best effort" because Meta sometimes 403s on already-revoked tokens.
  2. Tombstone the ciphertext. We overwrite the encrypted_token column with a sentinel value. Even with the encryption key, the original token is unrecoverable.
  3. Set deletedAt. The row stays for audit; queries filter on deletedAt IS NULL for active connections.

Workspace isolation — every connection scoped

Every connection is scoped to a single workspaceId. Every API route — read or write — verifies that connection.workspaceId === session.workspaceId. There is no cross-workspace read path; even an Admaxxer admin can't accidentally route a request through another customer's token. The Claude agent inherits the user's active workspace via session, so chat-driven tool calls are scoped identically.

This is enforced at the route layer (not just at the database layer) so a missing WHERE clause can't accidentally leak a token. Add a new route, you add the workspace check — no exceptions. The pattern is enforced by code review and a CI grep that flags any route handler that touches the adPlatformConnections table without a workspace filter.

Multi-tenant safety extends to the agent. The Claude agent can list, inspect, and (with explicit confirmation) modify campaigns — but only within the user's active workspace. There is no "admin override" tool, and no cross-workspace tool. If a user belongs to multiple workspaces (agencies often do), they explicitly switch workspace context before chatting; the active workspace is part of the session payload that the agent reads.

For full table relationships, indexes, and the adSyncLogs caching layer (which also respects workspace boundaries), see /docs/ADMAXXER-CONNECTIONS.md and shared/schema.ts.

What can the agent do with my connections?

The Claude AI agent (Sonnet, prompt-cached, streaming SSE) inherits the user's active workspace and reads from the same connection set the dashboard uses. The agent has six tools split across two surfaces:

This means the connection layer effectively gates every agent action. If your Meta token is expired and the connection is in credentials_unreadable state, the agent's read-only Meta tools fail with a clear error pointing the user to reconnect — not a confusing partial response. For the full agent surface and tool schemas, see Claude AI Agent.

Frequently asked

Do I need Facebook App Review or Google Ads developer-token approval?
No. The v1 paste-token model means you supply your own credentials — your long-lived Meta user token, or your Google OAuth refresh token plus your already-approved developer token. Admaxxer never goes through Meta App Review or asks you to wait on Google's developer-token approval queue.
How are my Meta and Google tokens stored?
AES-256-GCM with scrypt key derivation. Ciphertext is packed as iv:tag:ciphertext (base64). Plaintext only exists in memory during outbound API calls and is never written to logs, never sent to a third party, and never persisted to disk in plaintext form.
What happens when my Meta token expires?
Long-lived Meta user tokens are valid ~60 days. A daily cron at 09:00 UTC scans every connection; any token expiring inside a 7-day window triggers a Resend email reminder (throttled to one per 24 hours per connection). Once expired, the connection enters credentials_unreadable state and the UI prompts you to reconnect.
How do you handle Meta and Google rate limits?
Meta Marketing API: ~200 calls/hour/user-token. We auto-backoff on rate-limit error codes (17/32/613) and never spam. Google Ads: 15,000 ops/day default; access tokens cache for 50 minutes in-memory, and a 401 triggers exactly one refresh before failing the request. Ad-account safety is the #1 priority — we err on the side of caution every time.
Can I rotate my ENCRYPTION_KEY without breaking existing connections?
Yes. Admaxxer supports a primary key (ENCRYPTION_KEY) plus a fallback derived from SESSION_SECRET. During rotation, both keys decrypt — letting you re-encrypt at your own pace using scripts/re-encrypt-credentials.ts (dry-run by default; pass --apply to write). Once every connection is re-encrypted under the new key, you can retire the old one.
What does Admaxxer do when I disconnect an ad account?
Soft-delete. We call DELETE /{user_id}/permissions on Meta (best-effort), then overwrite the encrypted_token column to a tombstone value and set deletedAt. The row stays for audit history but the token is unrecoverable, even with the encryption key.
How many Meta or Google accounts can I connect per workspace?
Up to 10 active Meta connections per workspace by default (META_MAX_CONNECTIONS_PER_WORKSPACE). Google has the same envelope. Plan tier governs the absolute ceiling — Starter 2 connections, Pro 10, Agency unlimited (999 internal cap).
Can the Claude agent see or use my ad-platform connections?
Yes — every connection is scoped to your workspace, and the Claude agent inherits that workspace context. Read tools (list_campaigns, get_account_insights) require an active connection. Destructive tools (update_campaign, pause_all_low_roas) require both an active connection AND explicit user confirmation before they fire.

Next steps