# Competitor ad discovery

Discovery uses Meta's official Ad Library API for ads delivered in the EU and UK. Search results contain ad text and public Meta links. When separately enabled, inline previews render Meta's public Ad Library webpage to retrieve available media. This webpage extraction is separate from the documented Graph API fields and does not use an additional provider. The interface labels metadata as last observed and does not infer ad performance from delivery dates.

Search, preview, saving, and preparing adaptation briefs use no generation credits. **Create for my brand** is available on cards and in the Details drawer in Discover and Saved ads. It opens a shared recreation draft where users choose [Image ad](#create-an-image-for-your-brand), [Talking Head UGC, or Video Hook & Demo](#create-a-video-for-your-brand). Each workflow uses the workspace's brand information and language, retains source-ad provenance, and opens its existing editor for editing and export.

Ads > Create ads also provides image and video templates for starting a new ad. Image and Talking Head generation follow their existing credit rules; Hook & Demo assembles the user's uploaded clips without generation credits.

## Create an image for your brand

1. Call `POST /api/v1/ads/recreations` with `requestId` (a UUID), `adId`, and exactly one of `resultToken` or `savedId`. Source metadata is resolved on the server. Reuse the request ID if the response is lost. To make another version of a generated adaptation, supply its `contentId` instead of the ad reference.
2. Read the draft with `GET /api/v1/ads/recreations/{id}`. It contains immutable `source` metadata, editable `settings`, its `revision`, language, and `lastContentId`. The source observation survives expired searches, deleted saved references, and removed Meta ads.
3. Edit settings with `PATCH /api/v1/ads/recreations/{id}`, supplying the last `revision` and the full `settings` object. Settings contain `brief` (`hook`, `benefit`, `visualDirection`, `supportingText`, `ctaText`), `ratio` (`1:1`, `4:5`, `9:16`), `style`, `accentColor`, `productImageUrl`, and `referenceImageUrl`. Stale updates return 409 instead of overwriting another editor. The dashboard autosaves and keeps unsaved keystrokes in tab-local storage for reload recovery.
4. Call `POST /api/v1/ads/recreations/{id}/brief` with `revision` to draft/refine the five fields from current brand facts, tone, design guidelines, and language. Add product information on the Brand page first. An optional uploaded layout reference enables visual analysis; Meta preview links and CDN assets are rejected as generation inputs. Review all claims and copy before proceeding.
5. Generate through the existing `POST /api/v1/content/meta-ad` operation using `workspaceId`, `recreationId`, `recreationRevision`, and a fresh generation `requestId`. Saved settings override client creative fields. Brief validation runs before plan/library/credit preflight. Image generation costs the existing 3 credits. Poll the returned Content resource and use its normal download/export flow. Reuse the exact generation request ID on retries; a pending request returns 409, while an accepted request returns its existing job without another debit. A deliberately new image requires a new request ID.

Draft create and AI-brief operations need `ads:read` and `ads:write`; read needs `ads:read`; edits need `ads:write`. Generation retains `generations:write` and checks draft ownership independently. All explicit workspace IDs are team-validated for HTTP, CLI, and MCP.

Generated Content retains a source snapshot, the reviewed settings, language and draft revision under `adRecreation`. Reopening a generated image shows its original brief. **Create another version** forks that snapshot into a new editable draft. Competitor text is untrusted inspiration, not evidence for the user's offers or capabilities; a separate brief check rejects unsupported claims before spending credits. The image prompt uses only supplied brand assets and asks the provider to use layout references for composition without copying names, products, faces, logos, claims or offers. Users should review the rendered image before export.

```sh
viraloop ads recreate --ad-id AD_ID --saved-id SAVED_ID --request-id DRAFT_UUID --workspace WORKSPACE_ID --json
viraloop ads recreation DRAFT_ID --workspace WORKSPACE_ID --json
viraloop ads update-recreation DRAFT_ID --revision 0 --settings-file settings.json --workspace WORKSPACE_ID --json
viraloop ads draft-brief DRAFT_ID --revision 1 --workspace WORKSPACE_ID --json
viraloop content meta-ad --recreation-id DRAFT_ID --recreation-revision 2 --request-id IMAGE_UUID --workspace WORKSPACE_ID --json
```

`AdRecreation` stores brief drafts outside the Content library allowance. `AdRecreationRequest` reserves each paid request atomically using its unique deterministic `_id`; reservations do not expire. Content uses that same ID for durable replay protection. Known preflight rejections release the reservation. Failed record creation refunds before releasing it; provider failures use the existing once-only refund and render recovery. An ambiguous debit/database failure retains the reservation for operator reconciliation rather than allowing another charge. Inspect its `creditHistoryId` and matching Content before resolving a stranded request; do not clear a paid reservation without reconciling the debit.

Credential-free `[ad-recreation]` events include `draft_created`, `brief_prepared`, `image_started`, and `image_ready`, with draft, team, workspace, source ad IDs and whether the source was saved. Count unique source/draft IDs when measuring saved-reference-to-generated-image conversion. Brief text, asset URLs, tokens, and brand facts are excluded from these events. No production rollout or paid live test is implied by mocked local validation.

## Search and save

Use `GET /api/v1/ads/capabilities` to check availability. Search through `GET /api/v1/ads/search` with either `q` (ad text, up to 100 characters) or `pageId` (a numeric string). Filters are `country` (an EU/UK country code or `EU_UK`), `mediaType` (`ALL`, `IMAGE`, `VIDEO`), `status` (`ACTIVE`, `INACTIVE`, `ALL`), and `platform`. Defaults are all EU/UK countries, active ads, all media, and all platforms.

Responses contain `data.items`, `data.resultToken`, and `pagination.nextCursor`. Keep search filters unchanged when passing `cursor`. A page contains at most 25 records; counts are loaded records, not a guaranteed total. Duplicate Library IDs are collapsed. No cursor means Meta returned no next page.

To save a result, call `POST /api/v1/ads/saved` with `adId`, the `resultToken` from the page that contained it, and optional `workspaceId`. Metadata comes from the server cache. The same workspace/ad pair is idempotent. Search again if its result token expires. List references through `GET /api/v1/ads/saved`, optionally filtering by `pageId` and `page`; remove with `DELETE /api/v1/ads/saved/{id}`.

All endpoints accept `workspaceId`; explicit IDs are strictly checked against the authenticated team. API keys need `ads:read` for discovery/listing and `ads:write` for saving/removing. Existing scoped keys are not automatically upgraded.

The UI also parses Meta Ad Library advertiser URLs (`view_all_page_id`) and keyword URLs (`q`). Pasted URL country filters do not override the selected EU/UK filters. Individual-ad URLs open on Meta; the API does not silently scrape them. Name lookup uses separately approved Pages Search through `GET /api/v1/ads/advertisers?q=...`. A name match does not guarantee that the page has eligible ads.

```sh
viraloop ads capabilities
viraloop ads search "video editing" --country GB --media VIDEO --json
viraloop ads search --page-id 123456789 --workspace WORKSPACE_ID --json
viraloop ads save AD_ID --result-token RESULT_TOKEN --workspace WORKSPACE_ID
viraloop ads preview AD_ID --result-token RESULT_TOKEN --workspace WORKSPACE_ID --json
viraloop ads preview AD_ID --saved-id SAVED_REFERENCE_ID --workspace WORKSPACE_ID --json
viraloop ads saved --workspace WORKSPACE_ID
viraloop ads remove SAVED_REFERENCE_ID --workspace WORKSPACE_ID
```

The corresponding `viraloop_*` MCP tools and OpenAPI operations derive from the same registry. CLI/MCP, session, and API-key requests share team discovery budgets.

## Inline creative previews

Check `data.mediaPreviews` on the capabilities endpoint. `GET /api/v1/ads/preview` requires `adId`, the selected `workspaceId`, and exactly one of `resultToken` or `savedId`. A saved reference must belong to that team/workspace and match the ad ID; an expired or foreign search receipt cannot authorize retrieval. Client-supplied URLs or ad objects are rejected.

The response has `status` (`ready` or `unavailable`), `items` with `type` (`image` or `video`), `url` and optional `posterUrl`, plus `fetchedAt`, `expiresAt` and `cacheHit`. Only validated HTTPS Meta CDN media URLs are returned. They are temporary signed media links, not operator credentials. They are not durable generation inputs or a complete list of every dynamic creative variant. Never infer copy-to-media associations from their order. No raw Meta HTML, snapshot link, or token is sent to the browser.

Cards show a contained 384px creative preview with the advertiser and Save button overlaid above the media, two lines of copy, and Create for my brand, Details, and View creative on Meta actions. Library ID, dates, observed status, media filter, platforms, complete copy variants, and advertiser search are available in Details. Loading and unavailable previews reserve the same space as loaded media.

Previews load through a priority queue with at most two requests in flight per mounted workspace: the open drawer first, then visible cards, then cards within 200px of the viewport. Moving away removes unstarted work; completed and already-running requests remain shared with the drawer. Waiting previews say "Waiting for preview" and only started requests say "Loading creative". Videos start only on user interaction. Unavailable creatives retain their public Meta link. Broken assets can request a refresh (`refresh=true` or CLI `--refresh`), with a 60-second minimum interval for a fresh cached record. Successes cache for up to 15 minutes, shortened for known CDN expiry; unavailable previews cache for two minutes. Media is not persisted into saved references. A bounded team/workspace session cache retains validated preview URLs only until their original expiry; expired search references renew independently of the visible cards.

Preview rendering has its own configurable allowance: 250 fresh renders per team/UTC day, 30 per minute, 120 per hour across the deployment, and at most two concurrent renders. Cached repeats do not consume rendering allowance. These are separate from the official API request limits and do not spend generation credits. Redis must enforce leases and budgets before rendering; an outage pauses previews without affecting saved metadata. A server process reuses Chromium but creates and closes a separate browser context for every ad. It closes an idle browser after 60 seconds and recycles it after 40 uses or five minutes once active renders finish. Rate limits include retry/reset information. Login walls, challenges, missing media, browser timeouts and changed Meta markup fall back to the public Meta link; the service does not try to bypass them.

## Limits, retention, and failures

Result pages are cached for 15 minutes; advertiser name lookups for 24 hours. Cached repeats do not use the upstream allowance. Defaults: 50 upstream requests per team/UTC day, 10 per minute, 150 per hour across the service, and 500 saved references/workspace. Pagination and advertiser lookups consume the same upstream allowance. Requests are reserved atomically before the provider call, including provider failures. There are no automatic upstream retries that silently spend extra allowance.

Opaque cursors are tied to team, filters, and provider configuration. Search receipts are team-bound and refer to a cached page. Shared public cache entries do not contain saved IDs or workspace information. Switching workspaces clears active requests and scopes browser restoration to the selected team/workspace.

When Redis is unavailable, fresh searches and new saves pause; saved listing/removal continue through MongoDB. Meta quota responses create a shared cooldown and return `Retry-After` plus `error.retryAfterSeconds`. An app-usage header at 90% also applies a cooldown. Expired tokens, missing permissions, provider failures, unconfigured access, and expired cursors have distinct error types and recovery hints.

Saved references keep the normalized observation and source link until removed. They are not a live mirror or a backup of competitor media. Removed/expired Meta sources can leave a reference whose public preview no longer works. No background refresh or competitor monitoring runs in this release.

## Create a video for your brand

The card and Details action opens `/ads/recreate/{id}`. Choose **Image ad**, **Talking Head UGC**, or **Video Hook & Demo**. Image drafts created before format selection continue to work. Changing format retains the five-field brief and image references; it starts new format-specific settings.

For a video, PATCH the full settings with `format: "talking-head"` or `"hook-demo"`. Draft/refine the brief to receive an editable `script` and `scenes` array (`visual`, `duration`). Review brand claims, language, and timings. Scene durations must add up to `duration`.

- Talking Head uses `presenterImageUrl`, optional `productImageUrl`, `mode` (`speaker` or `scene`), and `captionOverlay`. It generates a 9:16 video of 4–15 seconds, using the existing 5 credits/second rate. Only the user's presenter/product assets become generation references. Reviewed scene directions feed the existing video prompt.
- Hook & Demo uses `hookAssetId` and `demoAssetId`, both uploaded video Assets in the selected workspace. Provide exactly two scenes, up to 30 seconds each and 60 seconds overall. Each clip must cover its planned scene duration. It keeps the clips' existing audio and adds the hook as editable text, without generation credits. The script guides recording; this format does not synthesize footage or a new voiceover.
- Optional `referenceImageUrl` and `referenceVideoAssetId` guide composition during drafting. Video analysis uses one uploaded preview frame only, not motion, speech or timing. Meta pages and media URLs are not generation inputs.

Call `POST /api/v1/ads/recreations/{id}/video` with `workspaceId`, the reviewed `revision`, and a UUID `requestId`. Required scopes: `ads:read` and `generations:write`. The server resolves the owned draft and assets, validates the brief, then applies existing plan/library/credit rules. Returns a Content resource; poll its `statusUrl` and open `/content?contentId=ID` to edit/export. Hook & Demo returns an editable deck; MP4 export still happens in its existing editor.

```sh
viraloop ads create-video DRAFT_ID --revision 2 --request-id VIDEO_UUID --workspace WORKSPACE_ID --json
```

The dashboard retains unsaved edits and the generation request ID across reloads. An interrupted request shows **Check generation** and retries the same ID. Do not submit a new ID to work around a pending request. Known preflight errors allow edits; ambiguous server/network failures retain the ID to prevent duplicate charges. Render failures use the existing recovery/refund path. Both video editors display the immutable source, observation date, original brief/script, and **Create another version**. The latter forks the original snapshot rather than silently changing its provenance.

Lifecycle logs additionally include `video_started` and `video_ready`. Hook & Demo `video_ready` means the deck exists, not that an exported MP4 exists. Discovery and brief drafting remain free of generation credits. Provider billing for briefing is an operator expense.
