# Advertise on Perchance This guide is published only at https://ad-manager.perchance.org/llms.txt. It describes the native Perchance API, not AdCP. Use only the advertiser's authorized budget and credentials. ## Start without a native access key An agent can prepare a campaign and fund it with an authorized coupon through the anonymous submission API. No login, Bearer key or Turnstile is required for this workflow. Use the advertiser's email so they can later sign in to manage the campaign. Checkout is unavailable. All requests below go to `https://ad-manager.perchance.org`. Send `Origin: https://ad-manager.perchance.org` on every submission request; it is **required** for POST and DELETE, including requests from scripts. JSON bodies use `Content-Type: application/json`. Upload bodies are raw canonical JPEG/MP4 bytes, not JSON or multipart data. 1. Prepare the submission with `POST /api/submissions`. Save a fresh UUID and the complete body before sending; reuse that exact body on a transport retry. `budget` and `dailyCap` are **USD decimal strings**, unlike the native commands' integer micro-USD fields. The daily cap is at most USD 100. Frequency is `hour` (default) or `day`. Replace the example dates with the intended flight. Both timestamps must be exact UTC ISO strings **with three millisecond digits and Z** (`YYYY-MM-DDTHH:mm:ss.sssZ`, as produced by `Date.toISOString()`); the flight must be positive and at most 366 days. ```http POST /api/submissions HTTP/1.1 Host: ad-manager.perchance.org Origin: https://ad-manager.perchance.org Content-Type: application/json { "email": "advertiser@example.com", "landingUrl": "https://example.com/game", "budget": "2.00", "dailyCap": "1.00", "frequency": "hour", "placements": ["footer_desktop"], "audience": {"kind": "all", "generators": []}, "idempotencyKey": "1ce6c4ba-17ad-41f6-aed1-d90febdbec45", "startAt": "2026-10-01T00:00:00.000Z", "endAt": "2026-10-08T00:00:00.000Z" } ``` For selected pages use `audience: {"kind":"generators","generators":["generator-slug"]}` with a nonempty array of lowercase slugs. For all pages the array must be empty. This entry point takes `audience`, not the native command's `targeting` object. Add `footer_mobile` to `placements` only if you will upload a creative for it. Save the returned `submissionId`, secret `clientKey` and `campaign._id`; preparing a campaign does not spend money. 2. Upload at least one creative for each selected placement. Generate a fresh UUID as `uploadId`, and preserve it, the placement and identical bytes for retries. Use `image/jpeg` for a JPEG or `video/mp4` for a canonical silent MP4; the media requirements and conversion command follow below. ```http POST /api/submissions//creatives?placement=footer_desktop&uploadId= HTTP/1.1 Host: ad-manager.perchance.org Origin: https://ad-manager.perchance.org X-Submission-Key: Content-Type: image/jpeg ``` Save the returned `creativeId` and `jobId`. Poll the submission's creative-status route to read the review job; the anonymous key cannot use native `get_job`: ```http GET /api/submissions//creatives/ HTTP/1.1 Host: ad-manager.perchance.org Origin: https://ad-manager.perchance.org X-Submission-Key: ``` Wait for `job.state: "complete"`, `verdict: "approve"` and `publication: "published"` for every selected placement. `manual_review` needs owner action; `rejected` needs a replacement; inspect `job.error` for `failed` work. To replace a creative before finalizing, DELETE that same URL with the same Origin and `X-Submission-Key` headers, then upload with a new `uploadId`. 3. Finalize the creative set. Here the submission key is in the JSON body: ```http POST /api/submissions//finalize HTTP/1.1 Host: ad-manager.perchance.org Origin: https://ad-manager.perchance.org Content-Type: application/json {"clientKey":""} ``` 4. Fund it with a coupon the advertiser has authorized you to use: ```http POST /api/submit HTTP/1.1 Host: ad-manager.perchance.org Origin: https://ad-manager.perchance.org Content-Type: application/json {"submissionId":"","clientKey":"","coupon":""} ``` Save the returned `campaignId` and `status`. This allocates funds and may activate the campaign; respect the intended budget before sending it. Finalize/fund use the submission's saved operation identity: retry the exact request after a lost response, without inventing an idempotency field. Without a coupon, sign in as the same advertiser to authorize wallet funding; the anonymous `clientKey` cannot spend an existing wallet. A scheduled or review-blocked campaign is not yet live. The `clientKey` authorizes only this submission. It is **not a native access key**: using it as a Bearer credential on `/api/v1/commands/*` or `/mcp` returns HTTP 401. Keep it private and use the following sign-in flow for account-wide reads, reports and ongoing management. ## Obtain a native access key 1. Open https://ad-manager.perchance.org/portal in a real browser and request a sign-in link for the same advertiser email. Complete Turnstile, read the email and open its single-use link. An agent with authorized browser/email access can perform this step too; no owner-issued key is necessary. The portal exchanges the email token at `/api/auth/consume` for an advertiser session. 2. In **Agent access keys**, choose a label, permissions and a lifetime allocation ceiling, then click **Create key**. The portal uses the authenticated session to call native `create_key` and issues a key that expires after 90 days. Copy the returned token into the agent's secret storage; the portal shows it only once. A zero allocation ceiling suits a reporting-only key. 3. Send that access key as `Authorization: Bearer ` on native REST and MCP requests. Access keys cannot create, rotate or revoke other keys; key management requires the advertiser's signed-in session or an owner. Write scopes do not include read permission. These are the complete access-key scope names from `SCOPES`: | Scope | Authority | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `reads` | Required for the account/catalog/campaign/creative/job/funding/ledger/feedback `get_` and `list_` commands, including polling `get_job`. | | `creatives:write` | Upload, import and remove creatives; retry/cancel creative jobs. | | `campaigns:write` | Create, edit, finalize, pause, resume and cancel campaigns. | | `funds:allocate` | Allocate wallet funds with `set_budget`, within the key's ceiling. | | `funds:redeem` | Redeem coupons into the advertiser's wallet. | | `feedback:write` | Submit advertiser performance feedback. | | `reports:read` | Read `get_report`, including report pages and CSV. | Include `reads` for the `get_`/`list_` calls in the workflows below; a reporting workflow normally needs both `reads` and `reports:read`. The command-specific exceptions are `get_report`, which uses `reports:read`, and `list_keys`, which requires a signed-in account session instead of an access key. Ask for the least authority needed. Returns do not replenish a key's lifetime allocation ceiling. Turnstile applies only to requesting the account sign-in email. Submissions, uploads, coupons and native commands do not require it. Sign-in requests have a cooldown; other account operations have generous volume limits. On HTTP 429, honor `Retry-After` and retry the saved operation instead of issuing parallel retries. ## Native REST and MCP The human console is https://ad-manager.perchance.org/portal. Connect an MCP client to `https://ad-manager.perchance.org/mcp`, with `Authorization: Bearer ` on **every** request. Use MCP protocol `2025-11-25`, POST JSON, and `Accept: application/json, text/event-stream`. This stateless server returns JSON: no sessions, server events or GET stream. Initialize, then discover `tools/list`. Never send this credential to creative URLs, landing pages, snippets or other hosts. A missing Origin is valid for a machine; an explicitly supplied Origin must be `https://ad-manager.perchance.org`. REST uses the same commands at `POST /api/v1/commands/` with JSON and the same Bearer. `GET /api/v1/schema` describes available commands. Resource routes also exist, including `GET /api/v1/catalog`, `/account`, `/campaigns`, `/campaigns/`, `/jobs`, and `/jobs/`. Unknown fields fail; do not invent targeting or payment options. Bounded namespaced `ext` annotations carry your labels only and never change delivery or billing. Native command money is **integer micro-USD**: $1 = 1,000,000. Read `get_catalog` before preparing a campaign. The product is static or animated footer display, priced per thousand **accepted viewable impressions**, with a frozen campaign price version. A rate of 100,000 micro-USD CPM costs 100 micro-USD per accepted viewable impression. A view requires at least 50% of the creative visible for one continuous second. Renders and clicks are unpaid; playing, finishing or failing a video does not create another billable impression. Supported placement IDs are `footer_desktop` (728×90 slot) and `footer_mobile` (320×50 slot). Static JPEGs are at most 307,200 bytes, between 32×16 and the slot's 2x dimensions (1456×180 or 640×100). Preserve aspect ratio, normalize orientation to 1, and avoid upscaling small images. Animated creatives must be **H.264/AVC MP4**, `avc1`, 8-bit `yuv420p`, progressive SDR, square pixels, normal rotation, with the `moov` header before media data (`faststart`). Use Baseline, Main or High profile at level3.1 or lower; exactly one video track, **no audio/subtitle/data tracks**. Dimensions must be exactly 728×90 or 1456×180 for desktop; 320×50 or 640×100 for mobile. Limits: 15 seconds, 25 fps, 2,097,152 bytes, average bitrate at most 8 Mbps. Use a zero-based timeline with every frame lasting at least 40 ms. The ad plays once, muted and inline, then freezes on a server-derived final frame. The server derives its own first-frame poster and final-frame JPEG. Never supply your own stills. API/MCP clients must convert before upload. For a desktop rendition: ```sh ffmpeg -i input.mov -map 0:v:0 -an -sn -dn -t 15 -vf "fps=25,scale=1456:180:force_original_aspect_ratio=decrease:force_divisible_by=2,pad=1456:180:(ow-iw)/2:(oh-ih)/2:color=white,setsar=1,format=yuv420p" -c:v libx264 -profile:v main -level:v 3.1 -preset veryfast -crf 23 -maxrate 900k -bufsize 900k -map_metadata -1 -movflags +faststart creative.mp4 ``` For mobile, replace both `1456:180` occurrences with `640:100`. Check the output byte size before uploading. This command trims to the first 15 seconds and removes sound. Animated WebP requires an FFmpeg build with animated WebP decoding, or the browser form. The form converts MP4, WebM, MOV, GIF, animated WebP and APNG locally; it downloads the converter only for animation. Source files must be below 64 MiB. Conversion may take a minute on mobile devices. Static images keep the lighter image-conversion path. Video is never transcoded by the server. Every decoded frame undergoes flash analysis; exceeding general-flash or saturated-red thresholds rejects the upload. Avoid more than three flashes per second, including on a small banner. Creatives are reviewed by the operator. The manifest includes `kind`, `durationMs`, `poster`, `endFrame`, `videoMain`, `renditionProfile`, `measurementPolicy` and optical `statistics`. Statistics include brightness, luminance over time, motion, flash counts and dominant colours. They do not change pricing or delivery. Creative identity is separate from its asset hash; changing a destination or media requires a new approved manifest. The only targeting selector is `targeting: {"generators": ["generator-slug"]}`; an empty array means all eligible pages. Daily caps are at most USD 100/day (`100000000` micro-USD). Frequency is `hour` (default, once per hour) or `day` (once per day). Browser-local frequency is best effort and may reset with storage; server abuse guards can also refuse billing. Clicks are the supported optimization goal. Checkout is unavailable. Funding comes from wallet or coupons; coupon money remains promotional. A key needs `funds:redeem` to redeem and `funds:allocate` to allocate. Its lifetime positive-allocation ceiling is separate from the wallet balance and the account ceiling. Returns do not replenish key authority. Every native mutation needs a new random `idempotencyKey` (UUID is suitable). Save the exact request **before** sending it. On a timeout or retryable failure, repeat that exact request and key. Do not replace the key merely because the response was lost. Replays return the original application response, even if the campaign has since changed. MCP wraps it in the current JSON-RPC ID. Ordinary responses replay for 30 days; credential/coupon secret responses for one hour. Expired operations cannot be executed again under the same identity: inspect current state before deliberately issuing a new command. Never log returned secrets. Machine edits require `expectedRevision` from a campaign read or the last mutation response. On `revision_conflict`, read the campaign, compare the user's intended change, and send a new request/key only if the change is still wanted. Accounting, review and clock transitions do not increment this control revision. Structured errors include `error`, `pointer`, `recovery` and, where relevant, `currentRevision`, `retryAfter` or `minimumMicros`. ## Workflow: prepare, review, fund and start 1. Call `get_catalog` and `get_account`. Confirm the user's total/daily budget and authority. 2. `create_campaign` with a UUID key, `name`, HTTPS `landingUrl`, `placements`, `budgetMicros`, `dailyCapMicros`, optional `frequency` (default `hour`), `targeting`, and exact UTC `startAt`/`endAt` (`YYYY-MM-DDTHH:mm:ss.sssZ`, positive flight up to 366 days). New machine campaigns are paused. Creation declares a budget; it does not allocate wallet money. 3. For each placement, call `upload_creative` with a new key, `campaignId`, `expectedRevision`, `placement`, `kind: "image" | "video"`, and canonical `dataBase64` JPEG or MP4 bytes. REST alternatively accepts raw bytes at `POST /api/v1/campaigns//creatives?placement=footer_desktop`, with `Content-Type: image/jpeg` or `video/mp4`, `Idempotency-Key` and numeric `If-Match` revision headers. Save the returned campaign revision and `jobId`. 4. Poll `get_job` by ID. Wait for `complete`; `manual_review` needs owner action, `rejected` needs a replacement, and failure details require explicit recovery. Approval alone does not prove publication. The exact reviewed rendition must be published before delivery. 5. `finalize_campaign` with ID/revision/new key. Read `get_campaign` for current blockers. 6. If using a coupon, `redeem_coupon` with `code` and a new key. Then `set_budget` with the campaign ID/revision, target `budgetMicros` and a new key. This atomically allocates the delta from available wallet funds. Fund only within the user's authority. 7. `resume_campaign` with ID/revision/new key. Inspect `blockers` and `deliverable`. Clearing buyer pause never clears owner suspension, review/publication blockers, flight or funding. ## Workflow: report, adjust and finish 1. `get_report` with a campaign ID; `list_creatives` returns assignments even before delivery. Treat reports as delayed aggregates, not a live billing feed. Use the report's currency and event-cohort fields. A paid impression requires at least half of the creative visible for one continuous second. Renders and clicks are unpaid measurements; CTR uses clicks associated with paid impressions. Historical paid counts and spend retain their original basis as disclosed by the report billing-policy change date. 2. To stop new delivery, `pause_campaign` with the current revision and a new key. Already issued claims may still settle. Read outstanding reservation amounts before reducing a budget. 3. `set_budget` sets the **total allocation target**, not a top-up amount. A decrease cannot cross settled plus outstanding spend. On `budget_reserved`, keep paused, allow quotas to drain, read current state and retry with an intentional new request. Daily caps have the equivalent floor. 4. `cancel_campaign` is terminal. It returns unused unreserved allocation to the wallet, then returns the remainder after outstanding quotas close. Flight end also returns unused funds. These are provenance-preserving wallet transfers, not cash refunds. Read the campaign and `get_account`/`list_funding_lots` to observe completion. Do not try to bypass a ceiling by rotating keys or splitting operations. Performance feedback does not change bills or trusted CTR. To import a source URL, check `get_catalog.product.ingestion.available`, then call `ingest_creative` with `campaignId`, `expectedRevision`, `placement`, `sourceUrl`, `kind: "image"` and an idempotency key. The URL must be public HTTPS and directly return JPEG, PNG or WebP; redirects and video are rejected. Limits are 8 MiB compressed, 16 million decoded pixels and 8192 pixels per side. Only the first frame is used, with orientation applied, transparency flattened to white, metadata removed, and contain sizing without upscaling. Review and publication bind the final JPEG. Find jobs using `list_jobs` (optional `campaignId`, `state: "open" | "failed" | "all"`, `limit` and `after`). Continue with `nextCursor` until null. The default open list includes failed work; review replaces ingestion under the same job ID. Job responses omit source URLs, object keys and lease secrets. Poll `get_job` using the returned `jobId`. Download and conversion states precede review/publication; finalization waits for ingestion to finish. `cancel_job` cancels an ingestion job before review begins. Once a creative exists, use `remove_creative` on its draft campaign. `retry_job` restarts only a failed job whose `error.retryable` is true. A retry reuses its committed private source; use a new ingestion request to fetch changed source bytes. When URL ingestion is unavailable, upload a prepared JPEG. For animation, use `upload_creative` with prepared MP4 bytes; URL imports are still a static-image workflow. Reports use half-open UTC **calendar dates**, for example `get_report({"from":"2026-09-01","to":"2026-09-08"})` covers September 1–7. Use `YYYY-MM-DD`, a positive window of at most 31 days, and no end beyond tomorrow's UTC midnight. The default is the current UTC day and six preceding days. Optional filters are campaign, creative, country, device and placement. `dimensions` is a comma-separated subset of `campaignId,creativeId,day,country,device,placement`. Zero-delivery assignment history is included, including removed draft assignments. `isFinal` becomes true only when the durable accounting watermark closes the whole window. `dataThrough` is its exclusive completeness boundary within the window, or null when none has closed. A provisional report can contain newer persisted counts. An outage or unresolved settlement prevents finality from advancing. A restart keeps committed counts and does not replay unflushed events as charges. `reportRevision` is an opaque digest of the report contents/finality. Do not infer finality from elapsed wall time or an empty report. Pages contain at most 100 rows. Continue with `get_report({"cursor":"..."})` until `nextCursor` is null; totals always describe the whole report. Pages retain the same snapshot for ten minutes even while delivery continues. Start again after `report_expired`. Use `format: "csv"` for CSV pages and omit each subsequent page's header when assembling a file. A fresh identical query may reuse a snapshot for 15 seconds. Snapshot construction and storage have explicit capacity limits; honor retry guidance when busy. To supply advertiser outcomes, call `provide_performance_feedback` with a stable `observationId`, `observationRevision: 1`, `campaignId`, `from`, `to`, `metric`, `performanceIndex`, `measurementSource`, and an idempotency key. Optional fields are `baseline`, `observationCount`, `confidence` (0–1), and the pair `creativeId` / `creativeRevision`. Missing baseline/count/confidence remain unspecified. Index 0 means no outcome, 1 means the supplied baseline, and above 1 means above baseline. For a correction, reuse the observation ID, increment its revision, and use a new idempotency key. The complete new observation replaces the old one. Read it using `list_performance_feedback`. Separate overlapping observations are displayed without addition or averaging. Feedback is stored and displayed only; it never changes billable counters, price or the measured-click optimizer.