# Affiliateo, full reference > 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) 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. This is the expanded companion to https://affiliateo.com/llms.txt. It exists so a model can answer questions about Affiliateo from one fetch. The endpoint list, error codes and ad object levels below are generated from the same constants the running API serves, so they cannot drift from the product. ## Core concepts **Business.** A workspace. An account can own up to 3. One account wallet spans all of them. **App.** What a business installs into a workspace to run something: Affiliate, Clipping, Web Traffic, Collab, Meta Ads, Google Ads, TikTok Ads, Apple Search Ads, Calendar and more. An app's URL is `/{business}/{appSlug}` and slugs are globally unique. **Affiliate.** A person promoting an app or a storefront with a tracked link. These are two separate programs with two separate link formats and they are never merged: `?ref=` is an affiliate sale link, `?a=` is a partner signup link. **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 always `affiliate_app`, one name, sent and read back. Never infer rails from it. ## Attribution The difference between the two rails is real and worth stating plainly: - **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. **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 rather than the network's own claim. ## Commissions and money Maximum commission is **70% on store-mediated sales** (Apple in-app purchases, Google Play, RevenueCat, Adapty, where 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; the app default rate and per-affiliate deals are capped at the moment of the sale, where the rail is known. Commissions are **locked at conversion time**: a customer's original rate survives renewals. There is **one wallet per account**, not per business. Withdrawals need identity verification and have a $20 minimum. Refunds and chargebacks reverse commissions as separate negative rows. ## Paid ads Four networks, one shape. For each, Affiliateo both READS stats (spend, clicks, conversions, the network's own reported revenue) and WRITES campaign management. Every ad row carries **two revenue views side by side**: verified revenue and ROAS from first-party tracked sales, and the network's own reported conversions and claimed ROAS. They disagree, and seeing both is the point. ### Ad object levels, per network These are the object types `POST/PATCH /ads/objects` can write. Call `GET /ads/objects?network=X` for the full field catalogue of any of them. - **meta**: campaign, adset, ad, creative Meta calls the middle level an ad set. The other three call it an ad group. - **google**: campaign, budget, adgroup, ad, conversion_action, asset, asset_group, asset_group_asset, asset_group_signal, asset_group_listing_group_filter, campaign_asset, ad_group_asset, customer_asset, ad_group_criterion, campaign_criterion The asset levels are how Performance Max is built: an asset group replaces ad groups, ads and keywords entirely. Assets are never deleted, you remove the LINK. The two criterion levels are why a LIVE campaign can gain keywords, audiences, locations and schedules rather than being frozen at launch. - **tiktok**: campaign, adgroup, ad, smart_plus_campaign, smart_plus_adgroup, smart_plus_ad smart_plus_* is a separate endpoint family, not a mode of the manual levels. It is the only campaign-CREATION path that works after 2026-12-31; editing, pausing and reporting on existing manual campaigns continue unchanged. Writes need the ad account allowlisted by TikTok. - **apple_search_ads**: campaign, adgroup, keyword, negative_keyword, ad, creative Keyword-level bidding, so keywords and negative keywords are first-class objects. ### The ads platform fee Campaigns **created through Affiliateo** (dashboard, API, or an AI agent) carry a 5% platform fee on their spend, metered from the business wallet as spend syncs. Campaigns created directly on the ad networks and merely connected for attribution are **free**, and are never paused or touched by us. Unpaid fees pause the ads endpoints with `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 Affiliateo-created campaigns are paused on the networks until the balance is back to zero. The answer to a 402 is `POST /api/v1/me/topup-link`. ### What is gated Connecting a network is an OAuth consent and **cannot be an API call**. Mint a hosted link with `POST /ads/connect-link` and hand it to the business owner. Funding an app, ending it, and connecting a payment provider are likewise dashboard-only. ## The API Base URL `https://affiliateo.com`. Documentation at https://affiliateo.com/docs/api, OpenAPI 3.1 at https://affiliateo.com/openapi.json, machine-readable index at https://affiliateo.com/api/v1. ### Authentication `Authorization: Bearer afk_…` Generate keys in the dashboard: any business dashboard → API tab. Keys are account-level and cover every business you own. Keys come in read-only and read-write levels; every write endpoint says so in its description. ### Rate limits 600 requests / minute by default (per-key overrides available for large integrations) Expensive endpoints (launch, upload, sync) have tighter additional budgets. 429 responses carry Retry-After. ### Errors Envelope: `{ "error": { "code", "message" } }` Codes: `UNAUTHORIZED`, `FORBIDDEN`, `ACCOUNT_INELIGIBLE`, `BANNED_FROM_BUSINESS`, `NOT_FOUND`, `VALIDATION_ERROR`, `NOT_CONNECTED`, `PAYMENT_REQUIRED`, `RATE_LIMIT_EXCEEDED`, `UPSTREAM_ERROR`, `INTERNAL_ERROR` ### Endpoints #### Businesses and apps Read the account, then create and edit the apps on it. - `GET /api/v1/businesses` Businesses this key can act on, with ad-network connection status and every app’s id/slug/type/status (source of the {appId} below). - `GET /api/v1/businesses/{slug}/apps` Every app on the business (every type, any status): id, slug, name, type, status, visibility. Types: affiliate_app (commission on sales), clipping, web_traffic, collab. The id/slug source for create/edit flows. - `POST /api/v1/businesses/{slug}/apps` Create an app — same validation and clamps as the dashboard wizard (percent commission ≤90, ≤70 store-mediated mobile, flat ≤$50, 1% floor; the name becomes the globally unique URL slug). JSON body with the wizard’s field names, or multipart with thumbnail / collab_media_0..4 file parts; JSON callers pass media as public https URLs (thumbnail_url, media_urls — fetched server-side, SSRF-guarded, magic-byte validated; placement collabs require ≥1 creative). clipping/web_traffic/collab are born pending_budget (owner funds in the dashboard, $100 min); affiliate apps (Stripe only via API on the web rail) are born pending the provider connection (browser OAuth) — the response’s next_step carries the dashboard URL to finish either. content_type for an affiliate program is "affiliate_app" (one name, sent and read back), and you set its rails with provider + website_url (web) and/or mobile_provider + a store URL (mobile); either can be added later, and sending neither is a 400. Mobile apps return their webhook secret once. Requires a read_write key. - `GET /api/v1/businesses/{slug}/apps/{appId}` App info for the embedded-signup pitch screen: commission rate/type (with per-product rates, prices, and ≈commission-per-sale estimates for the app’s visible products), affiliate count, total paid out, status, and whether joining requires an application. For web_traffic apps commission is null and a click_payout object carries the per-click rate (cpc_cents), budget, and remaining budget instead (exactly one of commission / click_payout is non-null). Also carries a full owner-facing settings object (the key holder is the owner) for read-modify-write edit flows. - `PATCH /api/v1/businesses/{slug}/apps/{appId}` Edit an app’s settings — same fields and clamps as the dashboard edit dialog: name (re-derives the slug), description, category, requirements, faqs, visibility (public/private/private_only/invite_only), commission rate/type (notifies affiliates; new customers only), per-product rates, URLs, platforms, rewards, submission window, retention, placements, plus media (thumbnail_url / remove_thumbnail / media_urls replace-all, or multipart file parts). Locked with explicit errors: content_type, provider, status, budget — funding, ending, and provider connects stay dashboard actions. {appId} = app UUID or slug. Requires a read_write key. - `GET /api/v1/businesses/{slug}/apps/{appId}/summary` The app’s affiliate program at a glance: affiliate counts, lifetime totals (clicks, conversions, renewals, revenue, commission) and payout buckets (pending/payable/paid/refunded) summed across every affiliate; for web_traffic apps these are per-click earnings (their conversion money stays zero). ?from=&to= (YYYY-MM-DD, UTC days, inclusive) adds the same numbers for that window (net money, Stripe convention); &include=daily adds a per-day series for charting (window ≤366 days). Owner tracking links and organic sales excluded, so it reconciles with the affiliates list. #### App affiliates People promoting one app. Embedded signup, their stats and conversions, and the hosted links that get them paid-ready, cashed out, or embedded in your own UI. - `POST /api/v1/businesses/{slug}/apps/{appId}/affiliates` Embedded affiliate signup (the "Become an affiliate" button): { email, name? } finds-or-creates the Affiliateo account, joins the business, enrolls the affiliate, and returns referral links (short + username + merchant-domain direct for apps with a website or landing page), stats, a verified flag (identity check passed), and a payout readiness object ({ identity_verified, payouts_enabled }, webhook-fresh with no Stripe call: gate your Verify / Cash out buttons on it) in one payload (web_traffic apps also return per-click payout buckets + a click_payout object). Safely repeatable: an existing affiliate returns 200 with current links + stats, and one who previously left the business is reactivated. Users banned from the business are rejected with FORBIDDEN. {appId} = app UUID or app slug. Requires a read_write key. - `GET /api/v1/businesses/{slug}/apps/{appId}/affiliates?email=…` One affiliate by ?email= or ?ref_code=: links, lifetime stats (clicks, conversions, renewals, revenue, commission), payout buckets (pending/payable/paid/refunded cents), and the payout readiness object ({ identity_verified, payouts_enabled }) for gating Verify / Cash out buttons (also on every list row). For web_traffic the buckets are per-click (paid = approved clicks × CPC, payable = unpaid qualified clicks × CPC; pending/refunded always 0) and a click_payout object adds the CPC + approved/payable click counts. Add &include=breakdown for click sources + countries (last 90 days), or &from=YYYY-MM-DD&to=YYYY-MM-DD for windowed click counts. Without email/ref_code: lists the app’s affiliates, cursor-paginated (?limit=, ?starting_after={id}, has_more in the response; &include=payouts for per-row buckets). Every row carries personal_commission — the affiliate’s owner-negotiated deal ({ type, rate }) or null when they ride the program rates; deals apply to NEW customers only (already-referred customers keep their stamped rate). - `GET /api/v1/businesses/{slug}/apps/{appId}/affiliates/conversions` Conversion history, cursor-paginated (?limit=, ?starting_after={id}): date, type (subscription, one_time, renewal, trial, refund, chargeback), gross amount, commission, paid/unpaid status, and the earning affiliate’s ref_code per row. ?email= or ?ref_code= scopes to one affiliate; without either it’s the app-wide feed (owner links + organic sales excluded). ?from=&to= (YYYY-MM-DD) narrows either mode to a date window. Refunds/chargebacks are separate negative rows. No customer PII. - `POST /api/v1/businesses/{slug}/apps/{appId}/affiliates/verification-link` Mint a short-lived hosted link that gets an affiliate PAID-READY (the "Verify" button): { email, return_url, name?, lang? } → { url, expires_in, verified }. The affiliate opens url on our domain, first confirms a 6-digit code we email them (proves the link is used by the inbox owner, not the minter; /me self-minted links skip the code), then creates their Stripe payout account and passes identity verification (only whichever step they still need: already-done steps are skipped, already-ready affiliates bounce straight back, and the link can only fill in what Stripe still asks for, never read or rewrite existing details), then is redirected to return_url (an https page, or a mobile deep link like myapp://verified) with ?affiliateo_status=complete appended, ?affiliateo_status=pending when a step can only finish on the Affiliateo dashboard, or ?affiliateo_status=expired when the link had already expired (mint a fresh one and retry). Sensitive data (SSN, ID photos, selfie) is collected ONLY on our domain and goes straight to Stripe/AWS, never through your servers, and we store only pass/fail. Same enrollment gates as the affiliates endpoint (banned → ACCOUNT_INELIGIBLE / BANNED_FROM_BUSINESS). Pair with the affiliate.verified webhook or the verified field on the affiliate endpoints. v1 is individuals only (companies onboard in the dashboard). Requires a read_write key. - `POST /api/v1/businesses/{slug}/apps/{appId}/affiliates/withdrawal-link` Mint a short-lived hosted "cash out" link for an EXISTING affiliate of this app: { email, return_url, lang? } → { url, expires_in }. The affiliate opens url on our domain, first confirms a 6-digit code we email them (proves the inbox owner is using it, not the minter; an active Affiliateo login as that same affiliate skips the code; accounts with 2FA enabled must sign in instead of using a code, so the emailed code alone never moves their money), then adds/picks a bank and withdraws their own wallet balance ($20 minimum; standard payout, or instant within minutes when the account is eligible) to it, then is redirected to return_url with ?affiliateo_status=complete (=pending if they left without withdrawing, =expired if the link had already expired: mint a fresh one and retry). The cashable amount is the affiliate’s account-wide withdrawable wallet balance (one wallet per account), not any single app’s payable; the link shows THIS app’s pending/payable/paid as context only. Never creates or enrolls anyone (the email must already be an affiliate here, else NOT_FOUND), and the affiliate must be paid-ready (identity verified) first. Requires a read_write key. - `POST /api/v1/businesses/{slug}/apps/{appId}/affiliates/embed-session` Mint a session for EMBEDDED ELEMENTS — the drop-in alternative to the hosted links above, where the affiliate never leaves your page: { email, components, allowed_origins, appearance?, lang? } → { client_secret, expires_in, components, requires_step_up, element_urls, allowed_origins, ignored_appearance_keys? }. Call from your BACKEND (the afk_ key must never ship in an app binary), hand client_secret to the frontend, and our JS SDK mounts each component as an iframe on your own page; or drop an element_urls entry straight into an