# Affiliateo > Affiliateo is an all-in-one platform where businesses launch affiliate programs for their web and mobile apps, sell products with built-in checkout, manage their paid ads (Meta, Google, TikTok, Apple Search Ads, Snapchat) with true click-to-revenue ROAS, and pay affiliates worldwide. Affiliates browse a marketplace of programs, promote with tracked links, and withdraw earnings globally. Stripe is the merchant of record; tracking SDKs exist for web, iOS (Swift), Android (Kotlin), Flutter, and React Native. Key facts: - Businesses install "apps" per workspace: Affiliate, Clipping, Web Traffic, Collab, Meta Ads, Google Ads, TikTok Ads, Apple Search Ads, Snapchat Ads, Calendar, and more. - One Affiliate app covers a website, a mobile app, or both, on a single app id with a single referral link that routes by device (iOS to the App Store, Android to the Play Store, everything else to the website). What the app can do is decided by which providers are connected, not by the app type, and a second rail can be added later by editing the app. The type is named `affiliate_app` — one name, sent and read back. Never infer rails from it. - Affiliate attribution differs by rail and the difference is real: web is DETERMINISTIC (the `?ref=` code is stored first-party on the merchant's own domain and read back at checkout), mobile is PROBABILISTIC (the App Store strips every URL parameter, so a fresh install is matched to a click within the last 7 days by IP/subnet, OS, country and recency). Expect some mobile installs to go unattributed. Maximum commission is 70% on store-mediated sales (Apple in-app purchases, Google Play, RevenueCat, Adapty — the stores take up to 30% off the top) and 90% everywhere else (Stripe and every web provider); per-product rates are capped per catalogue when saved, while the app default rate and per-affiliate deals are capped at the moment of the sale, where the rail is known. - Affiliate commissions are locked at conversion time (a customer's original rate survives renewals). - Ads attribution is first-party: ad clicks are stamped on visitors and locked onto conversions at sale time, so campaign ROAS reflects real attributed revenue. - Public REST API with account-level keys lets scripts and AI agents create and edit the business's apps (affiliate programs, clipping/click campaigns, collabs), enroll affiliates (embedded affiliate signup), read stats, and manage ad campaigns. Ad networks are connected by minting a hosted connect link (the OAuth consent itself belongs to Meta/Google/TikTok/Apple/Snapchat and cannot be an API call), and the account wallet is topped up the same way. Funding an app, ending it, and connecting a payment provider stay dashboard-only actions; funding itself is free, because the platform fee on a budget app is charged per payout instead (see the budget-app fee below). - ADS: THE DASHBOARD AND THE API DO THE SAME THINGS AT THE LEVEL OF CAPABILITIES, WITH ONE NAMED EXCEPTION, AND THE EXCEPTION IS NOT THE ONLY GAP AT THE LEVEL OF INDIVIDUAL LAUNCH FIELDS. The rule is that every ads capability the API has is reachable from the business dashboard's Ads tab too. The one whole capability that is not is the catalog WRITES: creating and deleting a product catalog (POST and DELETE `…/ads/catalogs`, plus answering a Google Merchant Center link invitation): the dashboard reads catalogues and writes the chosen id onto an ad group, but it has no control that makes or removes one, and the campaign wizard says so on screen. That is deliberate rather than pending — a catalog holds a product FEED, and nothing in this product uploads or reads a feed (on Google that is the separate Merchant API), so a create button would make an empty catalog nobody could fill. Everything else: the six endpoints that were the last without a screen now have one — TikTok ad review and appeals, TikTok comment moderation, the format-shaped creative builder (Snapchat's four richer formats and Apple's Custom Product Page ads), Google experiments and campaign drafts, Apple impression share, and keyword ideas plus bid recommendations. Do not tell an owner to use an API to find out why their ads stopped. Two of those six are not tables and the endpoint is why: ad review hangs off the ad's own row, because both its reads need an ad id and there is no list behind them, and the creative builder publishes no creative list and no delete on either network — its one GET lists Snapchat's interaction zones, which is the reuse and recovery path after a half-built collection rather than a table of creatives. Apple's impression share is on screen although Apple's own gateway has refused the retrieval half since 2026-03-16 — the screen prints Apple's refusal in Apple's words rather than hiding the feature or showing a blank card. The dashboard screens are gated on the ads capability rather than on ownership, matching the API exactly, so a co-founder with ads access reaches all six. The account itself is the exception on BOTH doors alike: connect-link, balance, billing and pixel stay owner-only, because somebody trusted to spend a budget is not automatically trusted to unplug the ad account or read the wallet behind it. What stays dashboard-only is not ads reads: funding an app, ending one, connecting a payment provider, and the OAuth consent an ad network demands (which is a hosted link). BELOW THE LEVEL OF CAPABILITIES, the campaign wizard is not a field-for-field cover of the launch body and no surface claims it is: fanOut.extraAdGroups (the second-ad-group half of Meta's creative fan-out) is the named example, reachable only with a key, while fanOut.extraAds, Apple's ad.productPageId, Meta's specialAdCategoryCountry and contextualMultiAds, and Snapchat's ageMin/ageMax/gender/endDate all became wizard controls and are no longer API-only. The dashboard is also NARROWER than the API in one place on purpose: a Google campaign whose campaign_budget is shared with other campaigns renders its daily-budget cell READ ONLY, showing Google's own sharing count, because the API answers 409 CONFLICT for the same edit. - ADS WRITE FAILURES, THE ONE DISTINCTION AN AGENT MUST NOT GET WRONG: `502 UPSTREAM_ERROR` is the only ads error worth retrying — it means the network was asked and it refused or did not answer. `409` with `error.code: "CONFLICT"` is OUR OWN refusal of a request that is well formed and permitted but that the account's state makes wrong to perform, and no network was asked at all, so retrying it can never succeed. The live case is `PATCH /api/v1/businesses/{slug}/ads/entities/{id}?network=google` editing `dailyBudgetCents` on a campaign whose Google `campaign_budget` is shared with other campaigns: nothing is sent to Google, the message names how many campaigns spend from that budget, and the fix is to change the shared budget in Google Ads or give the campaign a budget of its own first. It used to answer 502, which is exactly the code an agent loops on. Note that `409` also carries `NOT_CONNECTED`, a different code in the same envelope, so branch on `error.code` and never on the status alone. - Ad campaigns created OR MANAGED through Affiliateo (dashboard, API, or an AI agent) carry a 1% platform fee on their spend, metered from the business wallet as spend syncs. Managed means any successful write on the campaign or anything under it — budgets, bids, targeting, creative, and a pause or resume too, plus creating an ad set, ad group, ad or keyword inside a campaign built on the network itself — which starts metering that campaign from that day forward and never retroactively. Connecting an account and only READING its stats stays free, and a campaign nobody has ever created or written to through us is never metered. Unpaid fees (including estimates on recent spend the wallet can't cover) pause the ads endpoints (PAYMENT_REQUIRED) past a grace allowance; past the account's campaign-pause limit (starts at $50, rises to $1,000 with clean payment history) the metered campaigns are paused on the networks until the balance is back to zero. Campaigns never touched through Affiliateo are never paused. Two writes are refused with PAYMENT_REQUIRED while ANY billed fee is outstanding, however small and well below that grace allowance: switching a metered campaign back to ACTIVE, and creating a campaign that starts ACTIVE. Creating one PAUSED is never blocked — it spends nothing, so it owes nothing — and it can be switched on the moment the balance reaches zero, which is also why the launch endpoint and the campaign wizard are unaffected: both create paused on all five networks. - Budget apps (clipping, web traffic, collab) carry their 1% platform fee at PAYOUT time, not at funding time. Funding is free: every cent funded becomes the app's budget. Each payout then costs the budget the creator's amount plus 1% of it, taken from the same budget and credited to Affiliateo in the same transaction, so a $100 payout costs the budget $101 and a budget is best planned as intended payouts times 1.01. A payout the remaining budget cannot cover together with its fee is refused outright rather than part-paid. Nothing is ever charged on money that did not reach a creator, so ending an app refunds the whole unspent remainder with no fee retained. Because of that, budget_cents is GROSS: it holds the creator money and the fees still to come, and budget_cents minus spent_cents is the uncommitted money rather than what a creator can be offered. The app settings object on GET /api/v1/businesses/{slug}/apps/{appId} and the public discover feed both return payable_capacity_cents, the largest single payout the remaining budget still covers with its fee; read that instead of subtracting. The apps list is a thin id/name/type/status index and carries no money fields, so read the single-app endpoint for a budget app's numbers. Affiliate apps are unaffected: their 1% is per conversion and was always collected at payout. - Partner program: every account has an invite link (`/i/{code}`, `/i/{username}`, or `?a={username}` appended to ANY page URL). A signup through one is linked to the referrer permanently, single-level, and the referrer then earns 50% of every platform fee Affiliateo collects from them — sales, app sales, budget-app payouts and ad spend alike. At the 1% fee that is 0.5% of what the referred account sells. The cut always comes out of Affiliateo's fee, never out of the seller's net, the creator's payout, or any affiliate's commission on the same sale. - PARTNER-LINK FEE TRIAL: an account that signed up through one of those links pays NO platform fee at all for its first 7 days, counted from signup. It covers every surface — sales, subscriptions, one-time products, budget-app payouts and ad spend. Nothing is deferred to the end of the trial; a fee falling inside the window is never charged. Ad spend is priced per spend DAY rather than by when the meter runs, so a day inside the window stays free even though ad fees settle about two days behind. Because the referrer's share is half of what Affiliateo actually collects, a referred account earns its referrer nothing during those 7 days; the 50% starts when the trial ends and then runs for the life of the account. ## Docs - [Getting started](https://affiliateo.com/docs): what Affiliateo is and how the pieces fit together - [Fees & pricing](https://affiliateo.com/docs/fees): platform, payment, and payout fees by country - [Partner program](https://affiliateo.com/docs/partner): earn 50% of Affiliateo's platform fee from everyone you refer, for life, and the 7-day no-platform-fee trial their signup gets - [Payouts](https://affiliateo.com/docs/payouts): how affiliates and businesses get paid, methods, schedules, holds - [Local payment methods](https://affiliateo.com/docs/local-payment-methods): supported local rails by country - [Apps overview](https://affiliateo.com/docs/apps): every installable app and what it does - [Affiliate](https://affiliateo.com/docs/apps/affiliate): affiliate program for a website, a mobile app, or both; web providers (Stripe, Polar, Paddle, Whop, Shopify, WooCommerce) with npm + script-tag SDKs, mobile providers (RevenueCat, Adapty, Apple IAP, Google Play, Stripe) with Swift, Kotlin, Flutter, React Native and WebView SDKs; how deterministic web attribution differs from probabilistic install attribution, and the per-rail commission ceilings. Replaces the old /docs/apps/web-affiliate and /docs/apps/mobile-affiliate, which redirect here - [Mobile: Apple in-app purchases](https://affiliateo.com/docs/apps/affiliate/apple-iap): App Store purchase tracking setup - [Mobile: Google Play](https://affiliateo.com/docs/apps/affiliate/google-play): Play Billing purchase tracking setup - [Mobile: free affiliate access](https://affiliateo.com/docs/apps/affiliate#free-access): grant or revoke complimentary app access per affiliate (RevenueCat apps) - [Clipping](https://affiliateo.com/docs/apps/clipping): pay creators per view for short-form clips (CPM) - [Web Traffic](https://affiliateo.com/docs/apps/web-traffic): pay per qualified click (CPC) - [Collab](https://affiliateo.com/docs/apps/collab): sponsored content collaborations with deliverables - [Elements](https://affiliateo.com/docs/elements): embed Affiliateo UI inside your own product, and MIX it freely with the REST API above (build the screens you want control over, mount only the components you would rather not build — a session carries only the components you name, so asking for one is normal); ten components, session minting, theming tokens (colors, gradients, custom fonts), an optional date filter, an in-place cash-out flow, web/hybrid/native, plus React / React Native / Swift / Kotlin SDKs - [Refunds](https://affiliateo.com/docs/refunds): refund policy and how commissions reverse - [Disputes](https://affiliateo.com/docs/disputes): chargebacks, dispute fees, prevention ## API - [API reference](https://affiliateo.com/docs/api): App management (create and edit the business's apps — affiliate, clipping, web traffic, collab — with the same server-side validation as the dashboard wizard: list + full settings reads, JSON or multipart writes, media by public https URL for agents, per-type commission clamps, visibility tiers; affiliate-type apps are created pending their provider connection and the response points at the dashboard URL to finish — one app carries the web rail and the mobile rail together, and once its Stripe web rail is connected the API can switch AFFILIATE DISCOUNT CODES on or off (every affiliate gets a Stripe code made from their username, credited on a code typed at checkout with no link click — the merchant must also have allow_promotion_codes on their own Stripe Checkout session or Stripe shows no box to type one into); funding, ending, and provider connects deliberately stay dashboard-only, and budget apps expose payable_capacity_cents next to budget_cents/spent_cents because the 1% is charged per payout out of the same budget, making budget_cents gross); Embedded affiliate signup (add a "Become an affiliate" button to your own app or website: POST an email to enroll a user, with the account auto-created if needed, and get back their referral links, QR-ready short link, lifetime stats with click sources and countries, a payout readiness object ({ identity_verified, payouts_enabled }, on every affiliate response and /me/wallet, for gating Verify / Cash out buttons), pending/payable/paid earnings (per-click for Web Traffic apps: paid = approved clicks × CPC, payable = unpaid qualified clicks × CPC, plus a click_payout object with the CPC and approved/payable click counts), per-affiliate conversion history, and the app's commission rates + visible products (or, for Web Traffic apps, the per-click rate + budget) for the pitch screen); an embedded "get paid-ready" verification link (add a "Verify" button: mint a short-lived hosted page where the affiliate first confirms a 6-digit code emailed to them (so a minted link is useless to anyone but the inbox owner; self-minted /me links skip the code), then creates their Stripe payout account and passes identity verification on our domain (only whichever step they still need; the link can only fill in what Stripe still asks for, never read or rewrite existing details), then is redirected back to your app via https or a mobile deep link with ?affiliateo_status=complete, =pending when a step can only finish on the Affiliateo dashboard, or =expired when the link had already expired (mint a fresh one and reopen it); sensitive data like SSN, ID photos, and selfie goes straight to Stripe/AWS and never through your servers, and we store only pass/fail; confirm with the affiliate.verified webhook or the verified field on the affiliate endpoints); an embedded "cash out" withdrawal link (add a "Withdraw earnings" button: mint a short-lived hosted page where an existing affiliate confirms the same kind of 6-digit email code (or is recognized by an active login; accounts with 2FA enabled must sign in instead of using a code, so an emailed code alone never moves money), adds or picks a bank, and withdraws their account wallet balance ($20 minimum; standard payout, or instant within minutes when the account is eligible) to it, then is redirected back to your app with ?affiliateo_status; an affiliate who is not yet paid-ready is walked through the get-paid-ready flow on the same page and then continues straight into the withdrawal, so one Cash out button suffices; the cashable amount is their account-wide withdrawable wallet balance (one wallet per account), shown alongside the minting app's pending/payable/paid as context; a self-serve /me variant lets a user mint their own); an app-wide program summary (totals + payout buckets across all affiliates, date windows, per-day series for charting) and an app-wide conversions feed with date filters; the owner's own tracking link works through the same endpoints; an affiliate-side "me" view where any user reads their OWN data across every business they've joined (their affiliations with short + username + merchant-domain direct links plus the Stripe COUPON code when the app has discount codes on (a code typed at checkout, not a URL — QR codes render from the short link), per-app click sources/platforms + countries breakdown, lifetime + windowed stats, cross-business summary with wallet balance, conversion history, and a read-only money view of their account wallet: the full balance breakdown (withdrawable, held, pending deposits, locked ad budget, disputed, in-transit withdrawals) and the transaction ledger with each row's other party, gross/fee, and deposit receipts) using the same keys, no business ownership required; a parallel business/storefront affiliate program covered for BOTH sides: owners read their storefront affiliates (links, visitor counts, sales, commission, payouts, conversion history) and members read their own business affiliations + sales across businesses (business stats are net of refunds with windowed link clicks, counted per day, so no sources/countries breakdown on that side); manage Meta Ads (campaign stacks on five of Meta’s six ODAX objectives — traffic, sales, awareness, engagement and app promotion — plus video views, which Meta builds as an engagement campaign with a ThruPlay goal and which therefore needs a video; sales and app promotion additionally offer Advantage+, where the budget moves onto the campaign and Meta picks the audience and placements; catalogue sales is the one Meta objective a permission wall closes, because App Review did not grant catalog_management; LEADS is TWO refusals wearing one word and only half of it is a wall — Meta INSTANT FORMS need pages_manage_ads and leads_retrieval, both declined, while lead ads that send people to the advertiser's OWN website need no permission Meta has withheld and are simply NOT BUILT YET, so never say nothing on our side unlocks the goal as a whole, automated rules, custom audiences + lookalikes, account/campaign spend caps, billing reads, ad previews, delivery estimates, custom conversions, targeting search, pixel. BOOSTING AN EXISTING POST is buildable: send ad.media = { type: "existing_post", objectStoryId: "{page_id}_{post_id}" } and no text, headline, link, button or upload, because the published post already carries all five and the boosted version keeps the likes and comments the organic post earned, which an ad rebuilt from the same image does not — the id is typed rather than picked from a list because listing a Page's posts needs pages_read_engagement, ungranted, and a post belonging to a different Page is refused before anything is created. A video ad can also name its COVER FRAME (ad.media.thumbnailId, candidates from GET …/ads/media?video=), which Meta previously chose and we discarded. Two ad-set fields the objective does not decide are settable: billingEvent, hard-wired to IMPRESSIONS on every ad set this product ever created, and destinationType, Meta's conversion location, which was NEVER SENT and left Meta to infer one. The launch body also takes specialAdCategoryCountry (two-letter codes naming which countries a regulated declaration covers — REQUIRED rather than derived once a special ad category other than NONE targets two or more countries, and refused when no regulated category is declared; one targeted country still derives itself) and contextualMultiAds ('OPT_IN' | 'OPT_OUT', create-only — leaving it UNSET is NOT the same as OPT_OUT, because Meta enrols any creative made with no enrol status). Both are dashboard controls too, so neither is API-only. adset.endDate means the END OF THAT DAY ON THE AD ACCOUNT'S OWN CLOCK, not UTC: a launch naming one makes one extra Graph read to learn the account timezone, and an end date that cannot be placed on a real clock is refused rather than sent as UTC. An adset.publisherPlatforms list of which NONE is facebook / instagram / threads / messenger / audience_network is now a 400 instead of a silent launch on every platform Meta has; dropping SOME unknown values is unchanged. CREATIVE FAN-OUT is on both doors: fanOut.extraAds adds up to four more ads to the launch's single ad group, each with its own image or video and optional text and headline that inherit the base ad's when blank, and the campaign wizard writes it — fanOut.extraAdGroups, the second-ad-group half, stays API-only. SCHEDULED BUDGET BOOSTS on a campaign or ad set that already exists ride their own route, because the campaign field is create-only — and Meta publishes no update, no delete and no recurrence for the object, so every schedule is permanent and "every Friday" is twelve of them. Click-to-Messenger, click-to-WhatsApp and click-to-Instagram ads are fully specified and NONE is buildable: all three need pages_manage_ads and pages_read_engagement, and Instagram Direct needs instagram_basic on top), Apple Search Ads (Apple has no objective field at all, so a campaign is an app, a PLACEMENT and a bidding strategy: all four placements are creatable — search results, the Search tab, the Today tab and other apps’ product pages — on MANUAL_CPT or on MAX_CONVERSIONS, which bids toward installs and runs in search results only; the ad group is built in the same call with its targeting, its Search Match setting and both keyword lists, since a campaign without one can never serve; plus keyword and negative-keyword management afterwards, bid recommendations (campaign-wide or scoped to one ad group), and app/geo/product-page search. Custom Product Page ads are buildable end to end — the organisation-scoped creative and then the ad on it, with the existing creative REUSED rather than collided with, since Apple allows one per product page per organisation and publishes no update or delete for it. The launch carries it as an optional ad: { productPageId, name? } block, offered on all four placements and REQUIRED on APPSTORE_TODAY_TAB whenever an adGroup travels with the launch — a Today-tab launch with an ad group and no ad.productPageId is a 400 before anything is created, where it used to be a 201 for a campaign that could never serve. The 201 gains adId, creativeId, creativeState and creativeStateReasons; adId and creativeId are null on APPSTORE_SEARCH_RESULTS and APPSTORE_PRODUCT_PAGES_BROWSE, which is correct rather than an omission because those two render from the App Store listing and owe no ad object, and creativeState is null on a creative we just made (Apple's create returns only an id) and carries Apple's real verdict on a reused one. A failed AD create now rolls the campaign back and answers 502 (Apple refused) or 400 (our own check) instead of 201. The dashboard launcher asks for the product page as well, picked from the product_page lookup, so the Custom Product Page ad is not API-only. Apple's Campaign Management API v5 stops answering on 2027-01-26, and the Apple connection settings dialog shows a countdown inside the last 180 days and says plainly, once the date has passed, that every Apple call is failing; no migration date is published, because the successor still carries a blocker. The impression share report is here too, on its own route because it is Apple's one ASYNCHRONOUS report: it answers with an id and a state rather than rows, tells you where you rank against everyone else bidding on the same search terms, and its read half has been answering a 403 from Apple's own gateway since 2026-03-16, which the endpoint explains in words instead of passing the status through. Awareness, engagement, video views, leads and catalogue sales are not missing features on Apple: none of them exists anywhere in its API), Google Ads (Google is the network with no objective field either — the campaign TYPE is the objective — and all six types its API can create are creatable here, each as ONE atomic mutate with validate-only dry runs: Search, Performance Max, Display, Demand Gen, App and Shopping. There is deliberately no Video: Google’s own guide says the API can only fetch and report on Video campaigns, never create or update them. LEAD FORMS are built, and Google is the one network where the form is CREATED at launch rather than pointed at — a Search campaign carrying a leadForm block adds a LEAD_FORM asset and the campaign_asset that links it to the same atomic mutate, needs no pixel because the form is submitted on Google, and freezes its question list at creation (Google allows reordering, never adding or removing); image assets uploaded through the API and YouTube video registered by id, asset groups with search-theme and audience signals, keyword + negative-keyword management on live ad groups, keyword ideas with volume and bid ranges, geo/language search, optimizer recommendations, conversion actions. Video reaches a Google campaign as a YOUTUBE LINK rather than a file — Google publishes no video upload that takes bytes — and Demand Gen campaigns launch as a swipeable CAROUSEL or a video-responsive ad, not only the one multi-asset layout; responsive search ad headlines and descriptions can be PINNED to a slot, with a headline pinned to a description slot refused (Google accepts it and the asset then never serves) and pinning every headline refused too, because it leaves Google nothing to test. GOOGLE DOES PUBLISH AD PREVIEWS, narrowly: ShareablePreviewService covers Performance Max asset groups and certain YouTube ads and refuses responsive search and display ads outright, so a Search campaign has no preview anywhere on Google and Google's own AD STRENGTH grade plus its action items is the answer there instead. A/B EXPERIMENTS and campaign DRAFTS are here — Google is the only one of the five networks with an experiment API — where nothing splits or spends until the experiment is scheduled, promote and graduate are permanent and marked as such in the response, and the long-running errors have to be read from asyncErrors because scheduling and promoting report acceptance rather than completion. Plus labels and campaign groups, thirty days of CHANGE HISTORY where each row's clientType tells a change made through this product from one made in Ads Manager, in Editor, by a script or by Google's own automation (Google keeps no more than thirty days and answers day 31 with an empty list, which reads as "nobody touched it", so an out-of-range window is refused instead), and billing READS: billing setups, account budgets and invoices, with no "balance" invented because Google has none), and TikTok Ads (stats; six objectives — traffic, conversions, engagement, leads (an Instant Form id from Ads Manager; the leads themselves land in TikTok’s Leads Center), app promotion (a TikTok app id plus ios/android) and product sales (catalogue ads: the campaign declares the product source, the ad group names the catalogue, and the ad goes out as CATALOG_CAROUSEL with no uploaded creative at all, because TikTok renders every impression from the product’s own image, title and price) — each of which fixes the optimisation goal and the billing event together, because TikTok refuses mismatched pairs rather than correcting them, and the last four send no landing page at all; engagement is SPARK ADS, which promote a post that already exists on a TikTok account: the ad carries the post id and uploads nothing at all, because the post supplies the video, the caption and the account name, and it is the one TikTok objective Smart+ cannot express, so it stops being creatable at the end of 2026 with nothing to migrate to; full manual AND Upgraded Smart+ campaign management (one launch endpoint covers both and SMART+ IS TRIED FIRST — the order is ['smart_plus', 'manual'], so manual is the STANDBY that catches a create-endpoint refusal rather than the default, and after 2026-12-31 manual creation stops and Smart+ is the whole answer; the response reports which family answered in `path` so a later edit sends the matching `kind`. Two launch fields the Smart+ endpoints have nowhere to put are named before anything is created: adgroup sub-placements and ad showcase products. A request naming either is refused on the Smart+ attempt and finished by the manual family today, and REFUSED OUTRIGHT after 2026-12-31, because Smart+ declares neither and TikTok answers a field it does not recognise with silence rather than an error), plus pixels, identities and Business Center balances, with Smart+ needing no allowlist — TikTok confirmed general availability on 2026-08-03 — and its full campaign/ad group/ad stack VERIFIED LIVE on 2026-08-04: its ad create refuses a fixed call to action on most campaigns and needs a dynamic CTA portfolio id instead, which the launch path builds for you and sends inside ad_configuration, the one place TikTok reads it — everywhere else it is discarded with no error and the ad is then refused for having no call to action, and the pixel RENAME live too on the already-granted Pixel Management permission; CAROUSEL ads (1 to 35 images plus a music id, which TikTok requires on every carousel and which a Smart+ launch picks for you when you do not name one — an image-only ad has no other way to run on Smart+, since that family has no single-image format) beside the single image and single video formats, product SHOWCASE anchoring under an ad of any format, and PLACEMENTS beyond TikTok itself — Pangle, TikTok's other apps, sub-placements inside TikTok, or automatic — where naming none still buys exactly the inventory every previous launch bought; AD REVIEW AND APPEALS, which is why a refused ad can now be argued with rather than only observed: TikTok publishes review_info and appeal endpoints under /smart_plus/ and NONE for manual ads, so a manual ad answers with its status plus the reason there is no more rather than a 501, and the refusal reasons and the offending video frame are asked for by default because TikTok defaults both off and otherwise returns a status and nothing else — reads on Smart+ objects never depended on Smart+ ad creation working; and COMMENT MODERATION plus a blocked-word list, where the words are held at ACCOUNT level, so keeping one word out of your comments no longer means switching comments off on every ad group and paying the engagement TikTok's ranking rewards; then five further capabilities that TikTok has now GRANTED (verified live 2026-08-02) and which need no env flag; a business that connected TikTok BEFORE that approval must reconnect once, because permissions are fixed into the access token at consent, and until it does those calls come back carrying TikTok's own refusal with the missing permission named in it: custom and lookalike AUDIENCES plus saved audiences (a saved audience is a different object, a reusable targeting set holding locations/ages/interests and which custom audiences to include or exclude, and it is what an ad group's saved_audience_id points at, so it rides ?type=saved on every verb and answers under its own key; rule-based only, since customer-list uploads of hashed emails/phones are refused on TikTok exactly as on Meta; lookbackDays decides who is IN the audience and expireInDays when the audience itself dies, and swapping the two succeeds while building a different audience) needing "Audience Management"; product CATALOGS for dynamic product ads (Business Center scoped rather than ad-account scoped, so they take ?bcId= and the usual ?account= is REFUSED rather than ignored; create/rename/delete, a product-feed upload that is a URL TikTok fetches itself, the upload log that carries the per-row verdicts, currency and regionCode unchangeable after creation, product sets readable/renamable/deletable but never creatable because TikTok publishes no create for them, and the products read is the only source of the ad-level sku_ids and item_group_ids) needing "DPA Catalog Management"; promoted APPS (the read-only source of the app_id field at campaign, smart_plus_campaign and smart_plus_adgroup level, plus the optimisation events one app may legally bid on for one goal; these are TikTok promoted apps, NOT Affiliateo apps at /api/v1/businesses/{slug}/apps, and registration stays in TikTok Ads Manager because their API has no delete for a registered app) needing "App Management"; automated RULES that run on TikTok's schedule (create, bind an existing rule to more campaigns/ad groups/ads, and a results feed to audit what they actually did; money thresholds in cents or in dollars but never both spellings of one number, an account-wide rule needs an explicit confirmAccountWide, PATCH is a FULL REPLACE unlike Meta's partial so a Meta-shaped body would strip a rule and return 200, and TikTok has no rule delete and no enable/disable at all) needing "Automated Rules"; and CUSTOM CONVERSIONS, which are a different object per network (Meta's is account-level; what we wrap on TikTok is an event rule living ON a pixel, so every TikTok call takes a pixelId, eventType speaks the optimisation vocabulary SHOPPING rather than the Events API's Purchase, deleting one can strip a running ad group of its signal without TikTok objecting, and firing stats answer whether it ever fires — note that TikTok ALSO publishes a separate first-class /custom_conversion/ family keyed by custom_conversion_id and scoped by an APP or PIXEL event source, verified live 2026-08-05, which is a DIFFERENT object from the pixel event rule above: it is readable through the managed-resource surface as resource=custom_conversion, and its create/edit/delete are deliberately not wrapped, because the endpoints exist but no request body for them has ever been observed and TikTok answers an unrecognised field with silence rather than an error) needing "Custom Conversion Management"); hosted links that close the two steps an API cannot perform directly: an ad-network connect link (mint per business + network, hand it to the owner, they approve on the network and land back on your return_url with ?affiliateo_status) and a wallet top-up link (the answer to a 402 PAYMENT_REQUIRED, which pauses every ads endpoint until unpaid 1% platform fees are covered); and Snapchat Ads (stats, full campaign→ad squad→creative→ad stacks in one rolled-back-on-failure call across seven goals — traffic, conversions, awareness, engagement, video views, leads and app promotion — which is as much of our goal list as any other network serves, tied with Google; the goal picks the CREATIVE, so leads takes a lead-form id and app promotion takes a store id and an icon INSTEAD of a destination URL, and because neither of those creatives has a URL field, neither can carry our tracking tags or report ROAS; bidding is AUTO_BID or a bid you set yourself; product sales is the one goal not offered, and the field list is not the reason — Snapchat’s dynamic creative takes exactly two ids, a dynamic_template_id and a product_set_id matching the ad squad’s, and Snapchat publishes no endpoint for either one, while the catalogues themselves list fine; plus campaign/ad squad/ad/creative reads and writes through the same field registry as the other four, pixel and audiences; Snapchat ids are UUIDs at every level, unlike the numeric ids Meta, TikTok and Apple use. FOUR FORMATS that were impossible are buildable through a format-shaped creative route rather than a field bag: DEEP_LINK (the one an app advertiser actually wants, since an install ad sends everybody to the store including the people who already have the app), REMINDER, STORY (a preview tile plus a composite holding 1 to 20 slides in an order FROZEN after create, so a repeated slide is refused rather than deduplicated) and COLLECTION (tiles, then the interaction zone that orders them, then the creative — not atomic, and the zones are listable and reusable so a half-built one is recovered rather than re-uploaded). Every response carries the AD TYPE beside the creative id, because Snapchat spells every format twice in two different enums and refuses a mismatched pair at ad create, after the creative already exists. Video over 32MB now uploads instead of being refused with advice to compress it: a chunked upload drives automatically above Snapchat's simple-upload ceiling, on the one network whose ads ARE full-screen vertical video. The creative LIBRARY is listable and one media file is previewable, which matters more here than anywhere else because a Snapchat media id came back exactly once. A call to action Snapchat does not recognise is now REFUSED BY NAME instead of being silently downgraded to "More" (cross-network words like LEARN_MORE still produce the button they always did). Frequency caps are typed and settable at launch. Gender "other" is targetable and we were the ones dropping it — an advertiser asking for it previously got NO gender restriction at all, a WIDER audience than the one they picked. AGE TARGETING IS NOW REFUSED RATHER THAN SILENTLY CLAMPED, and the two bounds do NOT share a ceiling: ageMin is a whole number from 13 to 35, ageMax from 13 to 55, and an ageMax below ageMin targets nobody — all three are 400 VALIDATION_ERROR, so a request built on the older "13 to 55 on both bounds" reading now fails instead of quietly buying a different audience. Leave ageMax out entirely for no upper limit. ageMin, ageMax, gender and endDate are dashboard controls too, so the wizard can narrow by age and set a stop date. A launch may also carry profileId (alias profile_id), a Snapchat Public Profile id used ONLY when the connection carries none of its own — the connection's profile always wins — and a launch with no Public Profile anywhere answers 403 FORBIDDEN, not the 502 it used to: the refusal is OURS, so anything branching on the old status needs updating. And funding sources plus billing centres are readable, ORGANIZATION-scoped, which is the only place that explains ads stopping when nothing is wrong with the campaign: a funding source that has moved to SPENT or EXPIRED halts delivery with no error on any campaign, ad squad or ad); ad stats for every network with spend plus TWO revenue views side by side: verified revenue/ROAS from first-party tracked sales, and the network's own reported conversions and claimed ROAS (self-reported by the network's pixel/conversion tracking) for comparison; and THREE surfaces that answer identically on all five networks rather than per-network, so one reader serves them all — ID DISCOVERY (GET …/ads/lookups, every lookup returning { id, label, meta } where id is the value that goes on the wire: locations, postcodes, languages, interests, interest and purchase-intent keywords, behaviours, audiences and saved audiences, device models, carriers, ISPs, promoted apps and their optimisation events, pixels, conversion actions, catalogues, product sets, products, Business Centers, posting identities, Pages, organic posts, videos, images, creatives, Apple product pages, Google assets and topics, keyword ideas — with the same field catalogue naming, per field, which lookup fills it, so an id-shaped field stops being a dead end), and DIMENSIONAL REPORTING (GET …/ads/breakdowns, spend and delivery split by age, gender, country, region, city, metro, device, device make, platform, publisher, placement, hour of day, day of week, language, network type, interest, product, frequency or creative asset, in USD micros, live rather than stored (Snapchat's device make is DELIVERY METRICS ONLY, so conversions is null on every bucket of it; device model is declared and unbuilt, so it answers 501 rather than an empty list) — which is the "which placement or age group is wasting my budget" question that flat daily ad stats cannot answer; for the whole ad account by default and, with ?campaignId=, for ONE campaign on Meta, Google, TikTok and Snapchat, where every report states its scope and echoes the campaign id back, and where Apple Search Ads answers 501 rather than serving the account's numbers under a campaign id, since a wrong number wearing a right label has no symptom), and MANAGED RESOURCES (GET …/ads/resources, the seventeen list-shaped objects a network keeps beside a campaign — audiences, custom conversions, blocked words, identities, keywords, negative keywords, shared negative-keyword lists, their campaign attachments, labels, campaign groups, budget schedules, automated rules, recommendations, change history, ad strength, registered apps and balances — behind one contract with the same four verbs instead of seventeen dialects, where a POST carrying an `action` runs the row verbs that are not CRUD: rename, attach, detach, assign, apply, dismiss, bind; all seventeen answer today and several answer partly: audiences on Meta, Google, TikTok and Snapchat, automated rules on Meta (all four verbs) and TikTok (list, create and update, plus bind — TikTok publishes no delete and no on/off field, so a rule is narrowed rather than stopped), custom conversions on Meta (all four verbs), TikTok (list only) and Snapchat (list, create and delete, per event source because Snapchat publishes no account-wide enumeration; no update, because none of its six paths is a PUT or a PATCH), identities, registered apps, balances and blocked words on TikTok, keywords on Google (list and edit, off the same query the dashboard already runs; the text and the match type are immutable, so correcting either is delete-and-recreate) and Apple Search Ads (all four verbs, and the LIST is new — …/ads/keywords could add, re-bid, pause and remove a keyword and could not show you one; a new keyword is PAUSED unless you ask for ACTIVE, and omitting the bid inherits the ad group's), negative keywords on Apple Search Ads (all four verbs) and Google (list, create and delete — Google refuses an edit because keyword.text and keyword.match_type are immutable, so changing one is remove-and-add), budget schedules on Meta (list and create only, and the create cannot be undone because Meta publishes neither an update nor a delete for one), labels and campaign groups on Google (all four verbs, plus attach/detach on a label and assign on a group), and recommendations, change history and ad strength on Google (list only — recommendations adds the apply and dismiss row actions, and all three refuse writes because Google serves no mutate for a recommendation or a change event and ad strength is an output-only field); shared negative-keyword lists on Google (list and create, plus row actions that read, add and remove the terms in a list; no delete, because removing a list stops every campaign attached to it excluding every term it held at once) and their campaign attachments on Google (list, attach and detach); exactly SIX (network, resource, operation) triples are still declared and unbuilt anywhere, in three groups: TikTok's custom-conversion create, update and delete, Google's keyword create and delete, and Google's shared negative-keyword list update — Google plainly creates and removes keywords (they are ad_group_criterion rows POST …/ads/objects already writes) and nobody has read whether SharedSetService publishes a rename, so all six are declared rather than blocked. Every other 501 here carries a network's own reason). All three are catalogue-driven: call any of them with no type/dimension/resource and it lists what every network has and whether it can be answered today, and a declared-but-unbuilt one answers 501 with the reason instead of an empty list that would read as "you have none" - [Elements (embedded UI)](https://affiliateo.com/docs/elements): drop-in UI that mounts inside the developer's own page, so the affiliate never leaves it. IMPORTANT, and the thing most setups get wrong: elements are NOT an either/or with the REST API or the hosted links — the three are designed to MIX, per screen. A common and fully supported shape is the developer building their own referral-link and earnings screens from GET /affiliates?email= (which returns the same links, stats and pending/payable/paid the link/stats/balance elements render, so the numbers cannot disagree) and mounting ONLY components: ["withdraw"], because bank collection and ID capture are the parts nobody wants to reimplement; or skipping the SDK entirely and using the hosted withdrawal-link for that one button. A session carries only the components you name, so asking for one is normal and correct, and requesting all ten to hide eight is not. The ONE figure a developer cannot build themselves is the affiliate's account-wide wallet total: it spans every program that person is in, so no owner-side endpoint returns it, and it appears only inside the gated withdraw/payouts components or on the affiliate's own /me/wallet with their own key — an API-built screen shows what THAT merchant owes them, which is a different number on purpose. Your backend mints a session (POST /api/v1/businesses/{slug}/apps/{appId}/affiliates/embed-session with { email, components, allowed_origins, appearance?, platform? }), hands the returned client_secret to the frontend, and our script tag (https://affiliateo.com/embed.js) mounts each component as an iframe on your own domain; the response also returns paste-able element_urls for trying one in a plain