# Unified advertising workflows

Kads has one business-level campaign service shared by its dashboard, REST API, TypeScript client, Ember and external MCP clients. Kads-owned native developer applications provide account access; no Zernio/Composio dependency is used.

## Public flow

1. Create a workspace API key in `/mcp`. Use `Authorization: Bearer …` on `/api/v1/ads`.
2. `POST /connections/authorize` creates a short-lived customer consent link for OAuth networks. Apple, ChatGPT Ads and Outbrain use native credentials through the private form or server-only credentials endpoint. Connections and accounts remain workspace-scoped; twelve networks does not limit account count.
3. `GET /accounts` lists the caller's available account UUIDs. `GET /workflows` lists supported unified formats, goals, budget periods and required platform fields. `GET /networks/{network}` describes additional native capabilities and catalogs.
4. `POST /validate-campaign` with `{ accountId, campaign }` validates and previews without persistence or native writes. `POST /create` prepares the same complete workflow. Use `Idempotency-Key` for creation retries. It returns a review URL and operation ID.
5. After review, `POST /operations/{id}/execute` with `{ "confirmed": true }` creates the budget/hierarchy/targeting/creative steps. Serving entities stay paused. The SDK's `executeWorkflow` continues explicitly checkpointed batches until completion. HTTP users continue only when the response says `status: prepared, nextAction: execute`.
6. `POST /activate` with `{ accountId, sourceOperationId }` prepares activation of all serving resources created by the completed workflow, including keywords. Review and execute that second operation to activate spend. Leaves are enabled before their campaign/container. Unrelated account entities are excluded. Taboola crawling must finish before activation.
7. Read status and native receipts using `GET /operations/{id}`. Use resource endpoints for subsequent edits and `insights` for reporting. These advanced native endpoints still use native field names and units.

The live campaign table's Create button opens a structured workflow form. Mock campaign-table interactions remain local previews. `/developers` serves the customer-facing guide and links to the OpenAPI contract.

## Example: complete Meta campaign

```ts
import { KadsAdsClient } from "src/lib/ads/client";

const kads = new KadsAdsClient("https://your-kads-host.example", process.env.KADS_API_KEY!);
const created = await kads.createCampaign(accountId, {
  name: "Autumn launch",
  goal: "traffic",
  budget: { amount: "50.00", currency: "AUD", period: "daily" },
  schedule: { startAt: "2026-10-01T00:00:00Z" },
  targeting: { countries: ["AU"] },
  creative: { existingId: approvedCreativeId },
  platform: {},
}, "autumn-launch-001");

// After the user has reviewed created.data.reviewUrl:
const receipt = await kads.executeWorkflow(created.data.id, true);
```

The request's `platform` object is optional over HTTP and in the SDK input type. The service parses its default `{}`. Use the public OpenAPI schema for all input fields.

## Historical format snapshot

For current supported formats and prerequisites, query `GET /workflows` and the selected network's capabilities. This older snapshot predates later format/media work; Taboola is no longer active. See the [current testing guide](developer-testing.md).

| Network | Orchestrated format | Supported goals | Creative prerequisite |
| --- | --- | --- | --- |
| Meta | Campaign → ad set → optional link creative → ad | traffic, awareness, conversions | Existing creative, or Page + image URL/hash + copy; conversions require pixel/event |
| Google | Budget → Search campaign → geography criteria → ad group → keywords → responsive Search ad | traffic | Three headlines, two descriptions, URL; explicit EU political-ad declaration |
| TikTok | Campaign → ad group → video ad | traffic | Processed video ID, authorized identity, copy, destination; start/end required |
| Snapchat | Campaign → ad squad → ad | awareness | Existing eligible Snap creative |
| Pinterest | Campaign → ad group → optional image Pin → promoted Pin | traffic, awareness | Existing Pin or board + image URL + copy |
| LinkedIn | Campaign group → campaign → ad creative | traffic, awareness | Authorized post URN, locale, native targeting criteria |
| Reddit | Campaign → ad group → ad | traffic | Existing completed post, funding instrument, pixel, native targeting |
| Amazon | Sponsored Products campaign → ad group → keywords → product ad | conversions | Seller SKU or vendor ASIN, eligible retail listing |
| X | Campaign → line item → targeting criteria → promoted Tweet | traffic | Existing promotable Tweet, funding instrument |
| ChatGPT Ads | Campaign → ad group → optional image upload → chat card | traffic, awareness, conversions | Image URL/file ID, copy, destination, native targeting; conversions event if applicable |
| Apple | App Store Search Results campaign → ad group → keywords → creative assignment | app_installs | App ID, existing custom product-page creative |
| Taboola | Campaign → URL item | traffic | Brand and URL; provider crawling produces initial creative |

Unsupported combinations fail before persistence. Additional format/media/native prerequisites can still be prepared through the resource API. This release does not implement every provider format or every Zernio endpoint: audience writes/lookalikes, lead forms/leads, conversion ingestion, full-tree duplication/deletion, resumable media, scheduled multi-network import remain gaps. Do not market this as full endpoint parity.

## Money, identity and schedules

Unified amounts are decimal strings in major account currency units. Native micros/minor units are computed with integer arithmetic; over-precision is rejected rather than rounded. Currency must match the account. Native payload endpoints remain in native units.

Targeting IDs and identities stay network-specific. Geographic shortcuts are documented per network, and unsupported shortcuts fail rather than widening targeting. Advanced targeting is bounded to its targeting field; it cannot inject workflow references or credentials. Existing creative content is inherited; conflicting creative fields fail. Amazon geography comes from the selected marketplace.

Schedules use ISO instants. Date-only APIs use the connected account's timezone. TikTok timestamps are normalized to UTC and ChatGPT timestamps are converted to Unix seconds. Native providers can further reject dates, bid/budget floors, targeting combinations and policy eligibility.

## Durability and retries

A workflow is an immutable versioned plan in the existing `ads_operations` ledger. Idempotency is scoped by workspace, user and credential. Reusing a key with a different account or plan returns 409. Keys remain bound for the operation record's lifetime.

Execution claims the operation with a conditional database update. Before each native write, Kads saves the step's intent. It then saves the provider response and extracted native resource ID before sending dependent writes. The saved plan resolves IDs only from successful earlier receipts. Network-specific response paths are used; parent/account IDs are never guessed as create results.

A short execution batch may checkpoint between steps and return `prepared` with `nextAction: execute`. The SDK and campaign form continue those checkpoints automatically under the same reviewed plan. They never retry a transport error or ambiguous provider write. The original 30-minute review expiry applies to continuations.

If the process dies during a write, the operation can remain `running` with an in-flight step. A malformed response or network failure results in `unknown`. Inspect the native account and saved receipts; automatic replay is intentionally refused. Recovery/reconciliation tooling is not yet implemented. No transactional rollback of external campaign creation is claimed.

## Verification

- `NODE_OPTIONS=--conditions=react-server npx tsx --test 'src/lib/ads/**/*.test.ts' 'src/lib/ads/*.test.ts'`
- `NODE_OPTIONS=--conditions=react-server npx tsx scripts/verify-advertising-workflows.ts`: real local Supabase, scoped service and generated API tool, provider adapter stubbed; verifies persistence, idempotency, tenant isolation and cleanup. Blocks non-local fetches.
- `node scripts/verify-advertising-api.mjs`: local HTTP authentication/tenant/connection checks.
- `node scripts/advertising-local-check.mjs build`: production build using local Supabase configuration.

No native account connection, publishing or delivery has been verified without the user's eligible credentials and developer-app approvals.


## Signed operation webhooks

`POST /webhooks` accepts `{ "url": "https://your-app.example/kads", "accountId": "OPTIONAL_ACCOUNT_UUID" }` and returns the signing `secret` once. Registration requires write access. The subscription is workspace-scoped (and account-scoped where supplied); subscriptions created with an API key stop delivering if that key expires or is revoked. `GET /webhooks` never returns secrets. `DELETE /webhooks/{id}` removes the subscription and queued deliveries. `GET /webhooks/{id}/deliveries` lists the latest 100 receipts.

Events cover `operation.prepared`, `operation.running`, `operation.succeeded`, `operation.unknown` and `operation.cancelled`. A database trigger inserts them in the same transaction as the operation status change. Event payloads contain operation/account/network/status identifiers, not native creative bytes or stored credentials. Fetch the operation through the authenticated API for its full receipt.

The worker uses `/api/internal/advertising/events`, protected by the existing `CRON_SECRET` (or `SYNC_CRON_SECRET`). `vercel.json` schedules it every minute on deployments supporting that interval. Self-hosted installs must invoke it on the same cadence. It processes up to ten due deliveries per call, retries transient failures up to eight attempts with exponential backoff, reclaims expired five-minute delivery leases and records results. Retries are at-least-once; deduplicate by `Kads-Event-Id`. HTTP 4xx other than 408/429 is terminal. Notification retries never repeat advertising writes.

Headers:

```text
Kads-Event-Id: EVENT_UUID
Kads-Signature: t=UNIX_SECONDS,v1=HEX_HMAC_SHA256
```

Verify the HMAC over the exact string `timestamp + "." + eventId + "." + rawRequestBody`, using the returned secret as the UTF-8 HMAC key. Use a constant-time comparison, allow at most five minutes of clock skew, and store event IDs to deduplicate. Signatures bind the exact request bytes. `src/lib/ads/webhook-transport.ts` includes the verifier used by the tests.

Destinations require HTTPS on port 443 with a public IPv4 DNS record. Credentials/fragments, literal IP hosts, private/reserved DNS addresses and redirects are rejected. Delivery pins the verified address while preserving hostname/TLS certificate validation. Webhook registration is intentionally excluded from MCP because it returns a private signing key.


### Notification operations

Webhook delivery can be paused, resumed, tested and manually redelivered through the API and Integrations controls. Paused subscriptions retain pending events without consuming retry attempts. Resume makes queued work eligible again. Each outbound attempt is stored independently, so manual redelivery preserves the previous success/failure history. A test event has no advertising operation ID; consumers must accept that field as absent/null. Redelivery retains the event ID for receiver deduplication and never reruns an ad write. See the [API routes](public-api.md#bulk-controls-key-administration-and-webhook-operations).
