Cruva API Documentation
Using an LLM?
Download the entire API spec — every endpoint, request body, response example, parameter and stat — as a single structured JSON file optimized for LLM consumption. Drop it into your prompt or feed it to Claude, ChatGPT, Cursor or any other coding agent so it can build against the Cruva API without crawling these docs.
Authentication
All API requests require the following headers:
{"Content-Type": "application/json","x-api-key": "<your_api_key>","x-shop-id": "<your_shop_id>"}
Important: The x-shop-id header is a Cruva internal shop ID, it's not the same as your TikTok shop ID/code. To find your Cruva Shop ID:
- Go to cruva.com/dashboard/my-shops
- Click the dropdown on the right of the shop, then View Info
- Copy the Shop ID from the popup
Usage Limits
Rate limits. Each API key may make up to 10 requests per second and 1,000,000 requests per day, measured per shop. Requests past either threshold receive a 429 Too Many Requests; back off briefly and retry.
Maximum pagination depth. Paginated endpoints return results up to a depth of 10,000 rows. Once page_number × page_size (or repeated cursor paging) would reach beyond that point, no further rows are returned. To access records past the cap, narrow the result set with filters rather than paging deeper.
These limits are standard, canonical safeguards used across virtually every production data API. They exist to protect the underlying databases from queries that scan unbounded numbers of rows. The Cruva API is designed to serve targeted, filtered reads — it is not intended to be swept continuously to mirror or replicate an entire dataset.
Keeping a local copy in sync? Query deliberately. Scope each request with filters (date ranges, status, specific handles or IDs) and page only as deep as you actually need. On subsequent syncs, fetch and update only the records that have changed rather than re-pulling everything. A targeted, incremental approach stays comfortably within these limits and is faster and more reliable than repeatedly reading the full dataset.
Idempotency
Opt in to idempotent writes by sending an `Idempotency-Key` header alongside any supported write request. The key must match ^[A-Za-z0-9-]{1,128}$ (letters, digits, dashes, up to 128 chars).
How it works: the server caches the response (status, body) for 24 hours under a key scoped to your shop. A replay with the same Idempotency-Key returns the original response byte-for-byte — same campaign_id, same timestamps, same status code. Concurrent requests with the same key receive a 409 for up to 60 seconds while the first one is in flight. 5xx responses and uncaught exceptions are not cached — a retry can still succeed.
Supported endpoints: POST /automations/create, POST /automations/update, DELETE /automations/delete, POST /emails/campaigns/create, POST /emails/campaigns/update, DELETE /emails/campaigns/delete, POST /emails/senders/create-custom, POST /groups/create, DELETE /groups/delete, POST /lists/create, POST /lists/merge, DELETE /lists/delete, POST /creator-briefs/create, POST /creator-briefs/update, DELETE /creator-briefs/delete, POST /community/campaigns/create, POST /community/campaigns/update, DELETE /community/campaigns/delete, POST /affiliate/samples/approve, POST /affiliate/samples/reject, POST /affiliate/message/dm. Omitting the header on any of these is fine — the endpoint behaves normally with no caching.
Community payouts are idempotent by design without the header: /community/payouts/authorize is keyed on (campaign, creator, period) — a repeat call returns 400 already paid rather than double-paying.
Example: idempotent automation creation
# First call — creates the automation and caches the response under the keycurl -X POST https://api.cruva.com/automations/create \-H "x-api-key: <your_api_key>" \-H "x-shop-id: <your_shop_id>" \-H "Idempotency-Key: campaign-launch-2026-05-25-001" \-H "Content-Type: application/json" \-d '{"title": "Summer outreach","message_type": "dm","outreach_audience": "new_affiliates","dm_messages": [{ "type": "message", "content": "Hi!" }]}'# → 201 Created# {# "data": {# "message": "Automation created.",# "campaign_id": "6a14aa4f8c93e2105d7bf830",# "status": "stopped",# "messages_remaining": 3978511# }# }# Retry with the same Idempotency-Key — returns the original 201 byte-for-byte.# No second automation is created.
Idempotency - Lookup by Key
https://api.cruva.com/idempotency/<key>Retrieve a previously stored idempotent response by its key (scoped to the current shop via x-shop-id). Useful when a client loses connectivity mid-request and needs to recover the result without re-sending the original write.
Parameters:
key: URL segment. The same string previously sent asIdempotency-Key.
No request body. The key is passed as the URL segment <key> and must match ^[A-Za-z0-9-]{1,128}$.
Response 200 (in-flight)
{"idempotency": {"status": "processing"}}
Response 200 (completed)
{"idempotency": {"status": "completed","response_status": 201,"response_body": {"data": {"campaign_id": "6a14aa4f8c93e2105d7bf830","message": "Automation created.","messages_remaining": 3978511,"status": "stopped"}}}}
Error Responses
// 400 — key format invalid{ "error": "Invalid idempotency key format" }// 404 — no stored response (never made, or TTL expired){ "error": "No stored response for that key" }
Response Fields:
idempotency.status:processingif a concurrent write holds the key,completedif the cached response is available.idempotency.response_status: HTTP status code of the original response. Only present whenstatusiscompleted.idempotency.response_body: The exact body returned by the original request. Only present whenstatusiscompleted.
List Shops
https://api.cruva.com/account/shopsRetrieve every shop on your account — active or not — with is_active telling you which ones are live (false = paused, unpaid or cancelled). Use this to get the shop_id values you need for the x-shop-id header on every other endpoint.
No request body required. This endpoint only needs the x-api-key header — you don't need to pass an x-shop-id since it returns all shops on your account.
Response
{"data": [{"shop_id": "a1b2c3d4e5f6g7h8i9j0k1l2","shop_name": "Example Shop One","is_active": true,"created_at": "2024-06-29","plan": "growth"},{"shop_id": "m3n4o5p6q7r8s9t0u1v2w3x4","shop_name": "Example Shop Two","is_active": true,"created_at": "2025-06-10","plan": "scale"}]}
List Shop Products
https://api.cruva.com/shop/productsList the products that belong to a shop. Supports optional fuzzy search, filtering by open plan status, and sorting by price or units sold.
Request Body
{"search": "","is_open_plan": null,"sort_by": "units_sold","sort_direction": "desc"}
search — optional fuzzy search on product name. is_open_plan — true for open plan only, false for non-open plan only, null for all. sort_by — price or units_sold (default units_sold). sort_direction — asc or desc (default desc).
Response
{"data": [{"product_id": "1800291847362058192","product_name": "Glow Radiance Serum SPF30 – Hydrating Sun Shield","status": 1,"is_open_plan": true,"price": 850.00,"units_sold": 4312,"stock": 1204},{"product_id": "1800384726159403248","product_name": "Velvet Matte Lipstick – Long Lasting Natural Finish","status": 1,"is_open_plan": true,"price": 420.00,"units_sold": 1587,"stock": 356},{"product_id": "1800492618374920560","product_name": "Crystal Clear Eye Cream – Dark Circle Corrector","status": 1,"is_open_plan": false,"price": 690.00,"units_sold": 923,"stock": 89},{"product_id": "1800571839205748208","product_name": "Berry Blush Duo – Cream & Powder Compact Set","status": 3,"is_open_plan": true,"price": 1150.00,"units_sold": 45,"stock": 0}]}
List Shop SKUs
https://api.cruva.com/shop/skusList the SKUs (product variants) that belong to a shop. Supports optional fuzzy search, filtering to a single product, and sorting by price or stock.
Request Body
{"search": "","product_id": null,"sort_by": "stock","sort_direction": "desc"}
search — optional fuzzy search on SKU or product name. product_id — optional, filter to a single product's SKUs. sort_by — price or stock (default stock). sort_direction — asc or desc (default desc).
Response
{"data": [{"sku_id": "1729382916473829105","sku_name": "Shade 01 – Ivory","product_id": "1800291847362058192","product_name": "Glow Radiance Serum SPF30 – Hydrating Sun Shield","price": 850.00,"custom_cogs": 210.00,"stock": 745},{"sku_id": "1729382916473829106","sku_name": "Shade 02 – Beige","product_id": "1800291847362058192","product_name": "Glow Radiance Serum SPF30 – Hydrating Sun Shield","price": 850.00,"custom_cogs": null,"stock": 459},{"sku_id": "1729471625384950217","sku_name": "Ruby Red","product_id": "1800384726159403248","product_name": "Velvet Matte Lipstick – Long Lasting Natural Finish","price": 420.00,"custom_cogs": 95.00,"stock": 356}]}
Product Timeseries Search
https://api.cruva.com/timeseries/productsSearch products by performance over a specific date range. Returns per-product GMV breakdowns (total, affiliate, video, live, shop tab), units sold, shop tab impressions, subscription metrics and price during the period. Supports filtering by product.
Request Body
{"page_size": 10,"page_number": 1,"search_params": {"date_range": { "from": "2026-04-01", "to": "2026-05-01" },"sort_by": "total_gmv","sort_direction": "DESC","product_id": "product_id_123"}}
Parameters:
page_size / page_number: Pagination controlsdate_range: Object withfromandtodates (YYYY-MM-DD)sort_by: One oftotal_gmv,affiliate_gmv,video_gmv,live_gmv,shop_tab_gmv,shop_tab_impressions,units_sold,subscription_gmv,new_subscriptions,active_subscriptions,recurring_subscriptionssort_direction:ASCorDESCproduct_id: *(optional)* Filter by product ID
Subscription fields. subscription_gmv, new_subscriptions, active_subscriptions and recurring_subscriptions are the only subscription metrics with a per-product breakdown — the shop-wide ones (retention rate, take rate, subscription AOV, active subscribers, one-time and recurring GMV) are available from POST /shop/stats instead. subscription_gmv and new_subscriptions are period totals, but active_subscriptions and recurring_subscriptions are point-in-time counts: each product reports its level on its own latest reporting day, not a total across the range. Those two are not additive — do not sum them across periods or across products.
Response
{"data": [{"product_id": "product_id_123","product_name": "Example Product Name","price": 30.0,"status": 1,"is_open_plan": true,"total_gmv": 12345.67,"affiliate_gmv": 8000.0,"video_gmv": 7500.0,"live_gmv": 500.0,"shop_tab_gmv": 3845.67,"shop_tab_impressions": 123456,"units_sold": 410,"subscription_gmv": 1820.5,"new_subscriptions": 32,"active_subscriptions": 148,"recurring_subscriptions": 116}],"has_more": true,"total_count": 14}
SKU Timeseries Search
https://api.cruva.com/timeseries/skusSearch SKUs (product variants) by performance over a specific date range. Returns per-SKU GMV, units sold, orders and gross sales during the period, enriched with the SKU's name and price. Supports filtering by SKU or product.
Request Body
{"page_size": 10,"page_number": 1,"search_params": {"date_range": { "from": "2026-04-01", "to": "2026-05-01" },"sort_by": "gmv","sort_direction": "DESC","product_id": "product_id_123","sku_id": "sku_id_123"}}
Parameters:
page_size / page_number: Pagination controlsdate_range: Object withfromandtodates (YYYY-MM-DD)sort_by: One ofgmv,units_sold,orders,gross_salessort_direction:ASCorDESCproduct_id: *(optional)* Filter to a single product's SKUssku_id: *(optional)* Filter by SKU ID
Response
{"data": [{"sku_id": "sku_id_123","sku_name": "Shade 01 – Ivory","product_id": "product_id_123","product_name": "Example Product Name","price": 30.0,"gmv": 4820.5,"units_sold": 161,"orders": 154,"gross_sales": 5106.9}],"has_more": true,"total_count": 12}
Shop Stats
https://api.cruva.com/shop/statsRetrieve aggregated shop analytics and performance metrics for a given date range. Optionally include daily breakdowns for charting.
Request Body
{"date_range": { "from": "2026-03-01", "to": "2026-04-01" },"include_charts": true,"stats": ["affiliate_gmv","videos_posted","emv"],"product_id": "1234567890","cpm": 5}
Parameters:
date_range: Object withfromandtodates (YYYY-MM-DD)include_charts: Iftrue, includesdaily_countsarray for each statstats: Array of stat keys to retrieve (see table below)product_id: *(optional)* Filter results to a specific productcpm: *(optional)* CPM value for earned media value calculation. Defaults to 5timezone: *(optional)* IANA timezone name (e.g.America/Los_Angeles) deciding which calendar day each event falls on. Omit unless the day boundaries look wrong — see Timezones below
Timezones. By default you don't need to send anything: your numbers are already reported on the timezone your TikTok Shop is registered in, so the days line up with what you see in Seller Center.
Pass timezone when that isn't what you want — the boundaries look shifted, or you want to read the same period from another region's point of view. Give it an IANA name (America/Los_Angeles, Europe/London, Asia/Singapore); an unrecognised value returns 400 rather than being silently ignored.
One limit worth knowing: timezone re-cuts the days for events Cruva timestamps itself — DMs, replies, Target Collab invites, emails, videos posted, sample requests/approvals/shipments/deliveries, and lives. Metrics that arrive from TikTok already summarised per day — GMV, units sold, orders, commission, ad spend, shop-tab, AOV, refunds and every subscription metric — keep TikTok's own day boundaries, because a daily total can't be re-split after the fact. So on a mixed request the outreach and content stats move and the revenue stats don't. date_range is still read as plain calendar dates in the timezone you name, and the period-over-period comparison window shifts with it.
Available Stats
| Stat Key | Chart | Description |
|---|---|---|
| affiliate_gmv | Yes | Affiliate GMV driven by brand in time period |
| total_gmv | Yes | Total GMV including affiliate, ads, and organic |
| affiliate_units_sold | Yes | Affiliate units sold in time period |
| total_units_sold | Yes | Total units sold including affiliate, ads, and organic |
| videos_posted | Yes | All videos posted (affiliate + brand) |
| affiliate_videos_posted | Yes | Affiliate-only videos posted |
| video_views | Yes | Video views driven in time period |
| likes | Yes | Likes from affiliate videos |
| comments | Yes | Comments from affiliate videos |
| gpm | Yes | GMV per 1,000 views |
| emv | Yes | Earned media value = (views / 1000) × CPM |
| commission | Yes | Affiliate commission paid |
| distinct_creators | Yes | Unique creators who posted |
| first_time_posters | Yes | Affiliates who made their first ever post |
| daily_active_affiliates | Yes | Creators with sales — creators earning nonzero GMV. Unfiltered this is attribution-based (includes LIVE); with a campaign_id/product_id filter it counts video-attributed sales only |
| dms_sent | Yes | DMs sent from Cruva (includes open plan cards) |
| open_plan_cards_sent | Yes | Open plan cards sent from Cruva (subset of dms_sent) |
| followup_dms_sent | Yes | Followup DMs sent from Cruva (not included in dms_sent) |
| replies | Yes | Creator replies received |
| tc_invites_sent | Yes | Target Collaboration invites sent from Cruva |
| sample_requests | Yes | Sample requests received |
| tc_sample_requests | Yes | Sample requests via Target Collaboration (subset of sample_requests) |
| open_sample_requests | Yes | Sample requests via Open Collaboration (subset of sample_requests) |
| samples_approved | Yes | Samples approved |
| samples_delivered | Yes | Samples delivered |
| gmv_per_sample_delivered | Yes | GMV per sample delivered |
| live_gmv | Yes | LIVE stream GMV. Unfiltered requests use authoritative shop-wide totals; campaign_id/product_id filters only cover tracked lives |
| lives_posted | Yes | Number of LIVE streams |
| slideshow_gmv | Yes | GMV driven by shoppable slideshows (photo-mode posts). Tracked separately from video_gmv — the two do not overlap |
| slideshow_posts | Yes | Number of slideshows posted. Counted on the day each slideshow was published |
| slideshow_views | Yes | Views on shoppable slideshows during the period |
| video_gmv | Yes | GMV driven by short-form videos |
| shop_tab_gmv | Yes | GMV driven from the shop tab |
| shop_tab_impressions | Yes | Impressions on the shop tab |
| ad_spend | Yes | Total ad spend during the period |
| ad_roi | Yes | Return on ad spend (GMV / ad spend) |
| refund_amount | Yes | Total refunds (currency). Shop-wide — ignores campaign_id; product_id returns 0 |
| gmv_with_cofunding | Yes | GMV including cofunding (currency). Shop-wide — ignores campaign_id; product_id returns 0 |
| platform_orders | Yes | Total platform orders. Shop-wide — ignores campaign_id; product_id returns 0 |
| aov | Yes | Average order value (currency). Total is the average across non-zero days, not a sum. Respects product_id |
| new_content_gmv | Yes | GMV from videos posted within the period (currency). Respects campaign_id and product_id |
| emails_sent | Yes | Emails sent from Cruva. Respects campaign_id |
| samples_shipped | Yes | Samples shipped. Respects campaign_id, product_id, and only_show_campaign_stats |
| refundable_sample_requests | Yes | Refundable samples requested (creator buys and is refunded after posting). Respects campaign_id and product_id |
| refundable_samples_shipped | Yes | Refundable samples shipped. Respects campaign_id and product_id |
| refundable_samples_delivered | Yes | Refundable samples delivered/received. Respects campaign_id and product_id |
| samples_refunded | Yes | Refundable samples refunded, bucketed by refund date. Respects campaign_id and product_id |
| sample_refund_amount | Yes | Total refund amount paid for refundable samples (currency), bucketed by refund date. Respects campaign_id and product_id |
| customer_count | Yes | Unique customers. Whole-shop sum unless product_id is set |
| add_to_cart_count | Yes | Add to cart events. Whole-shop sum unless product_id is set |
| shipping_fees | Yes | Shipping fees (currency). Whole-shop sum unless product_id is set |
| product_impressions | Yes | Product impressions. Whole-shop sum unless product_id is set |
| subscription_revenue | Yes | Revenue from subscription orders (currency). Ignores campaign_id; respects product_id |
| one_time_purchase_gmv | Yes | GMV from one-time, non-subscription purchases (currency). Shop-wide — ignores campaign_id and product_id |
| recurrent_order_gmv | Yes | GMV from repeat/recurring subscription orders (currency). Shop-wide — ignores campaign_id and product_id |
| first_subscription_order_gmv | Yes | GMV from the first order of each new subscription (currency). Shop-wide — ignores campaign_id and product_id |
| non_recurring_gmv | Yes | One-time purchase GMV plus first-subscription-order GMV — the GMV that is not a repeat subscription order (currency). Shop-wide — ignores campaign_id and product_id |
| new_subscriptions | Yes | Subscriptions started during the period. Ignores campaign_id; respects product_id |
| active_subscribers | Yes | Distinct active subscribers. Point-in-time — the value on the latest reported day, not a sum over the period; unreported days carry the previous level forward in daily_counts. Shop-wide — ignores campaign_id and product_id |
| active_subscriptions | Yes | Active subscriptions (a subscriber may hold several). Point-in-time — latest reported day, not a sum. Ignores campaign_id; respects product_id |
| subscription_aov | Yes | Average subscription order value (currency). Period value is total revenue / total orders, not an average of daily averages. Shop-wide — ignores campaign_id and product_id |
| subscription_retention_rate | Yes | Percent of subscriptions retained (0–100). Period value is weighted by each day's active subscriptions, never a flat average. Shop-wide — ignores campaign_id and product_id |
| subscription_take_rate | Yes | Percent of orders that are subscriptions (0–100). Period value is weighted by each day's subscription revenue. Shop-wide — ignores campaign_id and product_id |
| avg_videos_per_creator | No | Average videos posted per creator |
| avg_gmv_per_video | No | Average GMV per video |
| sample_ratio | No | Sample requests / Target Collabs sent |
| reply_ratio | No | Replies / DMs sent |
Stats marked No for Chart will return total_count and percent_change only, even if include_charts is true.
Subscription stats come from the TikTok Shop seller subscription data, which has no campaign or product breakdown — so every subscription stat ignores campaign_id, and only subscription_revenue, new_subscriptions, and active_subscriptions respond to product_id. A day the shop reported nothing is skipped rather than counted as zero: in daily_counts the rate/AOV stats return null for those days (so a chart breaks the line instead of dipping to 0), and the two point-in-time counts carry the previous day's level forward. Because those two are point-in-time, their daily_counts will not sum to total_count.
Response (with charts)
{"data": {"stats": [{"key": "affiliate_gmv","title": "Affiliate GMV","total_count": 188444.1,"daily_counts": [{ "date": "2026-03-28", "count": 37140.0 },{ "date": "2026-03-29", "count": 37791.58 },{ "date": "2026-03-30", "count": 37531.16 },{ "date": "2026-03-31", "count": 36756.52 },{ "date": "2026-04-01", "count": 39224.84 }],"percent_change": -8.1},...]}}
Response (without charts)
{"data": {"stats": [{"key": "affiliate_gmv","title": "Affiliate GMV","total_count": 188444.1,"percent_change": -8.1},...]}}
Shop Performance Score
https://api.cruva.com/shop/spsRetrieve your shop's performance score. This is a measure of your shop's overall health on a scale of 0 to 5. If your score drops below 3.5, your shop will be restricted from sending messages to creators.
No request body required. Uses your x-shop-id header to identify the shop.
Response
{"data": {"sps": 4.5}}
CRM Search
https://api.cruva.com/affiliate/crm/listRetrieve affiliates who have worked with your store (videos, sample requests, showcases).
Request Body
{"page_size": 20,"page_number": 1,"sort_by": "gmv","sort_direction": "desc","just_count": false, // if true, returns only total_count (no results)"filters": {"handle": "example_creator","min_videos": 0,"affiliate_performance": {"min_gmv": 1000, // platform-wide 30-day GMV"max_gmv": 100000,"min_followers": 5000,"max_followers": 500000},"shop_performance": {"min_gmv": 0, // shop-specific lifetime GMV"max_gmv": 100000,"min_video_gmv": 0,"max_video_gmv": 100000,"min_live_gmv": 0,"max_live_gmv": 100000,"min_units_sold": 0,"max_units_sold": 10000}}}
Response
{"results": [{"shop_id": "your_shop_id_here","handle": "example_creator","creator_id": "7171717171717171717","nickname": "Example Creator","showcasing": true,"units_sold": 9081,"video_count": 80,"gmv": 271487.8,"last_post": null,"email": null,"phone_number": null,"video_gmv": 271447.81,"live_gmv": 0.0,"live_count": 0,"commission": 1234.5,"last_live": null,"live_duration": 0,"med_gmv_revenue": 107199, // total GMV earned across the platform in the last 30 days"follower_cnt": 84227,"brand_collaborations": 12, // brands this creator has worked with"post_rate": 1293,"video_engagement": 4.2,"category": "Beauty & Personal Care","category_splits": { "Beauty & Personal Care": 0.71, "Health": 0.29 },"regional_geography": ["US-CA", "US-TX"],"gender": "female","age": "25-34","race": null,"body_type": null,"economic_status": null,"top_follower_gender": "female","top_follower_ages": "18-24","bio": "Skincare + wellness. DM for collabs","tags": []}],"has_more": false,"page_size": 20,"page_offset": 0}
Response Fields:
gmv / video_gmv / live_gmv / units_sold / video_count / live_count / commission: What the creator earned for your shop, all-time (or withindate_rangewhen supplied).last_post / last_live / live_duration: Most recent video post date, most recent LIVE date, and LIVE minutes for your shop.email / phone_number: Contact details when known;emailfalls back to the creator's public profile email.med_gmv_revenue / follower_cnt / brand_collaborations / post_rate / video_engagement / video_avg_view_cnt: Platform-wide creator profile (not scoped to your shop): 30-day GMV across all brands, followers, number of brands worked with, posting rate, engagement rate and 30-day average views, as TikTok reports them.category / category_splits / regional_geography: Content category, share of content per category (0–1), and top viewer regions.gender / age / race / body_type / economic_status: Creator attributes inferred from content;nullwhen unknown.top_follower_gender / top_follower_ages: Dominant audience gender and age band.bio: Creator's TikTok bio.tags: Your shop's tags on this creator (see Tags).
Videos Search
https://api.cruva.com/affiliate/videos/listRetrieve videos and performance metrics.
Request Body
{"page_size": 20,"page_number": 1,"just_count": false, // if true, returns only total_count (no results)"filters": {"sort_by": "gmv","sort_direction": "desc","handle": "example_creator","product_filter": "product_id_123","date_range": { "from": "2026-04-01", "to": "2026-04-07" },"affiliate_performance": {"min_gmv": 100, // platform-wide 30-day GMV"max_gmv": 100000,"min_followers": 1000,"max_followers": 500000},"shop_performance": {"min_gmv": 0, // shop-specific lifetime GMV"max_gmv": 100000,"min_video_gmv": 0,"max_video_gmv": 100000,"min_live_gmv": 0,"max_live_gmv": 100000,"min_units_sold": 0,"max_units_sold": 10000}}}
Response
{"data": {"results": [{"shop_id": "your_shop_id_here","video_id": "12345678","handle": "example_creator","gmv": 130931.64,"post_time": "02/26/2025","view_count": 3417702,"units_sold": 3419,"ctr": 1.6,"like_count": 6382,"comment_count": 169,"title": "4 for 1 deal ends soon!","campaign_id": null,"products": ["product_id_123"],"product": "product_id_123"}],"has_more": false,"page_size": 20,"page_offset": 0}}
CRM Timeseries Search
https://api.cruva.com/timeseries/affiliatesSearch affiliates by performance over a specific date range — the windowed counterpart to CRM Search. Returns GMV (broken out by video, new-video and LIVE), commission, views, engagement metrics and more earned for your brand during the period. Supports sorting and filtering by handle.
Request Body
{"page_size": 10,"page_number": 1,"search_params": {"date_range": { "from": "2026-04-01", "to": "2026-04-07" },"sort_by": "gmv","sort_direction": "DESC","handle": "example_creator"}}
Parameters:
page_size / page_number: Pagination controlsdate_range: Object withfromandtodates (YYYY-MM-DD)sort_by: Field to sort by:gmv,video_gmv,new_video_gmv,commission,units_sold,video_count,live_count,views,likes,comments.live_gmvis not sortable — see the response field notes below.sort_direction:ASCorDESChandle: *(optional)* Filter by creator handlecampaign_id: *(optional)* Filter by campaignproduct_id: *(optional)* Filter by product ID
Response
{"data": [{"handle": "example_creator","nickname": "Example Creator","gmv": 4360.58,"video_gmv": 4218.30,"new_video_gmv": 1902.44,"live_gmv": 142.28,"commission": 186.88,"views": 554296,"likes": 4070,"comments": 50,"units_sold": 167,"video_count": 10,"live_count": 1,"engagement_rate": 0.74,"conversion_rate": 0.0301,"avg_gmv_per_view": 0.0079,"avg_order_value": 26.11,"creator_oecuid": "7171717171717171717","follower_cnt": 84227,"video_avg_view_cnt": 12840,"video_engagement": 4.2,"med_gmv_revenue": 107199,"post_rate": 1293,"category": "Beauty & Personal Care","top_follower_gender": "female","top_follower_ages": "18-24","gender": "female","age": "25-34","bio": "Skincare + wellness. DM for collabs"}]}
Response Fields:
creator_oecuid: Stable TikTok creator id — survives handle changes; join on this, nothandle.follower_cnt / video_avg_view_cnt / video_engagement / med_gmv_revenue / post_rate / bio / category / gender / age / top_follower_*: Platform-wide creator profile as TikTok reports it — not scoped to your shop or todate_range.video_avg_view_cntis TikTok's trailing-30-day average views per video;0means TikTok returned no value. Also present:is_fast_growing,units_sold_range,med_gmv_revenue_range,race,body_type,economic_status,face_visibility,tone,language,tags.gmv: Total shop GMV the creator drove in the window, across every channel (video, LIVE, showcase and other attributed sales). This is the total —video_gmvandlive_gmvare subsets of it, and they do not necessarily add up to it.video_gmv: GMV in the window attributed to the creator's videos, regardless of when each video was posted — so a video posted last year that still sells counts here. Clamped togmv.new_video_gmv: The subset ofvideo_gmvearned by videos posted inside the same `date_range` — i.e. GMV from fresh content, not the back catalogue. With nodate_rangesupplied there is nothing to scope by and this equalsvideo_gmv. Clamped togmv.live_gmv: GMV in the window from the creator's LIVE streams, taken from LIVE session data (the same source as the LIVE endpoints) rather than the daily video stats. Because it is joined on after ranking, it cannot be used as asort_byfield.video_count: Videos the creator posted for your shop in the window.live_count: LIVE streams the creator ran for your shop in the window.engagement_rate:(likes + comments) / views, as a percentage.conversion_rate:units_sold / views, as a percentage.avg_gmv_per_view:gmv / views.avg_order_value:gmv / units_sold.
Video Timeseries Search
https://api.cruva.com/timeseries/videosSearch videos by performance over a specific date range — the windowed counterpart to Videos Search. Returns per-video GMV, views, engagement and conversion metrics earned during the period. Supports filtering by handle and product.
Request Body
{"page_size": 10,"page_number": 1,"search_params": {"date_range": { "from": "2026-04-01", "to": "2026-04-07" },"sort_by": "gmv","sort_direction": "DESC","handle": "example_creator","product_id": "product_id_123"}}
Parameters:
page_size / page_number: Pagination controlsdate_range: Object withfromandtodates (YYYY-MM-DD)sort_by: Field to sort by (e.g.gmv,views,units_sold)sort_direction:ASCorDESChandle: *(optional)* Filter by creator handleproduct_id: *(optional)* Filter by product ID
Response
{"data": [{"video_id": "7606791381478018318","handle": "example_creator","title": "Product review video","post_time": "02/14/2026","product_id": "product_id_123","gmv": 4315.91,"commission": 176.88,"views": 543850,"likes": 3841,"comments": 49,"units_sold": 165,"engagement_rate": 0.72,"conversion_rate": 0.0303,"avg_gmv_per_view": 0.0079,"avg_order_value": 26.16}]}
Carousel Search
https://api.cruva.com/affiliate/slideshows/listRetrieve shoppable slideshows (TikTok photo-mode posts) and performance metrics. Slideshows are tracked separately from videos — a slideshow never appears in Videos Search and vice versa.
Request Body
{"page_size": 20,"page_number": 1,"just_count": false, // if true, returns only total_count (no results)"filters": {"sort_by": "gmv","sort_direction": "desc","handle": "example_creator","product_filter": "product_id_123","campaign_id": "campaign_id_123","date_range": { "from": "2026-04-01", "to": "2026-04-07" }}}
Parameters:
page_size / page_number: Pagination controls (server max page_size is 100)sort_by:gmv,post_time,view_count,like_count,comment_count,units_sold,ctr,engagement_rate. Ad metrics (roi,cpa,ad_spend) are video-only and not available here.sort_direction:ASCorDESChandle: *(optional)* Filter by creator handleproduct_filter: *(optional)* Filter by product IDcampaign_id: *(optional)* Filter by campaigndate_range: *(optional)* Filters by post time (YYYY-MM-DD)
Response
{"data": {"results": [{"shop_id": "your_shop_id_here","video_id": "7656557180228799758","handle": "example_creator","gmv": 5236.06,"post_time": "07/28/2026","view_count": 1367653,"units_sold": 310,"ctr": 0.0162,"like_count": 6382,"comment_count": 169,"engagement_rate": 0.48,"title": "3 ways to style this","campaign_id": null,"products": ["product_id_123"],"product": "product_id_123","slideshow_url": "https://tiktok.com/@example_creator/photo/7656557180228799758"}],"has_more": false,"page_size": 20,"page_offset": 0}}
Carousel Timeseries Search
https://api.cruva.com/timeseries/slideshowsSearch slideshows by performance over a specific date range — the windowed counterpart to Carousel Search. Returns per-slideshow GMV, views, engagement and conversion metrics earned during the period. Supports filtering by handle, product and campaign.
Request Body
{"page_size": 10,"page_number": 1,"search_params": {"date_range": { "from": "2026-04-01", "to": "2026-04-07" },"sort_by": "gmv","sort_direction": "DESC","handle": "example_creator","product_id": "product_id_123","campaign_id": "campaign_id_123"}}
Parameters:
page_size / page_number: Pagination controlsdate_range: Object withfromandtodates (YYYY-MM-DD). Required — defines the aggregation window.sort_by: Field to sort by (e.g.gmv,views,likes,comments,units_sold,commission)sort_direction:ASCorDESChandle: *(optional)* Filter by creator handleproduct_id: *(optional)* Filter by product IDcampaign_id: *(optional)* Filter by campaign
Response
{"data": [{"video_id": "7656557180228799758","handle": "example_creator","title": "3 ways to style this","post_time": "07/28/2026","product_id": "product_id_123","gmv": 4315.91,"commission": 0,"views": 543850,"likes": 3841,"comments": 49,"units_sold": 165,"engagement_rate": 0.72,"conversion_rate": 0.0303,"avg_gmv_per_view": 0.0079,"avg_order_value": 26.16,"slideshow_link": "https://tiktok.com/@example_creator/photo/7656557180228799758"}]}
LIVE Stream Search
https://api.cruva.com/affiliate/lives/listRetrieve live streams and performance metrics.
Request Body
{"page_size": 100,"page_number": 1,"just_count": false, // if true, returns only total_count (no results)"filters": {"sort_by": "gmv","sort_direction": "desc","handle": "example_creator","product_filter": "product_id_123","date_range": { "from": "2026-04-01", "to": "2026-04-07" },"affiliate_performance": {"min_gmv": 100, // platform-wide 30-day GMV"max_gmv": 100000,"min_followers": 1000,"max_followers": 500000},"shop_performance": {"min_gmv": 0, // shop-specific lifetime GMV"max_gmv": 100000,"min_video_gmv": 0,"max_video_gmv": 100000,"min_live_gmv": 0,"max_live_gmv": 100000,"min_units_sold": 0,"max_units_sold": 10000}}}
Response
{"data": {"results": [{"shop_id": "your_shop_id_here","campaign_id": null,"live_id": "7610921877288061726","handle": "example_creator","title": "Product Demo LIVE","start_time": "02/25/2025","duration": 14869,"units_sold": 22,"gmv": 701.2,"views": 87473,"likes": 1539,"comments": 133,"ctr": 0.05,"products": ["product_id_123", "product_id_456"],"products_stats": [{"product_id": "product_id_123","gmv": 564.15,"units_sold": 19,"impressions": 13963},{"product_id": "product_id_456","gmv": 137.05,"units_sold": 3,"impressions": 2567}]}],"page_size": 100,"page_offset": 0,"sort_by": "gmv","sort_direction": "DESC","has_more": false}}
TikTok Ads
Endpoints for paid TikTok performance: shared Spark ad codes plus GMV Max campaign and creative analytics.
To drill from a campaign into its creatives, take the campaign's campaign_id from GMV Max - List Campaigns and pass it to GMV Max - List Creatives.
Note: Spark video matching and GMV Max creative metadata require the TikTok for Business API to be installed on the brand. Without it, the joined video_data / content fields are returned as null.
For Instagram/Facebook partnership ads pushed from Cruva, see the Meta Ads section below.
Spark Ad Search
https://api.cruva.com/spark/listRetrieve spark ad codes shared with your store, with their authorization windows, status, and the joined performance data for each linked video.
Request Body
{"page_size": 10, // max 100"page_number": 1, // 1-based; overrides page_offset if set"page_offset": 0,"include_total": true, // run a COUNT and return total_count"just_count": false, // if true, returns only total_count (no results)"search_params": {"sort_by": "gmv", // gmv | ad_spend | roi | cpa | shared_time | created_at | auth_start_time | auth_end_time | post_time | status"sort_direction": "DESC", // ASC | DESC (nulls sort last)"handle": "smith", // case-insensitive substring match on creator handle"spark_code": "#aBcD...==", // exact match"status": "DELIVERING", // GMV Max delivery status; single value or array"expired": false,"video_ids": ["7600000000000000001", "7600000000000000002"],"shared_time": { "from": "2026-01-01", "to": "2026-12-31" }, // when the code was shared"auth_time": { "from": "2026-01-01", "to": "2026-12-31" }, // auth_start_time >= from AND auth_end_time <= to"post_time": { "from": "2026-01-01", "to": "2026-12-31" } // when the video was posted}}
All fields are optional; defaults are shown above. Each date-range filter also accepts flat keys (e.g. shared_time_from, shared_time_to), and search_params filters may be sent inline at the top level. Also callable over GET with query params.
Sortable columns (`sort_by`): gmv, ad_spend, roi, cpa, shared_time, created_at, auth_start_time, auth_end_time, post_time, status (nulls sort last).
Ad status values (`status`): NOT_ACTIVE, UNAVAILABLE, NOT_DELIVERYING, AUTHORIZATION_NEEDED, IN_QUEUE, DELIVERING, LEARNING, EXCLUDED, REJECTED, NOT_DELIVERING, REVIEWING. This is the GMV Max delivery status (same as GMV Max - List Creatives); both NOT_DELIVERYING and the correctly-spelled NOT_DELIVERING exist upstream as distinct values.
Note: Video matching requires the TikTok for Business API to be installed on the brand. Without it, we cannot join the underlying content data, so video_id and the video_data fields will be returned as null.
Response
{"data": {"results": [{"shop_id": "your_shop_id_here","handle": "example_creator","spark_code": "#aBcD1234eFgH5678iJkL90mNoPqRsTuVwXyZ+/aBcD==","shared_time": "06/11/2026","video_id": "7600000000000000111","auth_start_time": "06/11/2026","auth_end_time": "06/11/2027","status": "DELIVERING","expired": false,"ad_spend": 437.19,"roi": 2.04,"cpa": 14.1,"video_link": "https://tiktok.com/@example_creator/video/7600000000000000111","video_data": {"shop_id": "your_shop_id_here","video_id": "7600000000000000111","handle": "example_creator","campaign_id": "6b2c3d4e5f60718293a4b5c7","title": "5 AMAZING smells in this package!","post_time": "2026-05-31T06:33:49","analysis_time": "2026-06-01T22:48:23.347710","gmv": 507.39,"commission": 77.73,"ctr": 0.0451,"engagement_rate": 0.69,"view_count": 19888,"like_count": 129,"comment_count": 8,"units_sold": 62,"products": ["1700000000000000002"]}},{"shop_id": "your_shop_id_here","handle": "no_video_creator","spark_code": "#zZyYxXwWvVuUtTsSrRqQpPoOnN==","shared_time": "06/08/2026","video_id": null,"auth_start_time": null,"auth_end_time": null,"status": null,"expired": false,"ad_spend": null,"roi": null,"cpa": null,"video_link": "https://tiktok.com/@no_video_creator/video/None","video_data": null}],"page_size": 10,"page_offset": 0,"sort_by": "shared_time","sort_direction": "DESC","has_more": true,"total_count": 40}}
Response Fields:
spark_code: TikTok spark authorization code.created_at / shared_time: When the spark code was shared with the brand (MM/DD/YYYY).shared_timeis the canonical alias ofcreated_at.video_id: TikTok video id, ornullif not yet linked.auth_start_time / auth_end_time: Authorization window (MM/DD/YYYY), ornull.status: GMV Max ad delivery status — the same values as GMV Max - List Creatives:NOT_ACTIVE,UNAVAILABLE,NOT_DELIVERYING,AUTHORIZATION_NEEDED,IN_QUEUE,DELIVERING,LEARNING,EXCLUDED,REJECTED,NOT_DELIVERING,REVIEWING. (BothNOT_DELIVERYINGand the correctly-spelledNOT_DELIVERINGexist upstream as distinct values.)nullwhen the code isn't running as a GMV Max ad.expired: Whether the spark code is expired.ad_spend / roi / cpa: GMV Max delivery metrics for the ad, ornullwhen the code isn't running as a GMV Max ad.cpais cost per acquisition. Sortable viasort_by.video_link:https://tiktok.com/@{handle}/video/{video_id}(literalNonewhenvideo_idis null).video_data: Joined content row (matched onshop_id+video_id) with analytics fields, ornullwhen no matching content row exists. Timestamps are ISOYYYY-MM-DDTHH:MM:SS.total_count: Total matching rows;nullwheninclude_totalis false.
GMV Max - List Campaigns
https://api.cruva.com/ads/campaignsList the shop's GMV Max ad campaigns with metrics aggregated over a date range, plus shop-level summary totals with a prior-period comparison. To drill into a campaign's per-video creatives, pass its campaign_id to GMV Max - List Creatives.
Request Body
{"date_range": { "from": "2026-01-01", "to": "2026-12-31" }, // required"page": 1, // 1-based, default 1"page_size": 10, // default 10, clamped to 1-100"sort_by": "ad_spend", // ad_spend | ad_revenue | orders | campaign_name | operation_status | budget | roas_target"sort_direction": "DESC" // ASC | DESC (nulls sort last)}
Parameters:
date_range.from / date_range.to: Required.YYYY-MM-DDbounds. Returns400 "Missing date_range (from/to)"if absent.page: *(optional)* 1-based page number. Defaults to1.page_size: *(optional)* Results per page. Defaults to10, clamped to1–100.sort_by: *(optional)* One ofad_spend,ad_revenue,orders,campaign_name,operation_status,budget,roas_target. Invalid values fall back toad_spend.sort_direction: *(optional)*ASCorDESC(nulls sort last). Defaults toDESC.
Request keys accept both snake_case and camelCase (page_size/pageSize, sort_by/sortBy, date_range/dateRange, sort_direction/sortDir). Also reachable at the /v1/ads/campaigns alias.
The prior-period comparison window is computed automatically: the same number of days immediately preceding date_range. Invalid sort_by values silently fall back to ad_spend.
Response
{"data": {"totals": {"ad_spend": 412532.45,"ad_roi": 7.94,"ad_revenue": 3275489.10,"prev": {"ad_spend": 388110.22,"ad_roi": 7.41,"ad_revenue": 2876300.55}},"campaigns": [{"campaign_id": "1820000000000000001","campaign_name": "Pillow Bundle 2&4pk","operation_status": "ENABLE","budget": 8000.0,"roas_target": 5.7,"schedule_type": "SCHEDULE_FROM_NOW","schedule_start_time": "2025-01-16 18:18:59","schedule_end_time": "2035-01-14 18:18:59","product_specific_type": "CUSTOMIZED_PRODUCTS","auto_budget_enabled": false,"store_id": "7400000000000000001","item_group_ids": "1729000000000000001","ad_spend": 172521.52,"ad_revenue": 1516512.52,"ad_roi": 8.79,"orders": 40103,"cost_per_order": 4.30},{"campaign_id": "1820000000000000002","campaign_name": "Pillow 4pk Promo","operation_status": "ENABLE","budget": 2000.0,"roas_target": 6.0,"schedule_type": "SCHEDULE_START_END","schedule_start_time": "2025-11-13 00:00:00","schedule_end_time": "2038-01-01 00:00:00","product_specific_type": "CUSTOMIZED_PRODUCTS","auto_budget_enabled": false,"store_id": "7400000000000000001","item_group_ids": "1731000000000000002","ad_spend": 121487.72,"ad_revenue": 736974.12,"ad_roi": 6.07,"orders": 16030,"cost_per_order": 7.58}],"total_count": 166,"page": 1,"page_size": 10,"sort_by": "ad_spend","sort_direction": "DESC"}}
Error Responses
// 400 — campaigns require a date range{ "error": "Missing date_range (from/to)" }// 400 — missing or malformed shop header{ "error": "Missing X-Shop-Id header" }{ "error": "Invalid shop_id" }// 403 — shop scope / plan failures{ "error": "Shop not found or not owned by user" }{ "error": "This shop is not on an active paid plan" }
Response Fields:
totals: Shop-wide ad summary over the range (fromshop_performance):ad_spend(total ad spend),ad_revenue(total ad-attributed revenue), andad_roi(ad_revenue / ad_spend,0if no spend).totals.prev: The same three fields for the equal-length prior period, for trend / % change.campaigns: Page of campaigns. Campaigns with no spend in the range still appear (left-joined) with zeroed metrics.campaigns[].campaign_id: GMV Max campaign id.campaigns[].campaign_name: Campaign name, ornull.campaigns[].operation_status: Campaign state, e.g.ENABLE,DISABLE. May benull.campaigns[].budget: Campaign budget.campaigns[].roas_target: Target return on ad spend.campaigns[].schedule_type: e.g.SCHEDULE_FROM_NOW,SCHEDULE_START_END. May benull.campaigns[].schedule_start_time / schedule_end_time: Schedule window as datetime strings, ornull.campaigns[].product_specific_type: e.g.CUSTOMIZED_PRODUCTS. May benull.campaigns[].auto_budget_enabled: Whether auto-budget is enabled.campaigns[].store_id: TikTok store id, ornull.campaigns[].item_group_ids: Targeted product/item group id(s). Optionally pass asproduct_idsto GMV Max - List Creatives — thoughcampaign_idis the direct way to list a campaign's creatives.campaigns[].ad_spend / ad_revenue / orders: Metrics aggregated over the range.campaigns[].ad_roi:ad_revenue / ad_spend(0if no spend).campaigns[].cost_per_order:ad_spend / orders(0if no orders).total_count: Counts all campaigns for the shop, independent of the date range.
GMV Max - List Creatives
https://api.cruva.com/ads/creativesList per-creative (per-video) GMV Max ad performance aggregated over a date range, joined to video metadata. To list a specific campaign's creatives, pass its campaign_id (from GMV Max - List Campaigns).
Request Body
{"performance_date": { "from": "2026-06-01", "to": "2026-06-30" }, // optional; METRICS window, defaults to trailing 6 months"posted_date": { "from": "2026-05-01", "to": "2026-05-31" }, // optional; filters videos by post date"page": 1, // 1-based, default 1"page_size": 10, // default 5, clamped to 1-1000"sort_by": "ad_spend", // see sortable list in notes"sort_direction": "DESC", // ASC | DESC (nulls last)"campaign_id": "1820000000000000001", // restrict to one campaign"product_ids": ["1729000000000000001"], // restrict to these product IDs"video_ids": ["7600000000000000111"], // look up specific creative video IDs"statuses": ["DELIVERING"] // filter by ad status; omit for all}
Parameters:
performance_date.from / performance_date.to: *(optional)*YYYY-MM-DDbounds of the metrics window — all ad metrics are aggregated over these days. Either bound may be sent alone (fromdefaults to 6 months ago,toto today). Omit entirely for the trailing 6 months.posted_date.from / posted_date.to: *(optional)*YYYY-MM-DDbounds filtering videos by post date; videos with an unknown post date are excluded when set. Either bound may be sent alone. The legacydate_rangekey is an alias of this field.page: *(optional)* 1-based page number. Defaults to1.page_size: *(optional)* Results per page. Defaults to5, clamped to1–1000.sort_by: *(optional)* See the sortable list in the notes. Invalid values fall back toad_spend.sort_direction: *(optional)*ASCorDESC(nulls last). Defaults toDESC.campaign_id: *(optional)*string. Restrict to a single GMV Max campaign (itscampaign_idfrom GMV Max - List Campaigns). Combines with the product/video filters.product_ids: *(optional)*string[]orstring. Restrict to these product IDs (e.g. a campaign'sitem_group_ids).video_ids: *(optional)*string[]orstring. Look up specific creative video IDs.statuses: *(optional)*string[]orstring. Filter by ad status (see notes). Omit for all.
All fields are optional. Request keys accept both snake_case and camelCase (campaign_id/campaignId, page_size/pageSize, product_ids/productIds, video_ids/video_id/videoIds, sort_by/sortBy, sort_direction/sortDir). Also reachable at the /v1/ads/creatives alias.
Two independent date ranges:
performance_date is the metrics window: every ad metric is aggregated over exactly those days (ad_spend/gross_revenue/orders etc. are sums; roi, cpa, and click_rate are recomputed as ratio-of-sums over the window; ad_click_rate, conversion_rate, and the view rates are weighted averages of the daily values). Either bound may be sent alone — the missing side defaults. When omitted entirely, the window defaults to the trailing 6 months, the deepest range with daily data. status is the latest value observed within the window; the same creative returns different metrics for different windows.
posted_date filters which videos are included, by post date — independent of the metrics window ("videos posted in May, measured over June" is valid). The legacy date_range key is an alias of posted_date (its original meaning), so pre-existing integrations behave exactly as before.
The campaign_id and product/video filters combine — send campaign_id alone, products alone, both (a campaign narrowed to specific products), or neither (the whole shop).
Sortable columns (`sort_by`): date_posted, ad_spend, gross_revenue, roi, cpa, orders, cost_per_order, impressions, clicks, click_rate, ad_click_rate, conversion_rate, view_rate_2s, view_rate_6s, view_count. Invalid values fall back to ad_spend.
Ad status values (`statuses`): NOT_ACTIVE, UNAVAILABLE, NOT_DELIVERYING, AUTHORIZATION_NEEDED, IN_QUEUE, DELIVERING, LEARNING, EXCLUDED, REJECTED, NOT_DELIVERING, REVIEWING. (Both NOT_DELIVERYING and the correctly-spelled NOT_DELIVERING exist upstream as distinct values.) The response also returns available_statuses — the statuses actually present for the current shop/campaign/product/video/date scope, before the statuses filter is applied.
Response
{"data": {"creatives": [{"video_id": "7600000000000000111","product_id": "1729000000000000001","status": "UNAVAILABLE","date_posted": "2025-12-23","ad_spend": 349869.26,"gross_revenue": 409347.04,"roi": 1.17,"cpa": 34.62,"orders": 10105,"cost_per_order": 34.62,"impressions": 8847221,"clicks": 165403,"click_rate": 1.87,"ad_click_rate": 2.03,"conversion_rate": 6.1,"view_rate_2s": 42.35,"view_rate_6s": 21.08,"handle": "ava_wellness","title": "Oop the gym crowd is gonna be salty about this one","view_count": 22165182,"url": "https://www.tiktok.com/@ava_wellness/video/7600000000000000111","video_link": "https://www.tiktok.com/@ava_wellness/video/7600000000000000111"},{"video_id": "7600000000000000222","product_id": "1729000000000000002","status": "DELIVERING","date_posted": "2026-04-24","ad_spend": 258095.20,"gross_revenue": 281323.77,"roi": 1.09,"cpa": 18.11,"orders": 14251,"cost_per_order": 18.11,"impressions": 6120044,"clicks": 142880,"click_rate": 2.33,"ad_click_rate": 2.51,"conversion_rate": 9.97,"view_rate_2s": 39.4,"view_rate_6s": 18.62,"handle": "noah.digest","title": "If you bloat after ANYTHING even healthy food, this is for u","view_count": 15978701,"url": "https://www.tiktok.com/@noah.digest/video/7600000000000000222","video_link": "https://www.tiktok.com/@noah.digest/video/7600000000000000222"}],"total_count": 1284,"page": 1,"page_size": 10,"available_statuses": ["AUTHORIZATION_NEEDED", "DELIVERING", "EXCLUDED", "IN_QUEUE","LEARNING", "NOT_ACTIVE", "NOT_DELIVERYING", "REJECTED", "UNAVAILABLE"],"sort_by": "ad_spend","sort_direction": "DESC"}}
Error Responses
// 400 — missing or malformed shop header{ "error": "Missing X-Shop-Id header" }{ "error": "Invalid shop_id" }// 403 — shop scope / plan failures{ "error": "Shop not found or not owned by user" }{ "error": "This shop is not on an active paid plan" }
Response Fields:
creatives: Page of per-video creatives.creatives[].video_id: TikTok video id.creatives[].product_id: Product the creative advertised, ornull.creatives[].status: Ad status (see the status list in the notes) as last observed within the metrics window, ornull.creatives[].date_posted: Video post dateYYYY-MM-DD, ornull. Not affected bydate_range.creatives[].ad_spend / gross_revenue / roi / cpa: Ad performance over the metrics window.roiandcpaare ratio-of-sums (window revenue ÷ window spend; window spend ÷ window orders).creatives[].orders / cost_per_order / impressions / clicks: Order and reach counts over the metrics window.cost_per_orderequalscpaat window grain.creatives[].click_rate / ad_click_rate / conversion_rate / view_rate_2s / view_rate_6s: Percentages (e.g.1.87= 1.87%).click_rateis exact over the window (clicks ÷ impressions); the others are impression/click-weighted averages of the daily values.creatives[].handle: Creator handle, ornullif no matching content row.creatives[].title: Video caption/title, ornull.creatives[].view_count: Organic video views.creatives[].url / video_link:https://www.tiktok.com/@{handle}/video/{video_id}(or.../video/{video_id}when the handle is unknown).video_linkis an alias ofurl.available_statuses: Distinct statuses present in the base scope (shop + campaign + products + videos + date) before thestatusesfilter — drives a status-filter UI.total_count: Total matching creatives within the status-filter scope.
Meta Ads
Analytics for the Meta Ads suite: Instagram/Facebook partnership ads created by pushing creator TikTok videos from Cruva, and the creator programs those videos come from.
Scope — Cruva-pushed ads only. These endpoints are not a mirror of your whole Meta Ads account. Performance comes from a daily ClickHouse mirror covering the ads pushed through Cruva (the Meta Ads programs / licensed-content push flow) and the campaigns and ad sets they live in. Ads created directly in Meta Ads Manager and never pushed through Cruva generally do not appear — and in campaigns that mix both, non-Cruva entities can surface as raw Meta ids with null names, because display names are stamped at push time.
Drill-down: List Campaigns → pass campaign_id to List Ad Sets → pass adset_id to List Ads. Programs live in List Programs; pass a program_id to List Program Creators or to any of the ads endpoints to scope metrics to one program.
Revenue caveat: purchase_value (and sales on programs) is Meta-attributed purchase value — it requires the brand's site to send purchase events via the Pixel or Conversions API. TikTok Shop sales do not appear in these fields.
Meta Ads - List Campaigns
https://api.cruva.com/meta/campaignsList the Meta ad campaigns containing ads pushed from Cruva, with metrics aggregated over a date range. Cruva-pushed ads only — this is not your whole Meta Ads account (see the section overview). To drill into a campaign, pass its campaign_id to Meta Ads - List Ad Sets or Meta Ads - List Ads.
Request Body
{"date_range": { "from": "2026-07-01", "to": "2026-07-31" }, // required"campaign_ids": ["120210000000000001"], // optional; restrict to specific campaigns"program_id": "66b1f0aa77cc00112233dd01", // optional; only ads attributed to this program"page": 1, // 1-based, default 1"page_size": 10, // default 10, clamped to 1-100"sort_by": "spend", // spend | impressions | clicks | ctr | purchases | purchase_value | roas | cost_per_purchase | campaign_name"sort_direction": "DESC" // ASC | DESC (nulls sort last)}
Parameters:
date_range.from / date_range.to: Required.YYYY-MM-DDbounds for the metrics window. Returns400 "Missing date_range (from/to)"if absent.campaign_ids: *(optional)* Meta campaign id(s) to restrict to. Accepts a single string or an array.program_id: *(optional)* Meta Ads program id (from Meta Ads - List Programs). Only counts ads attributed to that program.page / page_size: *(optional)* 1-based page, results per page (default 10, max 100).sort_by / sort_direction: *(optional)* Sort key (see request body) andASC/DESC. Defaults:spendDESC, nulls last.
Request keys accept both snake_case and camelCase. Invalid sort_by values silently fall back to spend. Metrics are summed per campaign over the window from the daily ClickHouse mirror, so any date range is fast — no live Meta Graph API calls are made.
Add "format": "csv" to receive a presigned CSV download link instead of inline JSON (optionally with columns and csv_limit).
Response
{"data": {"campaigns": [{"campaign_id": "120210000000000001","campaign_name": "Creator ads - July","pushed_ads": 34,"spend": 8241.55,"impressions": 1204551,"clicks": 18342,"purchases": 411,"purchase_value": 24518.20,"ctr": 0.0152,"roas": 2.97,"cost_per_purchase": 20.05},{"campaign_id": "120210000000000002","campaign_name": null,"pushed_ads": 0,"spend": 312.09,"impressions": 60112,"clicks": 702,"purchases": 9,"purchase_value": 401.33,"ctr": 0.0117,"roas": 1.29,"cost_per_purchase": 34.68}],"total_count": 6,"page": 1,"page_size": 10,"sort_by": "spend","sort_direction": "DESC"}}
Error Responses
// 400 — a date range is required{ "error": "Missing date_range (from/to)" }// 400 — missing or malformed shop header{ "error": "Missing X-Shop-Id header" }{ "error": "Invalid shop_id" }
Response Fields:
campaigns[].campaign_id: Meta campaign id, ornullfor spend rows the mirror couldn't attribute to a campaign.campaigns[].campaign_name: Campaign name as stamped at push time, ornullwhen no Cruva push ever recorded a name for it (e.g. an entity created outside Cruva).campaigns[].pushed_ads: How many Cruva-pushed ads (registry rows) this campaign contains — all-time, not date-scoped.campaigns[].spend / impressions / clicks / purchases / purchase_value: Summed over the date range.purchase_valueis Meta-attributed (Pixel/CAPI); TikTok Shop sales don't appear here.campaigns[].ctr:clicks / impressionsover the window (a fraction, e.g.0.0152= 1.52%).campaigns[].roas:purchase_value / spend(0if no spend).campaigns[].cost_per_purchase:spend / purchases(0if no purchases).total_count: Total campaigns with activity in the window (within the filters).
Meta Ads - List Ad Sets
https://api.cruva.com/meta/adsetsList the Meta ad sets containing ads pushed from Cruva, with metrics aggregated over a date range and a campaign breadcrumb on every row. Cruva-pushed ads only (see the section overview). Filter by campaign_id (from Meta Ads - List Campaigns) to see one campaign's ad sets; pass an adset_id to Meta Ads - List Ads for the ads inside.
Request Body
{"date_range": { "from": "2026-07-01", "to": "2026-07-31" }, // required"campaign_id": "120210000000000001", // optional; single id or array via campaign_ids"adset_ids": ["120210000000000101"], // optional; look up specific ad sets"program_id": "66b1f0aa77cc00112233dd01", // optional"page": 1,"page_size": 10, // max 100"sort_by": "spend", // spend | impressions | clicks | ctr | purchases | purchase_value | roas | cost_per_purchase | adset_name | campaign_name"sort_direction": "DESC"}
Parameters:
date_range.from / date_range.to: Required.YYYY-MM-DDmetrics window.campaign_id / campaign_ids: *(optional)* Restrict to one or more Meta campaigns.adset_ids: *(optional)* Meta ad set id(s) to look up.program_id: *(optional)* Only count ads attributed to this Meta Ads program.page / page_size / sort_by / sort_direction: *(optional)* Pagination and sorting; defaults1/10/spend/DESC.
Same conventions as Meta Ads - List Campaigns: snake/camel keys, spend fallback sort, no live Meta calls, and "format": "csv" for a presigned CSV export.
Response
{"data": {"adsets": [{"adset_id": "120210000000000101","adset_name": "July creators","campaign_id": "120210000000000001","campaign_name": "Creator ads - July","pushed_ads": 22,"spend": 5120.33,"impressions": 733208,"clicks": 11250,"purchases": 268,"purchase_value": 16821.90,"ctr": 0.0153,"roas": 3.28,"cost_per_purchase": 19.11}],"total_count": 3,"page": 1,"page_size": 10,"sort_by": "spend","sort_direction": "DESC"}}
Response Fields:
adsets[].adset_id / adset_name: Meta ad set id and its push-time name (nullname when never pushed into from Cruva).adsets[].campaign_id / campaign_name: Parent campaign breadcrumb.adsets[].pushed_ads: Cruva-pushed ads in this ad set (all-time registry count).adsets[].spend … cost_per_purchase: Same metric semantics as Meta Ads - List Campaigns.total_count: Total ad sets with activity in the window (within the filters).
Meta Ads - List Ads
https://api.cruva.com/meta/adsList individual Meta ads (ad creatives) with metrics over a date range. These are the partnership ads created by pushing TikTok videos from Cruva — each row maps back to its source TikTok video and creator. Not a mirror of your whole Meta Ads account (see the section overview).
Request Body
{"date_range": { "from": "2026-07-01", "to": "2026-07-31" }, // required"campaign_id": "120210000000000001", // optional"adset_id": "120210000000000101", // optional"program_id": "66b1f0aa77cc00112233dd01", // optional"handle": "example_creator", // optional; exact creator handle (case-insensitive)"video_ids": ["7600000000000000111"], // optional; source TikTok video ids"page": 1,"page_size": 10, // max 1000"sort_by": "spend", // spend | impressions | clicks | ctr | purchases | purchase_value | roas | cost_per_purchase | handle | title | post_time | view_count"sort_direction": "DESC"}
Parameters:
date_range.from / date_range.to: Required.YYYY-MM-DDmetrics window.campaign_id / adset_id: *(optional)* Restrict to one campaign and/or one ad set (arrays accepted viacampaign_ids/adset_ids).program_id: *(optional)* Only ads attributed to this Meta Ads program.handle: *(optional)* Creator TikTok handle — only that creator's ads.video_ids: *(optional)* Source TikTok video id(s) — every ad created from those videos.page / page_size / sort_by / sort_direction: *(optional)* Pagination and sorting; defaults1/10/spend/DESC.
Same conventions as the other Meta Ads endpoints: snake/camel keys, spend fallback sort, "format": "csv" for a presigned CSV export. handle matches the push-time creator handle exactly (leading @ ignored).
Response
{"data": {"ads": [{"ad_id": "120210000000001001","campaign_id": "120210000000000001","campaign_name": "Creator ads - July","adset_id": "120210000000000101","adset_name": "July creators","program_id": "66b1f0aa77cc00112233dd01","handle": "example_creator","video_id": "7600000000000000111","title": "5 AMAZING smells in this package!","view_count": 19888,"like_count": 129,"comment_count": 8,"post_time": "2026-05-31 06:33:49","pushed_time": "2026-07-02 14:20:11.532000","video_link": "https://www.tiktok.com/@example_creator/video/7600000000000000111","spend": 812.40,"impressions": 118322,"clicks": 1922,"purchases": 44,"purchase_value": 2610.75,"ctr": 0.0162,"roas": 3.21,"cost_per_purchase": 18.46}],"total_count": 34,"page": 1,"page_size": 10,"sort_by": "spend","sort_direction": "DESC"}}
Response Fields:
ads[].ad_id: Meta ad id.ads[].campaign_id / campaign_name / adset_id / adset_name: Container breadcrumbs; names are push-time stamps (nullwhen unknown).ads[].program_id: Meta Ads program the push was attributed to, ornull.ads[].handle: Creator's TikTok handle from the push registry (falls back to the content row), ornull.ads[].video_id / title / view_count / like_count / comment_count / post_time: The source TikTok video and its organic stats (not ad metrics).ads[].pushed_time: When the ad was created through Cruva, ornull.ads[].video_link: TikTok URL of the source video, ornullwhen handle/video are unknown.ads[].spend … cost_per_purchase: Ad metrics over the window — same semantics as Meta Ads - List Campaigns.purchase_valueis Pixel/CAPI-attributed.total_count: Total ads with activity in the window (within the filters).
Meta Ads - List Programs
https://api.cruva.com/meta/programsList the shop's Meta Ads creator programs — joinable offers whose members license their TikTok content for Meta partnership ads — with member counts, ad performance, and creator earnings. To list a program's members, pass its program_id to Meta Ads - List Program Creators.
Request Body
{"statuses": ["active", "paused"], // optional; default active + paused. Add "archived" for deleted programs."date_range": { "from": "2026-07-01", "to": "2026-07-31" }, // optional; scopes ONLY the spend/sales rollup (all-time when omitted)"page": 1,"page_size": 10, // max 100"sort_by": "created_at", // created_at | name | spend | sales | roas | earnings_paid | earnings_pending | members_approved"sort_direction": "DESC"}
Parameters:
statuses: *(optional)* Subset ofactive,paused,archived. Defaults toactive+paused(archived = deleted programs, kept for history).date_range.from / date_range.to: *(optional)*YYYY-MM-DDwindow for the spend/sales rollup. Omit for all-time.page / page_size / sort_by / sort_direction: *(optional)* Pagination and sorting; defaults1/10/created_at/DESC.
spend/sales cover only the program's Cruva-pushed Meta ads. Member counts and earnings are always lifetime — date_range scopes just the ad-performance rollup. Add "format": "csv" for a presigned CSV export.
Response
{"data": {"programs": [{"program_id": "66b1f0aa77cc00112233dd01","name": "Core Creators","description": "Our main creator program.","short_id": "a1b2c3d4","join_url": "https://creators.cruva.com/p/a1b2c3d4","status": "active","approval_mode": "manual","cadence": "biweekly","commission_rules": [{ "type": "gmv_pct", "pct": 10 },{ "type": "flat_per_creator", "amount_cents": 5000 }],"created_at": "2026-06-12T18:04:11.201000","last_payout_at": "2026-08-01T09:00:00","members_approved": 48,"members_pending": 6,"spend": 8241.55,"sales": 24518.20,"roas": 2.97,"earnings_paid": 2451.82,"earnings_pending": 310.44}],"total_count": 2,"page": 1,"page_size": 10,"sort_by": "created_at","sort_direction": "DESC"}}
Response Fields:
programs[].program_id: Program id — pass to Meta Ads - List Program Creators or asprogram_idon the ads endpoints.programs[].join_url / short_id: The public join link creators use (hosted on Cruva Creators).programs[].status:active(join link open),paused(link closed, everything else unaffected), orarchived(deleted).programs[].approval_mode:manual— joining creators wait for brand approval;auto— approved immediately.programs[].cadence: Payout trigger:daily,weekly,biweekly, ormonthly. Payouts are queued on this schedule and only sent after brand approval.programs[].commission_rules: How members earn.typeis one ofgmv_pct(% of Meta-attributed sales),flat_per_order,flat_per_submission,flat_per_creator(one-time on join),meta_ad_performance(% of ad spend). Percentage rules carrypct; flat rules carryamount_cents.programs[].members_approved / members_pending: Live membership counts.programs[].spend / sales / roas: Ad performance of the program's Cruva-pushed Meta ads (all-time, or the requesteddate_range).salesis Meta-attributed purchase value (Pixel/CAPI).programs[].earnings_paid / earnings_pending: Creator payouts from the program's ledger: total paid out, and total currently pending brand approval.total_count: Programs matching the status filter.
Meta Ads - List Program Creators
https://api.cruva.com/meta/programs/creatorsList the creators inside the shop's Meta Ads programs — membership status, commission terms, lifetime paid earnings, and how many Cruva-pushed partnership ads run each creator's content. Scope to one program with program_id (from Meta Ads - List Programs), or omit it for all programs.
Request Body
{"program_id": "66b1f0aa77cc00112233dd01", // optional; omit for all programs"statuses": ["pending", "approved"], // optional; default pending + approved. Also: rejected, removed."handle": "smith", // optional; case-insensitive substring match on TikTok handle"page": 1,"page_size": 10, // max 100"sort_by": "joined_at", // joined_at | approved_at | handle | commission_pct | earnings_paid | ads_count"sort_direction": "DESC"}
Parameters:
program_id: *(optional)* Restrict to one program's members.statuses: *(optional)* Subset ofpending,approved,rejected,removed. Defaults topending+approved.handle: *(optional)* Case-insensitive substring match on the creator's TikTok handle.page / page_size / sort_by / sort_direction: *(optional)* Pagination and sorting; defaults1/10/joined_at/DESC(nulls sort last).
A creator can be in at most one program per brand at a time, so shop-wide listings have one row per creator. Live Meta whitelisting status is not included — it requires per-creator Graph API calls; check it on the dashboard's program Creators tab. Add "format": "csv" for a presigned CSV export.
Response
{"data": {"creators": [{"creator_id": "3f1c2d9e-8a45-4b6b-9d2c-51e0aa77bb01","handle": "example_creator","status": "approved","program_id": "66b1f0aa77cc00112233dd01","program_name": "Core Creators","commission_pct": 10,"commission_rules": [{ "type": "gmv_pct", "pct": 10 }],"joined_at": "2026-06-20T15:12:44.100000","approved_at": "2026-06-21T09:30:02.550000","earnings_paid": 512.34,"ads_count": 7,"ig_usernames": ["example.creator"]}],"total_count": 54,"page": 1,"page_size": 10,"sort_by": "joined_at","sort_direction": "DESC"}}
Response Fields:
creators[].creator_id: Cruva community account id (UUID) of the creator.creators[].handle: Creator's TikTok handle, ornull.creators[].status: Membership status:pending(awaiting brand approval),approved,rejected, orremoved.creators[].program_id / program_name: The program this membership belongs to.creators[].commission_pct: The GMV % snapshotted on the membership, ornull(older memberships only carry this; seecommission_rulesfor the full terms).creators[].commission_rules: The program's current commission rules (same shape as Meta Ads - List Programs).creators[].joined_at / approved_at: Membership timestamps (ISO),approved_atnullwhile pending.creators[].earnings_paid: Lifetime paid payouts to this creator from this program's ledger.creators[].ads_count: How many Meta partnership ads run this creator's content — Cruva-pushed ads only, all-time.creators[].ig_usernames: Instagram usernames linked on the creator's Cruva account (empty array when none). Live Meta whitelisting status is not included here.total_count: Total memberships matching the filters.
Sample Funnel
https://api.cruva.com/affiliate/samples/funnelAggregate view of a shop's sample requests: how many sit in each status right now, how far the cohort progressed through requested → approved → shipped → delivered → posted, and the median days between each stage.
Use this instead of paging Sample Request Search and tallying rows — it is a single aggregate over the whole matching set, with no pagination and no row cap, so it is both faster and correct where a paged count would truncate. Reach for the search endpoint when you need the individual rows (creator handles, apply_ids to act on, or a CSV export).
Note this counts the sample requests themselves, one row per request. It is not a per-creator view: a creator with three requests is counted three times.
Request Body
{ (all fields optional)"time_field": "timestamp", // which timestamp defines the cohort (see note)"time_from": "2026-08-01","time_to": "2026-08-30", // inclusive of the whole day"product_ids": ["product_id_123"], // or "product_id": "product_id_123""campaign_id": "campaign_id_123", // or "all_campaigns": true"source": "TC", // "TC" or "Open""include_ignored": false // true also counts ignored requests}
`time_field` picks which timestamp defines the cohort and defaults to timestamp (when the sample was requested), so the funnel reads as *"of the requests submitted in this window, how far did they get"*. Accepts timestamp, approved, shipped, delivered, and posted (the underlying column names — sample_approved, sample_shipped, sample_received, sample_posted — also work). Anchor on a later milestone to ask a different question: time_field: "approved" with an August window is *"of the samples we approved in August, how many got posted"*.
Omit time_from/time_to for all time. A non-default time_field restricts the cohort to rows that actually carry that timestamp.
Stage counts are cumulative, not exclusive. A request counts as having reached a stage if it ever passed through it, so funnel counts only ever shrink down the funnel. They are derived from the milestone timestamps *and* the status — a request sitting in Content Pending counts as delivered even if its sample_received is missing.
Response
{"data": {"time_field": "timestamp","time_from": "2026-08-01","time_to": "2026-08-30","total_count": 1608,"by_status": [ // current state, biggest first{"status": "Rejected","count": 1463,"pct_of_total": 90.98,"is_open": false // terminal status; no ageing reported},{"status": "To Review","count": 122,"pct_of_total": 7.59,"is_open": true, // still moving, so ageing is included"avg_age_days": 12.4, // days since the request came in"oldest_age_days": 74.2}],"funnel": [ // progression; counts never increase{"stage": "requested","label": "Requested","count": 1608,"pct_of_entry": 100.0,"conversion_from_previous_pct": null, // null on the entry stage"dropped_from_previous": null},{"stage": "approved","label": "Approved","count": 145,"pct_of_entry": 9.02,"conversion_from_previous_pct": 9.02,"dropped_from_previous": 1463},{ "stage": "shipped", "label": "Shipped", "count": 145, "pct_of_entry": 9.02, "conversion_from_previous_pct": 100.0, "dropped_from_previous": 0 },{ "stage": "delivered", "label": "Delivered", "count": 143, "pct_of_entry": 8.89, "conversion_from_previous_pct": 98.62, "dropped_from_previous": 2 },{ "stage": "posted", "label": "Posted", "count": 121, "pct_of_entry": 7.53, "conversion_from_previous_pct": 84.62, "dropped_from_previous": 22 }],"timing": [ // median_days is null when too few rows{ "step": "request_to_approval", "label": "Requested → Approved", "median_days": 0.38 },{ "step": "approval_to_shipment", "label": "Approved → Shipped", "median_days": 1.0 },{ "step": "shipment_to_delivery", "label": "Shipped → Delivered", "median_days": 3.23 },{ "step": "delivery_to_post", "label": "Delivered → Posted", "median_days": 3.0 }],"biggest_bottleneck": { // worst-converting step, or null"stage": "approved","label": "Approved","dropped": 1463,"conversion_pct": 9.02}}}
No matching requests
{"data": {"time_field": "timestamp","time_from": "1999-01-01","time_to": "1999-01-02","total_count": 0,"by_status": [],"funnel": [],"timing": [],"biggest_bottleneck": null}}
Response Fields:
total_count: Requests in the cohort — every request whosetime_fieldfalls in the window, after the product/campaign/source filters.by_status[]: Current state: where each request sits now, ordered bycountdescending. Statuses come from the data, so shops carrying legacy values (Delivered,Content Posted,Pending,Approved) will see them here.by_status[].pct_of_total: The status's share oftotal_count.by_status[].is_open:truewhile the request is still moving (To Review,Ready to Ship,Shipped,Content Pending,Overdue, …). Ageing fields are present only on these — the age of aRejectedrow is not a queue anyone is working through.by_status[].avg_age_days / oldest_age_days: Days since the request was submitted, averaged and maxed over the status. A large, oldTo Reviewqueue is a review backlog; a large, oldContent Pendingqueue is creators sitting on product. Open statuses only.funnel[]: Progression throughrequested→approved→shipped→delivered→posted. Cumulative, so counts never increase down the list.funnel[].pct_of_entry: The stage's share of the entry stage (total_count).funnel[].conversion_from_previous_pct: Share of the previous stage that reached this one.nullon the entry stage.funnel[].dropped_from_previous: Requests that reached the previous stage but not this one.nullon the entry stage.timing[].median_days: Median days for that transition, over requests carrying both timestamps.nullwhen no request has made the transition. Slow is not the same as leaky — a stage can convert well and still take weeks, so readtimingalongsidefunnel.biggest_bottleneck: The transition losing the largest *share* of the funnel (not the largest absolute count, which would always name the first step).nullwhen nothing drops. Note a large drop atapprovedis normal for most shops, which reject or expire the bulk of inbound requests — weigh the later steps and the timings before calling it a problem.
Sample Request Search
https://api.cruva.com/affiliate/samples/listRetrieve affiliate sample requests.
Request Body
{"page_size": 50, // max 100"page_number": 1, // (page * page_size) cannot exceed 1000"sort_by": "timestamp","sort_direction": "desc","just_count": false, // if true, returns only total_count (no results, faster response)"filters": { (all optional)"sample_status": ["To Review", "Ready to Ship"], // pass ["ignore"] to view only ignored requests"handle": "example_creator","product_filter": "product_id_123","product_ids": ["product_id_123", "product_id_456"], // alternative to product_filter if you want to filter by multiple specific products"date_range": { // filter by when the sample was requested"from": "2026-04-01","to": "2026-04-07"},"approval_time": { // filter by when the sample was approved"from": "2026-04-01","to": "2026-04-07"},"affiliate_performance": { // platform-wide affiliate performance (not specific to this shop)"min_gmv": 1000,"max_gmv": 100000,"min_followers": 5000,"max_followers": 500000},"shop_performance": { // shop-specific performance"min_gmv": 0,"max_gmv": 100000,"min_video_gmv": 0,"max_video_gmv": 100000,"min_live_gmv": 0,"max_live_gmv": 100000,"min_units_sold": 0,"max_units_sold": 10000}},"prev_worked_with": true, // true = creator has previously posted/shipped/etc. for this shop"expiring_soon": false, // true = expire_date within next 3 days"source": "TC" // "TC" or "Open"}
Sort options: timestamp, med_gmv_revenue, med_gmv_revenue_range, follower_cnt, post_rate, pps, gpm, average_gmv_per_video, expire_date, shop_gmv, engagement.
All filter fields are optional.
Response
{"data": {"results": [{"shop_id": "your_shop_id_here","creator_id": "7171717171717171717","sampled_product": "product_id_123","status": "To Review","campaign_id": null,"timestamp": "2025-09-10T01:00:57.815932","sample_approved": "2025-09-12T14:22:11.000000","sample_received": null,"expire_date": "2025-10-10T00:00:00.000000","source": "TC","apply_id": "9191919191919191919","handle": "example_creator","bio": "creator bio text","follower_cnt": 4413,"post_rate": "N/A","video_engagement": 0.04,"med_gmv_revenue": 0, // total GMV earned across the platform in the last 30 days"med_gmv_revenue_range": 1,"pps": 0,"gpm": 0,"shop_gmv": 0,"average_gmv_per_video": 0,"previous_approved": false // true if this creator has another active sample with this shop}],"page_number": 1,"page_size": 50,"total_count": 1, // populated on page 1 or when just_count = true"has_more": false}}
Count-only Response (just_count: true)
{"data": {"total_count": 142}}
Refundable Sample Request Search
https://api.cruva.com/affiliate/samples/refundable/listRetrieve refundable sample requests — samples the creator purchases themselves and is refunded for after posting. Unlike regular sample requests there is no request/approval step, so the status set is reduced and each row carries refund fields (refund_time, refund_amount, review_expire_time) plus SKU-level detail (sku_id, sku_desc) and the offered commission_rate.
Request Body
{"page_size": 50, // max 100"page_number": 1, // (page * page_size) cannot exceed 1000"sort_by": "timestamp","sort_direction": "desc","time_field": "timestamp", // which timestamp the date_range filters on (see note)"just_count": false, // if true, returns only total_count (no results, faster response)"filters": { (all optional)"sample_status": ["Shipped", "Refunded"],"handle": "example_creator","product_filter": "product_id_123","product_ids": ["product_id_123", "product_id_456"], // alternative to product_filter for multiple products"exclude_product_ids": ["product_id_789"],"sku_ids": ["1729382056", "1729382057"],"date_range": { // applied against time_field"from": "2026-04-01","to": "2026-04-07"},"affiliate_performance": { // platform-wide affiliate performance (not specific to this shop)"min_gmv": 1000,"max_gmv": 100000,"min_followers": 5000,"max_followers": 500000},"shop_performance": { // shop-specific performance"min_gmv": 0,"max_gmv": 100000,"min_video_gmv": 0,"max_video_gmv": 100000,"min_live_gmv": 0,"max_live_gmv": 100000,"min_units_sold": 0,"max_units_sold": 10000}},"campaign_id": "campaign_id_123" // or "all_campaigns": true}
Sort options: timestamp, sample_shipped, sample_received, refund_time, refund_amount, commission_rate, review_expire_time, follower_cnt, shop_gmv.
`time_field` selects which timestamp column date_range filters on: timestamp (when the sample was requested, default), sample_shipped, sample_received, refund_time, or review_expire_time — e.g. "refunded in June" → time_field: "refund_time".
All filter fields are optional.
Response
{"data": {"results": [{"shop_id": "your_shop_id_here","creator_id": "7171717171717171717","sampled_product": "product_id_123","status": "Refunded","campaign_id": null,"timestamp": "2026-06-10T01:00:57.815932","apply_id": "9191919191919191919","sku_id": "1729382056","sku_desc": "Color: Black / Size: M","commission_rate": 1500, // basis points (1500 = 15%)"sample_shipped": "2026-06-12T14:22:11","sample_received": "2026-06-15T09:10:00","refund_time": "2026-06-20T18:05:42","refund_amount": 19.99,"review_expire_time": "2026-07-15T00:00:00","handle": "example_creator","bio": "creator bio text","follower_cnt": 4413,"med_gmv_revenue": 0, // total GMV earned across the platform in the last 30 days"med_gmv_revenue_range": 1,"shop_gmv": 0,"product_info": {"product_id": "product_id_123","product_name": "Example Product","is_open_plan": true,"price": 24.99,"units_sold": 1200}}],"page": 1,"page_size": 50,"total_count": 1, // populated on page 1 or when just_count = true"has_more": false}}
Count-only Response (just_count: true)
{"data": {"total_count": 37}}
Approve Sample Requests
https://api.cruva.com/affiliate/samples/approveApprove one or more sample requests by their apply_id. You can get these IDs from the Sample Request Search endpoint above.
Request Body
{"apply_ids": ["9191919191919191919","9191919191919191920"]}
Parameters:
apply_ids: Array ofapply_idvalues from the Sample Request Search response
Response
{"data": {"message": "Successfully approved 2 sample requests","success_count": 2}}
Reject Sample Requests
https://api.cruva.com/affiliate/samples/rejectReject one or more sample requests by their apply_id. You can get these IDs from the Sample Request Search endpoint above.
Request Body
{"apply_ids": ["9191919191919191919","9191919191919191920"]}
Parameters:
apply_ids: Array ofapply_idvalues from the Sample Request Search response
Response
{"data": {"message": "Successfully rejected 2 sample requests","success_count": 2}}
Outreach - Send Direct Message
https://api.cruva.com/affiliate/message/dmSend a direct message to a creator. Identify the recipient by either handle or conversation_id — one of the two is required. Only available in the US and UK for now.
Request Body
{"handle": "example_creator", // required unless conversation_id is supplied"conversation_id": "7451907556216407553" // required unless handle is supplied"message": "Hello, collab with us!"}
Parameters:
handle: Creator's TikTok handle. Required unlessconversation_idis supplied. The conversation is created if one doesn't exist yet.conversation_id: Send into an existing conversation, e.g. one returned by List Inbox. Required unlesshandleis supplied, and takes precedence when both are given. Skips the handle lookup, so it also works for creators who aren't in our affiliates database.message: The message text to send.
Response
{"data": {"success": true,"message_id": "123456789","message": "Success"}}
Response Fields:
success:falseif TikTok rejected the send — checkmessagefor the reason. Note that the endpoint still returns200in that case, so branch on this field rather than the status code.message_id: TikTok's ID for the message just sent.message:Success, or the upstream error reason whensuccessisfalse(e.g. an IM quota limit based on your 30-day affiliate GMV).
Outreach - List Messages
https://api.cruva.com/affiliate/message/listList the messages exchanged with a creator, most recent first. Results are paginated with an opaque cursor (TikTok requires token-based pagination, not numeric offsets).
Request Body
{"handle": "example_creator","page_size": 20,"page_token": "eyJjdXJzb3IiOiIxNzMwOTk..." // omit on first request}
Parameters:
handle: Creator's TikTok handlepage_size: Number of messages to return. Max 20.page_token: Optional. Omit on the first request. To fetch the next page, pass back thenext_page_tokenreturned in the previous response. Continue untilhas_moreisfalse.
Response
{"data": {"success": true,"message": "success","has_more": true,"messages": [{"message_id": "7451907556216407553","content": "","type": "TARGET_COLLABORATION_CARD","sender": "brand","send_time": "Apr 08, 2026 10:20 PM UTC"},{"message_id": "7451907556216407554","content": "Hi example_creator! Thanks for reaching out about a potential collaboration. Feel free to browse our products and request a sample. We'll review your request shortly!","type": "TEXT","sender": "brand","send_time": "Feb 27, 2026 02:33 PM UTC"},{"message_id": "7451907556216407555","content": "How can we work together","type": "TEXT","sender": "creator","send_time": "Feb 27, 2026 02:33 PM UTC"}],"next_page_token": "eyJjdXJzb3IiOiIxNzMwOTk..."}}
Response Fields:
messages: Array of messages for this page, most recent first.messages[].message_id: TikTok's unique ID for the message.messages[].type: Message type, e.g.TEXT,IMAGE,PRODUCT_CARD,TARGET_COLLABORATION_CARD,FREE_SAMPLE_CARD,EMOTICONS, orSYSTEM. OnlyTEXTmessages are guaranteed to have acontentstring. TikTokNOTIFICATIONevents are filtered out.messages[].sender:creatororbrand.has_more:trueif additional pages are available.next_page_token: Opaque cursor for the next page. Pass this aspage_tokenon the subsequent request. Absent (or empty) oncehas_moreisfalse.
Pagination Example
# Loop until has_more is false, threading next_page_token forward.page_token = Nonewhile True:body = {"handle": "example_creator", "page_size": 20}if page_token:body["page_token"] = page_tokenresp = requests.post(list_url, headers=headers, json=body).json()data = resp.get("data", {})for message in data.get("messages", []):handle(message)if not data.get("has_more"):breakpage_token = data.get("next_page_token")if not page_token:break
Outreach - List Inbox
https://api.cruva.com/affiliate/message/inboxList conversations in the shop's message inbox, optionally filtered by status. Results are paginated with an opaque cursor (TikTok requires token-based pagination, not numeric offsets).
Request Body
{"conversation_status": "UNREPLIED","page_size": 20,"page_token": "eyJjdXJzb3IiOiIxNzMwOTk..." // omit on first request}
Parameters:
conversation_status: One ofALL,UNREPLIED,READ, orUNREADpage_size: Number of conversations to return. Max 50.page_token: Optional. Omit on the first request. To fetch the next page, pass back thenext_page_tokenreturned in the previous response. Continue untilhas_moreisfalse.
Response
{"data": {"success": true,"conversations": [{ "conversation_id": "7451907556216407553", "username": "example_creator_1", "unread_count": 2 },{ "conversation_id": "7451907556216407554", "username": "example_creator_2", "unread_count": 2 },{ "conversation_id": "7451907556216407555", "username": "example_creator_3", "unread_count": 3 },{ "conversation_id": "7451907556216407556", "username": "example_creator_4", "unread_count": 1 },{ "conversation_id": "7451907556216407557", "username": "example_creator_5", "unread_count": 1 }],"has_more": true,"next_page_token": "eyJjdXJzb3IiOiIxNzMwOTk..."}}
Response Fields:
conversations: Array of conversations for this page.conversations[].conversation_id: TikTok's unique ID for the conversation. Pass this to Send Direct Message asconversation_idto reply without a handle lookup.conversations[].username: The creator's TikTok handle.conversations[].unread_count: Number of messages in the conversation the shop hasn't read yet.has_more:trueif additional pages are available.next_page_token: Opaque cursor for the next page. Pass this aspage_tokenon the subsequent request. Absent (or empty) oncehas_moreisfalse.
Pagination Example
# Loop until has_more is false, threading next_page_token forward.page_token = Nonewhile True:body = {"conversation_status": "ALL", "page_size": 20}if page_token:body["page_token"] = page_tokenresp = requests.post(inbox_url, headers=headers, json=body).json()data = resp.get("data", {})for convo in data.get("conversations", []):handle(convo)if not data.get("has_more"):breakpage_token = data.get("next_page_token")if not page_token:break
Outreach - List Activity Logs
https://api.cruva.com/outreach/logs/listSearch the per-message activity log across all of the shop's outreach — DM/invite automations and email campaigns alike. One row per attempted message: creator handle, campaign, channel, timestamp, and the error when it failed. campaign_id is optional by design: "which campaign messaged @handle?" is a handle_search with no campaign filter.
Request Body
{"page": 1, // optional, default 1"page_size": 25, // optional, default 25, max 100"handle_search": "gracewearsit", // optional — substring match on the creator handle"campaign_id": "6a14aa4f8c93e2105d7bf830", // optional — an automation or email campaign id"message_type": "Email", // optional — see message types below"status": "fail", // optional, "success" | "fail""error_code": "refused", // optional — substring match on the error text"date_from": "2026-08-01T00:00:00Z", // optional ISO-8601 lower bound"date_to": "2026-08-14T23:59:59Z" // optional ISO-8601 upper bound}
Parameters:
handle_search: Case-insensitive substring match on the creator handle.campaign_id: Restrict to one campaign — an automationcampaign_id(from/automations/list) or an email campaign id. Omit to search across every campaign.message_type: Filter by channel. One of the values in the reference below.status:success= delivered;fail= the row carries anerror_code.error_code: Case-insensitive substring match on the error text — e.g.refused,already.date_from / date_to: ISO-8601 bounds on the log timestamp. Either or both.
Response 200
{"data": {"results": [{"handle": "gracewearsit","creator_id": "7495637218804154932","timestamp": "2026-08-14T11:52:03","campaign_id": "6a6ba7b231e1010f55846996","campaign_name": "TEST","message_type": "Email","status": "fail","error_code": "Recipient refused: grace@example.com","sender_email": "pizza@surgify.io"}],"total_count": 1,"page": 1,"page_size": 25,"has_more": false}}
Response Fields:
results[]: Log rows, newest first.results[].status:successorfail—failrows carry the failure reason inerror_code.results[].sender_email: The mailbox the message was sent from (email campaigns;nullon rows written before this field existed).total_count: Total rows matching the filters.
Automations - List Automations
https://api.cruva.com/automations/listList the outreach automations (campaigns) configured for the shop. Results are paginated and can be filtered by name or campaign id.
Request Body
{"page": 1,"page_size": 10,"search": "","campaign_id": "","status": "active","message_type": "invite+dm","sort_by": "affiliate_gmv","sort_direction": "desc"}
Parameters:
page: Optional. Page to fetch. Defaults to1.page_size: Optional. Number of results per page. Defaults to25, max100.search: Optional. Case-insensitive substring match against the automation name.campaign_id: Optional. Filter to a specific automation.status: Optional. Filter by automation state. One ofactiveorstopped.message_type: Optional. Filter by outreach mode. One ofdm,invite, orinvite+dm.sort_by: Optional. Sort field — expand Sortable fields below. Defaults tocreated_at.sort_direction: Optional.ascordesc(default).
Response
{"data": {"has_more": true,"page": 1,"page_size": 10,"results": [{"campaign_id": "7c3a91e4d2f8b56091ad473e","shop_id": "4d8e1f2a9b3c70516a8d2e4f","campaign_name": "[Product 2] - Lookalike Audience - MOF","status": "active","created_at": "2026-05-11 04:30:32.098000","dm_messages": [{"message_type": "invite_card"},{"content": "Hi [affiliate_name]! I noticed your amazing content and wanted to reach out about a potential collaboration.","message_type": "message"},{"content": "Hi [affiliate_name]! Any thoughts?","followup_time_days": 3,"message_type": "followup"}],"invite_details": {"invite_title": "Join our affiliate program","invite_message": "Hi [affiliate_name]! We'd love to have you on board — check out the product and let us know if you'd like a sample.","contact_email": "brand@example.com","expire_time": 6,"expire_grain": "weeks","expiration_type": "relative","expiration_date": null,"resolve_conflicts": true,"products": [{"commission": 25,"id": "191958783233","shopAdsCommission": 12,"showShopAdsCommission": true}],"sample_policy": {"auto_approve_samples": false,"offer_samples": true}},"last_updated_at": "2026-05-11 04:30:32.498000","message_type": "invite+dm","messages_sent": 4823,"messages_remaining": 12047,"replies_received": 612,"sample_requests": 187,"videos_posted": 94,"affiliate_gmv": 28453.71,"outreach_audience": "list","outreach_filters": {},"lists": ["Summer Creators", "VIPs"],"group_id": null,"send_to_all": true,"content_type": "any","time_limits": {"from": "08:00","timezone": "America/New_York","to": "22:00"},"daily_message_limits": { "monday": 100, "friday": 50 },"daily_limits_timezone": "America/New_York","filter_by_entry_date": false,"entry_date_threshold": null,"exclude_groups": [],"exclude_list_ids": [1421],"exclude_automations": []},...],"total_count": 95}}
Response Fields:
results: Array of automation objects for the current page.campaign_id: Identifier to pass to/automations/toggle.message_type: Outreach mode. One ofinvite,dm, orinvite+dm.dm_messages: Ordered sequence of messages the DM automation sends. Each item has amessage_type(e.g.message,image,product,batch_product,batch_image,followup) and a payload that varies by type. Empty whenmessage_typeisinvite.invite_details: Configuration for invite automations:invite_title,invite_message,contact_email, the expiry (expire_time/expire_grainfor relative, orexpiration_type: "date"+expiration_date),resolve_conflicts, theproductsoffered (with commission rates), andsample_policy.nullwhenmessage_typeisdm.outreach_audience: Which creators the automation targets (list,new_affiliates, orgroups).outreach_filters: Filters applied on top of the audience — see the Creator Filters Reference.lists: Forlistaudiences: every uploaded-list title the campaign targets (legacy single-list bots surface as a one-element array).send_to_all:falsemeans the campaign skips creators already messaged by the shop's other campaigns.content_type: Preferred creator content type:any,live, orvideo.time_limits: Daily send window and timezone the automation respects. Empty object when unrestricted.daily_message_limits: Per-weekday send caps (lowercase weekday keys, 0-3000). Empty object when uncapped.group_id: Forgroupsaudiences: the CRMgroup_idthe automation targets.nullotherwise.exclude_groups / exclude_list_ids / exclude_automations: Creators to skip: members of these CRM groups (group_id), these lists (list_id, from/lists/list), or creators already targeted by these automations (campaign_id).messages_remaining: Estimated audience left to contact.nullwhen the estimator hasn't run for this campaign.has_more:trueif additional pages are available.total_count: Total number of automations matching the query.
Automations - Create Automation
https://api.cruva.com/automations/createCreate a new outreach automation. Returns the new campaign_id and the estimated messages_remaining for audiences that can be precomputed. Optionally idempotent — see the Idempotency section. Use [affiliate_name] as a placeholder for the affiliate's name in the message and invite message text fields.
Request Body
{"title": "Summer outreach", // required"message_type": "invite+dm", // required, "dm" | "invite" | "invite+dm""outreach_audience": "new_affiliates", // required, "new_affiliates" | "groups" | "list""outreach_filters": { }, // optional — see Creator Filters Reference"list_ids": [1421, 1508], // required if outreach_audience == "list" — up to 10 lists"group_id": "6a15c8b29f4d1e7350a8c742", // required if outreach_audience == "groups""invite_details": { // required if message_type is "invite" or "invite+dm""title": "Try our product!", // max 29 chars"message": "Hey [affiliate_name], love your content...", // max 500 chars"expire_time": 6,"expire_grain": "weeks", // "days" | "weeks" | "months""expire_date": "2026-09-01T00:00:00Z", // OR a fixed calendar expiry (wins over expire_time)"contact_email": "brand@example.com","offer_free_samples": true, // required boolean"auto_approve_free_samples": false, // defaults to false if not provided"resolve_conflicts": true, // defaults to false if not provided"products": [{ "product_id": "1800291847362058192", "commission": 15, "shop_ads_commission": 5 }]},"dm_messages": [ // optional, ordered DM steps — see item types below{ "type": "invite_card" },{ "type": "message", "content": "Hi [affiliate_name]!" },{ "type": "followup", "content": "Any thoughts?", "followup_time_days": 3 }],"status": "stopped", // optional, "active" | "stopped", default "stopped""send_to_all": true, // optional, default true — see below"content_type": "any", // optional, "any" | "live" | "video""time_limits": { // optional daily send window"from": "09:00","to": "20:00","timezone": "America/New_York"},"daily_message_limits": { // optional per-weekday caps, 0-3000 each"monday": 100,"friday": 50},"daily_limits_timezone": "America/New_York", // timezone the daily caps reset in"filter_by_entry_date": false, // optional"entry_date_threshold": "2026-06-01T00:00:00Z","exclude_groups": ["6a15c8b29f4d1e7350a8c742"],"exclude_list_ids": [1421], // list_id values from /lists/list"exclude_automations": ["7c3a91e4d2f8b56091ad473e"]}
Parameters:
title: Display name for the automation.message_type: Outreach mode.invitesends Target Collab invitations only,dmsends direct messages only,invite+dmcombines both (recommended — the invite card rides inside the DM thread).outreach_audience: Who to target.new_affiliates= the platform-wide creator pool narrowed byoutreach_filters;groups= a saved CRM segment (requiresgroup_id);list= one or more saved lists (requireslist_ids— see the Lists endpoints).outreach_filters: Optional filter object narrowing thenew_affiliatespool. Full field vocabulary and allowed values in the Creator Filters Reference below.list_ids: Required forlistaudiences: an array of up to 10list_idvalues, as returned byPOST /lists/createandPOST /lists/list.group_id: Forgroupsaudiences: agroup_idfrom/groups/list.invite_details: Required whenmessage_typeisinviteorinvite+dm. Must includetitle(≤29 chars),message(≤500 chars),contact_email,offer_free_samples(explicit boolean), at least one entry inproducts, and an expiry: either relative (expire_time≥ 1 +expire_grain) or a fixed date (expire_date, ISO-8601 — takes precedence when both are sent).auto_approve_free_samplesandresolve_conflictsare optional booleans.invite_details.products[]: Products offered on the invite. Each entry:product_id(string),commission(percent, 1-80), optionalshop_ads_commission(percent, 0.01-80).dm_messages: Ordered list of DM steps — required for `dm` / `invite+dm`. Ignored for pureinvite. `invite+dm` enforces a strict sequence: item[0]must be{ "type": "invite_card" }and item[1]must be a plain{ "type": "message", "content": ... }— an invite card with no message after it is rejected (400). Extra messages/images and follow-ups may only come after that pair. Plaindmneeds at least one non-followup step and never contains aninvite_card. Max 5 items per campaign (including the invite card and follow-ups) — TikTok blocks senders after 5 unanswered messages, so requests past the cap return400. Each item is{ "type": ... }plus a type-specific payload — expand DM message item types below.message/followupcontent is capped at 2,500 characters. Follow-up delays must be unique integers ≥ 1. Server assigns each step a UUID.status: Initial status. One ofactiveorstopped. Defaults tostoppedso you can review before launch.send_to_all: Defaulttrue— message every matching creator. Setfalseto skip creators already messaged by any of this shop's other campaigns (cross-campaign dedupe).content_type: Preferred creator content type for invites:any(default),live(shoppable LIVE), orvideo(shoppable video).time_limits: Daily send window:from/toas 24-hourHH:MMstrings plus an IANAtimezone(e.g.America/New_York). Always pass the timezone — without it the times are interpreted as UTC. Omit the object entirely for round-the-clock sending.daily_message_limits: Per-weekday send caps. Keys are lowercase weekday names (monday…sunday), values are integers 0-3000. A missing day is uncapped;0pauses that day entirely.daily_limits_timezone: IANA timezone whose midnight resets the daily caps.filter_by_entry_date / entry_date_threshold: Whenfilter_by_entry_dateistrue, only creators whose audience entry date passesentry_date_threshold(ISO-8601) are messaged.exclude_groups / exclude_list_ids / exclude_automations: *(optional)* Creators to skip: members of these CRM groups (group_id), these lists (list_id, from/lists/list), or creators already targeted by these automations (campaign_id).
Side effect: for outreach_audience of new_affiliates or list, the server invokes an audience-size estimator and stores messages_remaining on the bot. This step fails soft — the automation is created even if the estimator times out (in that case messages_remaining is returned as null).
Limits: a shop may have at most 1000 non-archived automations (403 past the cap), and a list audience may combine at most 10 lists.
Response 201
{"data": {"message": "Automation created.","campaign_id": "6a14aa4f8c93e2105d7bf830","status": "stopped","messages_remaining": 3978511}}
Error Responses
// 400 — validation error (the specific field is named in the message){ "error": "invite_details.contact_email is required" }// 403 — 1000-bot cap reached{ "error": "Automation limit reached for this shop" }
Response Fields:
campaign_id: Identifier of the new automation. Pass this to/automations/toggleor/automations/delete.messages_remaining: Estimated audience size fornew_affiliates/list.nullforgroupsor when the estimator failed soft.
Email Campaigns - List Sender Emails
https://api.cruva.com/emails/senders/listList the shop's linked sender emails (the mailboxes email campaigns send from — Gmail, Outlook, SMTP, or custom-domain addresses) and its custom sending domains with their verification status. Only a verified domain can have addresses created on it via Create Custom Sender Email.
Response 200
{"data": {"sender_emails": [{"email": "outreach@surgifycreators.com","type": "ses","sender_name": "Sam from Surgify","daily_limit": 60,"messages_sent_today": 14,"created_at": "2026-08-14 09:12:44.183000"},{"email": "brand@gmail.com","type": "gmail","sender_name": null,"daily_limit": 60,"messages_sent_today": 0,"created_at": "2026-06-02 17:30:11.902000"}],"custom_domains": [{ "domain": "surgifycreators.com", "status": "verified", "created_at": "2026-08-13 20:01:37.554000" }]}}
Response Fields:
sender_emails[].type:gmail,outlook,smtp, orses(custom domain address — no mailbox behind it).sender_emails[].sender_name: From display name (custom domain addresses only;nullotherwise).sender_emails[].daily_limit / messages_sent_today: Per-mailbox daily cap and today's usage — sending pauses for a mailbox once it hits its cap.custom_domains[].status:pending,verified, orfailed. Domains are added and DNS-verified in the Cruva dashboard (Outreach > Email Campaigns > Manage Sender Emails > Manage Domains) — that step can't be done via API because it requires publishing DNS records.
Email Campaigns - Create Custom Sender Email
https://api.cruva.com/emails/senders/create-customCreate a sender email address on one of the shop's verified custom domains — e.g. domain surgifycreators.com + local_part outreach makes outreach@surgifycreators.com. No mailbox or credentials needed; the address sends through the verified domain. Calling again for an existing address updates it (e.g. its sender_name) without using another plan slot. Optionally idempotent — see the Idempotency section.
Request Body
{"domain": "surgifycreators.com", // required — a verified domain from /emails/senders/list"local_part": "outreach", // required — the part before the @"sender_name": "Sam from Surgify" // optional From display name, max 100 chars}
Parameters:
domain: A custom domain already linked to the shop withstatus: "verified"(see List Sender Emails). Unverified or unknown domains are rejected.local_part: The address name before the@— lowercase letters, digits, and. _ % + -between alphanumerics.sender_name: Optional From display name shown in recipients' inboxes. Change it later with Update Sender Name.
Plan limits: new addresses count against the shop's linked-email allowance (Basic: 1, Growth: 3, Scale: 100). Re-creating an existing address is an update, not a new slot.
Response 201
{"data": {"message": "Sender email created.","email": "outreach@surgifycreators.com","sender_name": "Sam from Surgify"}}
Error Responses
// 404 — domain not linked to this shop{ "error": "Domain 'surgifycreators.com' is not linked to this shop. Add and verify it in Cruva under Outreach > Email Campaigns > Manage Sender Emails > Manage Domains (requires publishing DNS records)." }// 400 — domain linked but not verified yet{ "error": "Domain 'surgifycreators.com' is not verified yet (status: pending). Publish its DNS records and wait for verification before creating addresses." }// 403 — plan's linked-email allowance used up{ "error": "The Growth plan allows up to 3 linked emails. Please upgrade to link more." }
Email Campaigns - Update Sender Name
https://api.cruva.com/emails/senders/update-sender-nameSet or clear the From display name on a custom domain sender email (type: "ses"). Gmail/Outlook/SMTP mailboxes carry their own account name and are rejected with a 400.
Request Body
{"email": "outreach@surgifycreators.com", // required — from /emails/senders/list"sender_name": "Sam from Surgify" // max 100 chars; empty string clears the name}
Response 200
{"data": {"message": "Sender name updated.","email": "outreach@surgifycreators.com","sender_name": "Sam from Surgify"}}
Email Campaigns - Create Email Campaign
https://api.cruva.com/emails/campaigns/createCreate an email outreach campaign that emails creators from the shop's linked sender emails. Returns the new campaign_id and the estimated audience size where it can be precomputed. Optionally idempotent — see the Idempotency section. Use [affiliate_name] as a placeholder for the creator's name in subject and email_body. Only plain email campaigns are creatable here — Target Collab email campaigns require invite machinery owned by the dashboard.
Request Body
{"title": "Summer email outreach", // required"subject": "Partner with us, [affiliate_name]?", // required"email_body": "<p>Hi [affiliate_name], ...</p>", // required, HTML supported, max 2 MB"sender_emails": [ // required — linked addresses, max 10"outreach@surgifycreators.com","brand@gmail.com"],"cc_emails": ["manager@brand.com"], // optional, max 10"daily_limit": 120, // optional — max 150 per sender email; default 60 per sender"outreach_audience": "new_affiliates", // optional, default "new_affiliates"; also "groups" | "list""outreach_filters": { }, // optional — see Creator Filters Reference"list_ids": [1421, 1508], // required if outreach_audience == "list" — up to 10 lists"group_id": "6a15c8b29f4d1e7350a8c742", // required if outreach_audience == "groups""status": "stopped", // optional, "active" | "stopped", default "stopped""send_to_all": true, // optional, default true"filter_by_entry_date": false, // optional"entry_date_threshold": "2026-06-01T00:00:00Z","exclude_groups": ["6a15c8b29f4d1e7350a8c742"],"exclude_list_ids": [1421] // list_id values from /lists/list}
Parameters:
title: Display name for the campaign.subject: Email subject line. Supports the[affiliate_name]placeholder.email_body: Email body — HTML supported, max 2 MB. Supports the[affiliate_name]placeholder.sender_emails: Addresses to send from — every one must already be linked to the shop (400names any that aren't; check with List Sender Emails). Max 10. Sending rotates across them and respects each mailbox's own daily limit.cc_emails: Optional CC recipients added to every email. Max 10, each a valid address.daily_limit: Campaign-wide emails/day cap. At most 150 per sender email (400past the cap); defaults to 60 per sender email.outreach_audience: Who to target — same semantics as/automations/create:new_affiliates(platform-wide creator pool narrowed byoutreach_filters; only creators with a known email address receive anything),groups(requiresgroup_id), orlist(requireslist_ids).outreach_filters: Optional filter object narrowing thenew_affiliatespool. Full vocabulary in the Creator Filters Reference.list_ids: Required forlistaudiences: an array of up to 10list_idvalues.group_id: Forgroupsaudiences: agroup_idfrom/groups/list.status: Initial status.activestarts sending immediately; defaultstoppedso you can review before launch.send_to_all: Defaulttrue— email every matching creator.falseskips creators already contacted by the shop's other campaigns.filter_by_entry_date / entry_date_threshold: Whenfilter_by_entry_dateistrue, only creators whose audience entry date passesentry_date_threshold(ISO-8601) are emailed.exclude_groups / exclude_list_ids: *(optional)* Creators to skip: members of these CRM groups (group_id) or these lists (list_id, from/lists/list).
Side effect: for outreach_audience of new_affiliates or list, the server invokes an audience-size estimator and stores messages_remaining on the campaign. This step fails soft — the campaign is created even if the estimator times out (then messages_remaining is null).
Limits: a shop may have at most 1000 non-archived email campaigns (403 past the cap).
Response 201
{"data": {"message": "Email campaign created.","campaign_id": "6a6ba7b231e1010f55846996","status": "stopped","messages_remaining": 812445}}
Error Responses
// 400 — a sender address isn't linked to the shop{ "error": "sender email(s) not linked to this shop: hello@notlinked.com. List available senders with GET /emails/senders/list." }// 403 — 1000-campaign cap reached{ "error": "Limit reached. You can only create 1000 email campaigns per store." }// 413 — email_body over 2 MB{ "error": "email_body is too large (max 2 MB)" }
Response Fields:
campaign_id: Identifier of the new email campaign. Pass it to/emails/campaigns/update,/emails/campaigns/toggle, or/emails/campaigns/delete; its per-message send results appear in List Activity Logs.messages_remaining: Estimated audience size fornew_affiliates/list.nullforgroupsor when the estimator failed soft.
Email Campaigns - List Email Campaigns
https://api.cruva.com/emails/campaigns/listList the shop's email campaigns with their full configuration — subject, body preview, sender emails, CC, audience, daily limit — and send stats. Archived campaigns are hidden unless status: "archived" is passed. Use this to read a campaign's current values before an update.
Request Body
{"page": 1, // optional, default 1"page_size": 25, // optional, default 25, max 100"search": "summer", // optional — substring match on campaign name"campaign_id": "6a6ba7b231e1010f55846996", // optional — exact match on one campaign"status": "active", // optional, "active" | "stopped" | "archived""message_type": "email", // optional, "email" | "target_collab_email""sort_by": "created_at", // optional — see sortable fields below"sort_direction": "desc", // optional, "asc" | "desc""include_body": false // optional — include the full HTML email_body}
Parameters:
status: Filter by status. Without it, everything except archived is returned (matching the dashboard's default view).message_type:email= plain email campaign;target_collab_email= TC + Email campaign (creatable only in the dashboard).include_body: Whentrue, each row also carries the full HTMLemail_body(up to 2 MB per campaign — request narrow pages). Every row always includes a tag-stripped 300-charbody_preview.
Response 200
{"data": {"results": [{"campaign_id": "6a6ba7b231e1010f55846996","shop_id": "695ff965c185d97c5b1b8d10","campaign_name": "Summer email outreach","status": "active","message_type": "email","subject": "Partner with us, [affiliate_name]?","body_preview": "Hi [affiliate_name], we love your content and would like to...","cc_emails": ["manager@brand.com"],"sender_emails": ["outreach@surgifycreators.com"],"daily_limit": 120,"messages_sent_today": 37,"outreach_audience": "new_affiliates","outreach_filters": { "categories": ["Beauty & Personal Care"] },"lists": [],"group_id": null,"send_to_all": true,"filter_by_entry_date": false,"entry_date_threshold": null,"exclude_groups": [],"exclude_list_ids": [],"messages_sent": 412,"messages_failed": 3,"email_views": 188,"messages_remaining": 812033,"send_error": null,"created_at": "2026-08-10 14:22:37.554000","last_updated_at": "2026-08-14 09:12:44.183000"}],"page": 1,"page_size": 25,"total_count": 4,"has_more": false}}
Response Fields:
sender_emails: The linked addresses this campaign sends from, resolved to email addresses.send_error: Set (with the raw error) when sending is genuinely broken on the campaign's SMTP/custom-domain sender and the campaign was paused;nullotherwise. One-off failures appear only in List Activity Logs.email_views: Opens recorded via the tracking pixel.email_body: Full HTML body — present only wheninclude_body: true.
Email Campaigns - Update Email Campaign
https://api.cruva.com/emails/campaigns/updatePartial update of an existing email campaign: only the fields present in the request change; everything else keeps its stored value. Same field semantics and validation as Create Email Campaign, and the merged result is re-validated as a whole — e.g. switching the audience to groups fails unless a group_id is on file or provided. The campaign's type (plain email vs TC + Email) cannot be changed. Optionally idempotent — see the Idempotency section.
Request Body
{"campaign_id": "6a6ba7b231e1010f55846996", // required"title": "Summer email outreach v2", // optional — all other fields optional too"subject": "Quick question, [affiliate_name]","email_body": "<p>Hi [affiliate_name], ...</p>","sender_emails": ["outreach@surgifycreators.com"], // REPLACES the whole sender list"cc_emails": [], // REPLACES the whole CC list ([] clears)"daily_limit": 100,"outreach_audience": "list","list_ids": [1421],"status": "active" // optionally start/stop in the same call}
Parameters:
campaign_id: The email campaign to edit (from/emails/campaigns/list).sender_emails / cc_emails: Replace the entire list when provided. Sender addresses must be linked to the shop; if the new sender count lowers the 150-per-sender ceiling below the storeddaily_limit, the limit is clamped automatically (or passdaily_limitexplicitly).status: Optionally also setactive/stoppedin the same call.
Side effect: changing any audience field (outreach_audience, outreach_filters, list_ids) re-runs the audience-size estimator (fail-soft) and refreshes messages_remaining.
Response 200
{"data": {"message": "Email campaign updated.","campaign_id": "6a6ba7b231e1010f55846996","status": "active","messages_remaining": 4188}}
Error Responses
// 400 — merged result would be invalid{ "error": "campaign would have no group_id (required for 'groups' audience)" }// 404 — campaign doesn't exist on this shop{ "error": "Email campaign not found" }
Email Campaigns - Start / Stop
https://api.cruva.com/emails/campaigns/toggleStart (active) or stop (stopped) an email campaign. Both directions also clear any stale error/pause flags (send errors, permission issues, daily-cap holds), matching the dashboard's start/stop behavior — so a campaign paused by a fixed sender problem restarts cleanly.
Request Body
{"campaign_id": "6a6ba7b231e1010f55846996", // required"status": "active" // required, "active" | "stopped"}
Response 200
{"data": { "message": "Email campaign set to active" }}
Email Campaigns - Delete Email Campaign
https://api.cruva.com/emails/campaigns/deletePermanently delete an email campaign. This cannot be undone — to keep the campaign's stats visible in the dashboard, stop it (or archive it in the dashboard) instead. Its historical send rows remain queryable in List Activity Logs. Optionally idempotent — see the Idempotency section.
Request Body
{"campaign_id": "6a6ba7b231e1010f55846996" // required}
Response 200
{"data": { "message": "Email campaign deleted." }}
Creator Filters Reference
The outreach_filters object on /automations/create (audience new_affiliates) selects creators from the platform-wide creator index. All fields are optional and combine with AND; array fields match OR within the field.
Numeric range fields (send either or both bounds):
follower_min / follower_max — follower count.
min_gmv / max_gmv — the creator's platform-wide affiliate GMV over the last 30 days, across all brands. This is the creator-tier basis (L1-L7), not their GMV with your shop.
min_live_gmv / max_live_gmv — LIVE-stream GMV over the last 30 days.
engagement / max_engagement — video engagement rate in percent (5 = 5%). engagement is the lower bound.
avg_view_cnt / max_avg_view_cnt — average video views. avg_view_cnt is the lower bound.
post_rate — minimum posting rate, percent.
brand_collaborations / max_brand_collaborations — number of brand collaborations.
min_pps / max_pps — promotion performance score, 0-5.
min_content_quality / max_content_quality — content quality score, 0-100.
is_fast_growing — boolean.
Value-list fields — expand each reference below for the exact accepted strings. Values are matched case-insensitively for the demographic/appearance fields.
Caps: the audience estimator silently drops categories past 5 entries, and units_sold / follower_ages past 2 entries — stay within those.
Deprecated: the legacy gmv_ranges and live_gmv bucket arrays still exist on old automations but should never be sent — always use the numeric min_gmv/max_gmv and min_live_gmv/max_live_gmv bounds.
Automations - Update Automation
https://api.cruva.com/automations/updatePartial update of an existing automation: only the fields present in the request change — everything else keeps its stored value. Accepts the same fields as Create Automation (same shapes, same validation), plus campaign_id. The merged result is validated as a whole, so an update can never leave an automation in a state Create would have rejected. Optionally idempotent — see the Idempotency section.
Request Body
{"campaign_id": "7c3a91e4d2f8b56091ad473e", // required — from /automations/list"title": "Summer outreach v2", // any create field may be included…"outreach_filters": { "min_gmv": 25000 }, // …only what you send changes"invite_details": { // partial too — only these subkeys change"contact_email": "partnerships@example.com"},"time_limits": {}, // {} or null CLEARS the send window"status": "stopped" // optionally set status in the same call}
Parameters:
campaign_id: The automation to edit. Obtain from/automations/list.…any create field: See Create Automation for every field and its rules. Omitted fields are untouched.invite_details: Merged partially: pass only the subkeys to change (e.g. justcontact_email).productsreplaces the whole product list when provided (each entry re-validated). Passexpire_dateto switch to fixed-date expiry, orexpire_time+expire_grainfor relative.dm_messages: Replaces the entire message sequence when provided, re-validated against the finalmessage_type—invite+dmrequires[0]= invite_card and[1]= plain message, same as create. Max 5 items — TikTok blocks senders after 5 unanswered messages.time_limits / daily_message_limits: Pass{}(ornull) to clear the send window or the daily caps; pass a full object to change them.status: Optional.activeorstopped— saves a separate/automations/togglecall.
Whole-state validation: switching message_type to invite/invite+dm fails with 400 unless complete invite details are on file or provided in the same call; switching to dm fails while the stored steps still contain an invite_card (send replacement dm_messages); switching outreach_audience to groups/list requires the matching group_id/list_ids.
Side effect: when the audience or its filters change, the audience-size estimator re-runs (fail-soft) and messages_remaining is returned.
Response
{"data": {"message": "Automation updated.","campaign_id": "7c3a91e4d2f8b56091ad473e","status": "stopped","messages_remaining": 184203}}
Error Responses
// 400 — validation error on the merged result (the field is named){ "error": "automation would be missing invite_details field(s): contact_email" }// 404 — not found, or not owned by this shop{ "error": "Automation not found" }
Response Fields:
messages_remaining: Present only when the audience changed: the re-estimated audience size (nullif the estimator failed soft).
Automations - Toggle Automation
https://api.cruva.com/automations/toggleStart or stop an existing automation by id.
Request Body
{"campaign_id": "7c3a91e4d2f8b56091ad473e","status": "active"}
Parameters:
campaign_id: The automation to update. Obtain from/automations/list.status: Eitheractive(starts the automation) orstopped(pauses it).
Response
{"data": {"message": "Automation set to active"}}
Error Responses
// 400 — missing/invalid campaign_id, or status not in {active, stopped}{ "error": "status must be one of: active, stopped" }// 404 — automation not found, or not owned by this shop{ "error": "Automation not found" }
Automations - Delete Automation
https://api.cruva.com/automations/deleteHard-delete an automation. The record is removed from the database entirely — there is no archive, and the deletion frees a slot against the 1000-bot per-shop limit. The body is parsed with request.get_json(silent=True) so a missing or unparseable body returns a clean 400 instead of 415. Optionally idempotent — see the Idempotency section.
Request Body
{"campaign_id": "6a14aa4f8c93e2105d7bf830"}
Parameters:
campaign_id: The automation to delete. Obtain from/automations/list.
Response
{"data": {"message": "Automation deleted."}}
Error Responses
// 400 — missing or malformed campaign_id (also returned when the body is missing/unparseable){ "error": "Missing campaign_id" }// 404 — not found, or not owned by this shop{ "error": "Automation not found" }
Automations - List Categories
https://api.cruva.com/automations/categories/listReturn the static list of product categories used by outreach-filter UIs. The values returned here are the exact strings accepted by outreach_filters.categories on /automations/create.
No request body required. Only the x-api-key header is required — x-shop-id is not needed for this endpoint since the list is global.
Response
{"data": ["Home Supplies","Kitchenware","Textiles & Soft Furnishings","Household Appliances","Womenswear & Underwear","Menswear & Underwear","Shoes","Health","Beauty & Personal Care","Phones & Electronics","Computers & Office Equipment","Pet Supplies","Sports & Outdoor","Toys & Hobbies","Furniture","Tools & Hardware","Home Improvement","Automotive & Motorcycle","Fashion Accessories","Food & Beverages","Books, Magazines & Audio","Luggage & Bags","Collectibles","Jewelry Accessories & Derivatives"]}
Groups
Groups are saved CRM segments — a named, reusable filter that resolves to a set of creators. Reference a group from an automation by setting outreach_audience: "groups" and passing the group's group_id on /automations/create.
Groups - List Groups
https://api.cruva.com/groups/listList the CRM groups configured for the shop, paginated and searchable by name.
Request Body
{"page": 1,"page_size": 25,"search": "vip"}
Parameters:
page: Optional. Page to fetch. Defaults to1.page_size: Optional. Number of results per page. Defaults to25, max100.search: Optional. Case-insensitive substring match against the group title.
Response
{"data": {"results": [{"group_id": "6a15c8b29f4d1e7350a8c742","name": "VIP creators","creator_count": 432,"filters": { "min_followers": 50000, "language": "english" },"created_at": "2026-05-25 14:02:11.000000","last_updated_at": "2026-05-25 14:02:11.000000"}],"page": 1,"page_size": 25,"total_count": 12,"has_more": false}}
Response Fields:
group_id: Identifier to pass to/automations/create(withoutreach_audience: "groups") or to/groups/delete.creator_count: Cached affiliate count for the group's filter. May be0if the group has no filters or if the count-resolution step previously failed soft.filters: The filter payload that defines this group's audience.
Groups - Create Group
https://api.cruva.com/groups/createCreate a CRM group from a filter payload. Optionally idempotent — see the Idempotency section.
Request Body
{"title": "VIP creators","filters": {"min_gmv": 1000, // GMV driven for THIS shop"max_gmv": 100000,"min_videos": 1, // videos posted for this shop"min_days": 0, // days since their last post for you"max_days": 30,"products": ["1800291847362058192"], // engaged with any of these"statuses": ["To Review", "Ready to Ship", "Shipped"],"replied": true,"min_followers": 10000, // platform-wide stats"language": ["english"]}}
Parameters:
title: Display name for the group. Required, non-empty after trim.filters: Filter object — all fields optional and AND-combined; array fields OR-match within the field. Most fields are scoped to this shop (the creator's relationship with *your* store); the platform-wide exceptions are called out below. The dashboard requires at least one filter — an unfiltered group resolves to the empty set until edited.Performance with this shop: Inclusive min/max bounds, all shop-scoped:min_gmv/max_gmv(GMV driven for you),min_units_sold/max_units_sold,min_videos/max_videos(videos about your products),min_commission/max_commission(their commission rate with you),min_live_gmv/max_live_gmv(LIVE GMV for you),min_live_count/max_live_count(number of LIVEs).Recency (days):min_days/max_days— days since the creator's last post for this shop.min_days_live/max_days_live— days since their last LIVE.min_days_messaged/max_days_messaged— days since you last messaged them.min_days_replied/max_days_replied— days since they last replied.min_sample_received_days/max_sample_received_days— days since a sample was received.min_content_unfulfilled_days/max_content_unfulfilled_days— days a received sample has gone without posted content.Products & samples:products— engaged with ANY of these product ids (posted, sampled, or sold); setproducts_and: trueto require ALL,products_exclusive: truefor ONLY these.exclude_products— drop creators who engaged with these.posted_products— actually posted about these.sampled_skus— received these as samples.statuses— sample-request statuses (expand below).refundable_sample_products— product ids the creator received as refundable samples (creator buys and is refunded after posting).refundable_sample_skus— SKU ids received as refundable samples.refundable_sample_statuses— refundable-sample statuses (expand below).sample_source— how the sample was requested (expand below).showcasing— boolean, currently showcasing your products.Outreach history:messaged_by/messaged_not_by— arrays ofcampaign_ids from /automations/list: creators messaged (or not) by those campaigns;messaged_and: truerequires ALL,messaged_exclusive: truemeans ONLY those.replied— boolean, has ever replied to your outreach.previously_worked_with— boolean.contains_email/contains_phone— boolean, contact info on file.include_tags/exclude_tags— your CRM tag strings.handle— substring match on handle/nickname.Platform-wide creator stats: NOT shop-scoped:min_followers/max_followers,min_engagement/max_engagement(percent),min_post_rate/max_post_rate(percent),min_med_gmv_revenue/max_med_gmv_revenue(the creator's 30-day GMV across ALL brands — the L1-L7 tier basis), and demographic arraysgender,age,race,body_type,economic_status,tone,language— exact values in the Creator Filters Reference.
Side effect: when filters is non-empty, the server resolves the audience size (creator_count) before returning. This step fails soft — the group is created either way, and a creator_count of 0 may indicate either an empty result set or a resolver failure.
Limits: a shop may have at most 200 groups. Hitting the cap returns 403.
Response 201
{"data": {"message": "Group created.","group_id": "6a15c8b29f4d1e7350a8c742","creator_count": 432}}
Error Responses
// 400 — title missing/empty, or filters is not an object{ "error": "title is required" }// 403 — 200-group cap reached{ "error": "Group limit reached for this shop" }
Groups - Delete Group
https://api.cruva.com/groups/deleteHard-delete the group. The body is parsed with request.get_json(silent=True) so a missing or unparseable body returns a clean 400 instead of 415. Optionally idempotent — see the Idempotency section.
Note: this does not cascade — any automation that references this group via outreach_audience: "groups" retains its now-dangling group_id and will fail to send until you point it at a different group.
Request Body
{"group_id": "6a15c8b29f4d1e7350a8c742"}
Parameters:
group_id: The group to delete. Obtain from/groups/list.
Response
{"data": {"message": "Group deleted."}}
Error Responses
// 400 — missing or malformed group_id (also returned when the body is missing/unparseable){ "error": "Missing group_id" }// 404 — not found, or not owned by this shop{ "error": "Group not found" }
Lists
Lists are named sets of creator handles — the audiences behind outreach_audience: "list" automations. Unlike Groups (which are saved *filters* that re-resolve dynamically), a list is a fixed roster: exactly the creators you put in it.
A list is identified by its title within your shop — there is no separate list id. Creating into an existing title appends; re-adding a handle already in the list is a silent no-op.
Handles are normalized and verified. Every handle is trimmed, stripped of a leading @, lowercased, and validated against [a-z0-9._] (max 30 chars). Handles are then resolved against Cruva's creator index — handles that don't match a known TikTok Shop creator are skipped and reported back in unmatched (they couldn't be messaged anyway).
Every list-creating endpoint caps at 10,000 creators per call and reports added_count, so you always know exactly what was written.
Lists - Create List
https://api.cruva.com/lists/createCreate a list (or append to an existing one) from an array of creator handles. Handles are normalized, resolved against the creator index, and stored with their resolved creator id. Optionally idempotent — see the Idempotency section.
Request Body
{"list_id": 1421, // append to an existing list…"title": "Summer Creators", // …or name a new one (max 255 chars)"handles": [ // required, 1-10,000 handles, @ optional"@creatorone","creator.two","not_a_real_handle"],"include_unmatched": false // optional, default false}
Parameters:
title: Name for a new list, unique per shop. Pass this to create a list; posting to a title the shop already has appends to it. The response returns thelist_id.list_id: Append to this existing list. Pass eitherlist_idortitle.handles: 1 to 10,000 TikTok handles per call, with or without a leading@. Normalized to lowercase; entries failing[a-z0-9._]{1,30}are dropped and reported ininvalid.include_unmatched: Defaultfalse: handles that don't resolve to a known creator are skipped (reported inunmatched). Settrueto store them anyway — note automations can only message known creators.
Success check: added_count is the number of rows actually written. 0 with a 2xx means every resolved handle was already in the list. A 400 with unmatched means nothing resolved at all.
Response 201
{"data": {"message": "List created.","list_id": 1421,"title": "Summer Creators","submitted_count": 3,"resolved_count": 2,"added_count": 2,"unmatched": ["not_a_real_handle"],"invalid": []}}
Error Responses
// 400 — neither list_id nor title, empty handles, over the 10,000 cap,// or none of the handles matched a known creator{ "error": "None of the handles matched a known creator. Pass include_unmatched=true to store them anyway.", "unmatched": ["..."] }
Response Fields:
list_id: The list written to — pass this to the other list endpoints and to an automation'slist_ids.submitted_count: Handles received in the request.resolved_count: Handles that matched a known creator.added_count: Rows actually written (excludes duplicates already in the list).unmatched: Valid handles that didn't match any known creator — skipped unlessinclude_unmatched.invalid: Entries dropped by normalization (illegal characters or over 30 chars).
Lists - List Lists
https://api.cruva.com/lists/listBrowse and search every list on the shop, with live member counts. Pass title to resolve one exact name to its list_id. Each list reports how many of its members can be DM'd (affiliate_count) and how many can be emailed (email_count), so the same list can drive an automation and an email campaign.
Request Body
{"page": 1,"page_size": 25,"title": "Summer Creators","search": "summer","sort_by": "created_at","sort_direction": "desc"}
Parameters:
page: Optional. Page to fetch. Defaults to1.page_size: Optional. Results per page. Defaults to25, max100.title: Optional. Exact list name, case-insensitive — this is how you resolve a name to thelist_idthat every other list endpoint and an automation'slist_idstake. Titles are unique per shop, so a match returns exactly one list.search: Optional. Case-insensitive substring match against the list title, for browsing. Combine withtitleor use on its own.sort_by: Optional. One ofcreated_at(default),title,affiliate_count, oremail_count.sort_direction: Optional.ascordesc(default).
Response
{"data": {"results": [{"list_id": 1421,"title": "Summer Creators","affiliate_count": 1975,"email_count": 1204,"created_at": "2026-07-01 21:16:30.559923"}],"page": 1,"page_size": 25,"total_count": 12,"has_more": false}}
Response Fields:
list_id: The list's identifier — pass to/lists/get,/lists/delete,/lists/rename,/lists/remove, or an automation'slist_ids.title: The list's name, as shown in the dashboard. Editable viaPOST /lists/rename.affiliate_count: Distinct members carrying a TikTok handle: the audience a DM or invite campaign can reach.email_count: Distinct members carrying an email address: the audience an email campaign can reach.
Lists - Get List Members
https://api.cruva.com/lists/getFetch a list's members. Creator stats (nickname, followers, platform GMV, engagement) are joined live from the creator index at read time — they are never stale copies.
Request Body
{"list_id": 1421,"identity": "handle","page": 1,"page_size": 25,"sort_by": "gmv","sort_direction": "desc"}
Parameters:
list_id: The list's id.404when the list does not exist or belongs to another shop.identity: Optional. Narrows to the members a channel can reach:handlefor DM/invite outreach,emailfor email campaigns. Defaults to every member.page / page_size: Optional. Defaults1/25, max page size100.sort_by: Optional. One ofaffiliate(handle),email,followers,gmv,engagement, orcreated_at(default).sort_direction: Optional.ascordesc(default).
Response
{"data": {"list_id": 1421,"title": "Summer Creators","identity": "handle","results": [{"handle": "strangefruitugc","creator_oecuid": "7494855447334848677","messaged": false,"created_at": "2026-07-01 21:16:30.559923","nickname": "Strange Fruit","email": "creator@example.com","category": "[\"Beauty & Personal Care\"]","followers": 48200,"affiliate_gmv": 31250.5,"engagement": 512}],"page": 1,"page_size": 25,"total_count": 1975,"has_more": true}}
Response Fields:
list_id: The list's id — the identifier every other list endpoint takes.handle / email: A member carries both where they are known, so one list can serve DM outreach and an email campaign.messaged: Alwaysfalse. Kept for payload stability.affiliate_gmv: The creator's platform-wide 30-day affiliate GMV.engagement: Video engagement rate as percent × 100 (512= 5.12%).
Lists - Rename List
https://api.cruva.com/lists/renameRename a list. Automations reference lists by list_id, so a rename does not affect any campaign targeting the list.
Request Body
{"list_id": 1421,"new_title": "Summer Creators 2026"}
Parameters:
list_id: The list to rename.404when the list does not exist or belongs to another shop.new_title: Required. Max 255 chars, and must not collide with another of the shop's lists.
Response
{"data": {"message": "List renamed.","list_id": 1421,"title": "Summer Creators 2026"}}
Error Responses
// 404 — unknown list_id, or a list belonging to another shop{ "error": "List not found" }// 409 — a list named new_title already exists{ "error": "A list with that title already exists" }
Lists - Merge Lists
https://api.cruva.com/lists/mergeUnion two or more lists into a new list. The source lists are kept intact, and every member is copied with all of its identities — handle, email and creator id. Merging into a title that already exists is rejected with 409. Optionally idempotent — see the Idempotency section.
Request Body
{"title": "All Summer Creators","source_list_ids": [1421, 1508]}
Parameters:
title: Name for the new merged list — must not already exist.source_list_ids: 2+list_idvalues to union. Must be unique, and must belong to your shop.
Response 201
{"data": {"message": "Lists merged.","list_id": 1602,"title": "All Summer Creators","merged_count": 2612}}
Error Responses
// 400 — fewer than 2 sources, duplicate sources, or non-integer ids{ "error": "source_list_ids must contain at least 2 lists" }// 404 — none of the source lists exist (or all are empty){ "error": "Source lists not found or empty" }// 409 — target title already exists{ "error": "A list with that title already exists" }
Response Fields:
merged_count: Distinct creators written to the new list (duplicates across sources collapse to one).
Lists - Remove Entries
https://api.cruva.com/lists/removeRemove specific handles (or email addresses) from a list without deleting the list itself.
Request Body
{"list_id": 1421,"identity": "handle","handles": ["@creatorone", "creator.two"]}
Parameters:
list_id: The list to edit.identity: Optional. Which column the entries match:handle(default, passhandles) oremail(passemails).handles / emails: Entries to remove. Handles are normalized the same way as on create, so@CreatorOnematchescreatorone.
Response
{"data": {"message": "Entries removed.","removed_count": 2}}
Lists - Delete List
https://api.cruva.com/lists/deleteHard-delete a list and every member in it. Refused with 400 while any non-archived campaign still targets the list — repoint or delete the automation first, so a sender never wakes up to a vanished audience. The body is parsed with request.get_json(silent=True) so a missing or unparseable body returns a clean 400 instead of 415. Optionally idempotent — see the Idempotency section.
Request Body
{"list_id": 1421}
Parameters:
list_id: The list to delete. Obtain from/lists/listor/lists/create.
Response
{"data": {"message": "List deleted.","removed_count": 1975}}
Error Responses
// 400 — the list is still referenced by a campaign{ "error": "Cannot delete list: it is in use by a campaign." }// 404 — unknown list_id, or a list belonging to another shop{ "error": "List not found" }
Tags
Tags are free-text labels on creators in your CRM — the same chips you see on the CRM table, and what include_tags / exclude_tags filter on in /affiliate/crm/list. There is no separate tag object: tagging a creator with a new name creates the tag, and a tag with no creators left disappears. Creators are addressed by TikTok handle and must already be in the shop's CRM. Tag names are 1–60 characters, case preserved (VIP and vip are different tags). Dynamic tags configured in the dashboard are recomputed from a saved group on every CRM refresh and will drop creators added by hand under the same name.
Workflows
Workflows are the multi-step creator journeys built in the dashboard's Workflows builder: a trigger (a creator-matching filter, or a recurring schedule), then a graph of steps — DMs, TikTok invites, emails, waits, conditions, tags, lists. The API lets you read, create, pause/activate and delete workflows by workflow_id; editing an existing step graph is done in the builder, which re-points creators already inside it. Workflows are a Scale-plan feature: reads work on any plan, but toggle returns 403 below Scale.
Workflows - List Workflows
https://api.cruva.com/workflows/listList the shop's workflows (deleted ones excluded), newest edited first.
Request Body
{"page": 1,"page_size": 25,"search": "onboarding" // optional, case-insensitive match on the name}
Parameters:
page: Optional. Defaults to1.page_size: Optional. Defaults to25, max100.search: Optional. Case-insensitive substring match against the workflow name.
Response
{"data": {"results": [{"workflow_id": "64b1a2c3d4e5f60718293a4b","name": "Welcome new creators","description": "onboarding flow for new creators","status": "active", // active | paused"trigger_type": "creator_matches", // creator_matches | recurring"step_count": 4,"triggered": 1284, // creators that have entered, all time"template_id": null,"user_touched": true,"updated_at": "2026-08-26T12:24:27Z","last_sync_at": "2026-08-27T14:07:07Z"}],"total_count": 1,"has_more": false}}
Workflows - Get Workflow
https://api.cruva.com/workflows/getOne workflow with its trigger and full step graph. Each step carries its authored config, its next pointer, branches (onTrue / onFalse, used by conditions), and triggered — how many creators have reached it.
Request Body
{"workflow_id": "64b1a2c3d4e5f60718293a4b"}
Parameters:
workflow_id: From/workflows/list.
Response
{"data": {"workflow_id": "64b1a2c3d4e5f60718293a4b","name": "Welcome new creators","status": "active","trigger_type": "creator_matches","step_count": 4,"triggered": 1284,"trigger": {"type": "creator_matches","config": { "match": "AND", "conditions": [] },"next": "s1"},"steps": [{"id": "s1","type": "send_dm", // send_dm | send_tc_invite | send_tc_dm | send_email | add_to_list | tag_creator | wait | wait_until | condition | exit"config": { "messageItems": [{ "type": "message", "content": "Hi [affiliate_name]! ..." }] },"next": "s2","branches": { "onTrue": null, "onFalse": null },"triggered": 1284},{"id": "s2","type": "wait","config": { "amount": 3, "unit": "days" },"next": "s3","branches": { "onTrue": null, "onFalse": null },"triggered": 433}]}}
Workflows - Create Workflow
https://api.cruva.com/workflows/createCreate a workflow from a trigger and a step graph — the same shape /workflows/get returns, so duplicating a workflow is a get followed by a create with a new name. The graph is validated exactly as the dashboard builder does (known step types, every next / branch pointer resolves to a step, no cycles, at most 200 steps); a shop can hold 100 live workflows. New workflows start paused with all counters at zero. Requires the Scale plan (403 otherwise).
Request Body
{"name": "Sample to First Sale (copy)","description": "optional, up to 500 characters","status": "paused", // optional; active | paused (default)"trigger": {"type": "creator_matches", // creator_matches | recurring"config": { "match": "AND", "conditions": [] },"next": "s1"},"steps": [{ "id": "s1", "type": "send_dm", "config": { "messageItems": [{ "type": "message", "content": "Hi [affiliate_name]!" }] }, "next": "s2", "branches": { "onTrue": null, "onFalse": null } },{ "id": "s2", "type": "wait", "config": { "amount": 3, "unit": "days" }, "next": null, "branches": { "onTrue": null, "onFalse": null } }]}
Parameters:
name: Display name, up to 120 characters.description: Optional, up to 500 characters.status: Optional.paused(default) oractive. An active workflow starts messaging creators on the next hourly tick — create paused and activate after reviewing it in the dashboard.trigger:{type, config, next}.nextis the id of the first step (ornullfor an empty workflow). Forcreator_matches,configis a condition group — see Condition rules below.steps: Array of{id, type, config, next, branches}. Ids only need to be unique within this workflow, so a graph copied from/workflows/getcan be passed as-is;triggeredcounters on copied steps are ignored and reset to 0.
Response
{"data": {"workflow_id": "64b1a2c3d4e5f60718293a4c","name": "Sample to First Sale (copy)","status": "paused","trigger_type": "creator_matches","step_count": 2,"triggered": 0,"template_id": null,"user_touched": false,"updated_at": "2026-08-27T21:10:03Z","last_sync_at": null}}
Response
{ "error": "Step s2 points at a missing step" }
Workflows - Activate / Pause
https://api.cruva.com/workflows/toggleSet a workflow to active or paused. Pausing stops new sends; creators already inside keep their place and resume when it is reactivated. Requires the Scale plan (403 otherwise).
Request Body
{"workflow_id": "64b1a2c3d4e5f60718293a4b","status": "paused"}
Parameters:
workflow_id: From/workflows/list.status:activeorpaused.
Response
{"data": { "message": "Workflow set to paused" }}
Workflows - Delete Workflow
https://api.cruva.com/workflows/deleteDelete a workflow. It is paused and hidden immediately; queued sends are dropped and creators inside it are released during the next executor pass. Same behaviour as deleting from the dashboard.
Request Body
{"workflow_id": "64b1a2c3d4e5f60718293a4b"}
Parameters:
workflow_id: From/workflows/list.404if already deleted.
Response
{"data": { "message": "Workflow deleted." }}
Creator Briefs
Creator briefs are shareable landing pages you send to creators: example videos to emulate, proven opening hooks, do/don't guidelines, an optional info PDF, and links to your community hub and campaigns. Every brief gets a short public link — cruva.com/fw?id=<code>.
Using a brief in an automation is just a link. Paste the brief's link into the automation's invite_details.message or a dm_messages item's content on /automations/create — e.g. "Hi [affiliate_name]! Here's everything you need to get started: cruva.com/fw?id=ab12cd".
Two brief types:
Static — a fixed page. With video_mode: "manual" you pick the exact videos; with "auto" the page features the shop's current top videos per your sourcing rules.
Dynamic — the page generates itself per product: creators pick one of your products and see its top videos and hooks live, always fresh.
Requires the Growth plan; max 50 briefs per shop. Branding (logo, banner color) is shop-level and managed in the dashboard.
Creator Briefs - Create Brief
https://api.cruva.com/creator-briefs/createCreate a creator brief and get back its shareable link. Optionally idempotent — see the Idempotency section.
Request Body
{"name": "Glow Serum launch brief", // required, internal name"headline": "Post like our top sellers", // required, shown to creators"brief_type": "static", // "static" | "dynamic""video_mode": "manual", // static only: "manual" | "auto""video_urls": [ // required for static + manual"https://www.tiktok.com/@creator/video/7350000000000000000"],"dynamic_content": { // sourcing rules for auto/dynamic"all_products": true,"products": [],"video_count": 5,"sort_by": "gmv","default_time_range": "30d","linked_handles_mode": null},"top_hooks": ["Wait until you see the results..."],"guidelines": [{ "type": "do", "description": "Show the product clearly within the first 3 seconds" },{ "type": "dont", "description": "Avoid false claims" }],"support_email": "creators@example.com","text_box_title": "Payment terms","text_box_body": "Commission is paid out weekly...","show_hub_link": false,"show_campaigns": false,"section_order": ["videos", "hooks", "guidelines", "community", "pdf", "textbox"]}
Parameters:
name / headline: Both required.nameis the internal label;headlineis the page title creators see.brief_type:static(fixed page, default) ordynamic(per-product live content — creators pick a product and see its top videos/hooks, always current).video_mode: Static briefs only:manual(you providevideo_urls) orauto(the page features the shop's top videos perdynamic_content). Dynamic briefs are always auto.video_urls: TikTok video URLs to feature. Required (≥1) for static + manual; ignored for auto/dynamic.dynamic_content: Video-sourcing rules for auto/dynamic briefs:all_products(defaulttrue) or aproductsid array (required for dynamic when `all_products` is false);video_count1-50 (default 5);sort_by;past_months(default 12) /all_time;default_time_range;linked_handles_mode— expand the references below.top_hooks: Opening-hook suggestions shown to creators.guidelines: Array of{ type, description }do/don't rules — expand Guideline types below.support_email: Contact email shown on the brief (validated).text_box_title / text_box_body: Optional free-text section on the page.show_hub_link / show_campaigns: Toggle the community-hub button and active-campaign cards.section_order: Order of the page sections — expand Page sections below.
Limits: max 50 briefs per shop (403 past the cap); requires the Growth plan (403 otherwise). The info-PDF upload is dashboard-only; pdf_button_text / pdf_button_color may still be set via the API.
Response 201
{"data": {"message": "Creator brief created.","brief_id": "6a1f00c89b3e21504d7ab911","url_code": "ab12cd","link": "https://cruva.com/fw?id=ab12cd"}}
Error Responses
// 400 — validation error (the specific field is named){ "error": "video_urls must contain at least one video for a manual brief — or use video_mode "auto" / brief_type "dynamic"" }// 403 — plan ineligible, or 50-brief cap reached{ "error": "Creator briefs require the Growth plan or higher." }
Response Fields:
link: The brief's public URL — paste this into an automation's invite message or DM content to send it to creators.url_code: The 6-character identifier inside the link. Stable for the brief's lifetime.
Creator Briefs - Update Brief
https://api.cruva.com/creator-briefs/updatePartial update — only the fields present in the request change. Accepts the same fields as Create Brief plus brief_id. The brief's link never changes, so automations already sending it keep working: the page simply updates in place. Optionally idempotent — see the Idempotency section.
Request Body
{"brief_id": "6a1f00c89b3e21504d7ab911", // required — from /creator-briefs/list"headline": "Updated: post like our best sellers","top_hooks": ["New hook that converts"]}
Parameters:
brief_id: The brief to edit. Obtain from/creator-briefs/list.…any create field: See Create Brief. Omitted fields are untouched. The merged result is re-validated — e.g. switching tovideo_mode: "manual"fails unless videos are on file or provided.
Response
{"data": {"message": "Creator brief updated.","brief_id": "6a1f00c89b3e21504d7ab911","url_code": "ab12cd","link": "https://cruva.com/fw?id=ab12cd"}}
Error Responses
// 400 — validation error on the merged result{ "error": "dynamic briefs need products: set dynamic_content.all_products to true or provide dynamic_content.products" }// 404 — not found, or not owned by this shop{ "error": "Creator brief not found" }
Creator Briefs - List Briefs
https://api.cruva.com/creator-briefs/listList the shop's creator briefs — including each brief's shareable link and view count. Pass brief_id to fetch a single brief's full configuration.
Request Body
{"page": 1,"page_size": 25,"search": "serum","brief_id": ""}
Parameters:
page / page_size: Optional. Defaults1/25, max page size100. Sorted by most recently updated.search: Optional. Case-insensitive substring match on the brief name.brief_id: Optional. Fetch a single brief.
Response
{"data": {"results": [{"brief_id": "6a1f00c89b3e21504d7ab911","name": "Glow Serum launch brief","headline": "Post like our top sellers","brief_type": "static","video_mode": "manual","link": "https://cruva.com/fw?id=ab12cd","url_code": "ab12cd","views": 412,"video_urls": ["https://www.tiktok.com/@creator/video/7350000000000000000"],"video_count": 1,"top_hooks": ["Wait until you see the results..."],"guidelines": [{ "type": "do", "description": "Show the product clearly within the first 3 seconds" }],"support_email": "creators@example.com","has_pdf": false,"dynamic_content": null,"show_hub_link": false,"show_campaigns": false,"section_order": ["videos", "hooks", "guidelines", "community", "pdf", "textbox"],"created_at": "2026-07-01 18:20:11.000000","last_updated_at": "2026-07-18 09:03:42.000000"}],"page": 1,"page_size": 25,"total_count": 6,"has_more": false}}
Response Fields:
link: The brief's public URL — drop it into an automation's invite message or DM content.views: How many times the brief page has been opened.video_count: Featured video count — the manual list's length, ordynamic_content.video_countfor auto/dynamic briefs.
Creator Briefs - Delete Brief
https://api.cruva.com/creator-briefs/deleteHard-delete a brief. Its public link stops resolving immediately — any automation message that already sent the link will point at a dead page, so update the automation copy first. The body is parsed with request.get_json(silent=True) so a missing or unparseable body returns a clean 400 instead of 415. Optionally idempotent — see the Idempotency section.
Request Body
{"brief_id": "6a1f00c89b3e21504d7ab911"}
Parameters:
brief_id: The brief to delete. Obtain from/creator-briefs/list.
Response
{"data": {"message": "Creator brief deleted."}}
Error Responses
// 400 — missing or malformed brief_id{ "error": "Missing brief_id" }// 404 — not found, or not owned by this shop{ "error": "Creator brief not found" }
Community
The community platform hosts creator contests and retainer deals on your public brand page at creators.cruva.com: races, contests, leaderboards, bingo boards, sweepstakes, and retainers. Every campaign gets a unique 6-character slug; its public share link is https://creators.cruva.com/<slug> — creators open that link to view and join the campaign.
The share link is designed to be dropped into outreach: create a campaign, take the share_link from the response, and reference it in an automation's DM or invite message (/automations/create) to invite creators at scale.
Campaigns created here appear in the dashboard (Community → Campaigns) and go live on the public page immediately when status is active. Requires the Growth plan or higher.
Community - List Campaigns
https://api.cruva.com/community/campaigns/listList the community campaigns configured for the shop, with their public share links. Paginated; filterable by title, lifecycle status, and campaign type.
Request Body
{"page": 1,"page_size": 25,"search": "","status": "active","campaign_type": "race"}
Parameters:
page: Optional. Page to fetch. Defaults to1.page_size: Optional. Number of results per page. Defaults to25, max100.search: Optional. Case-insensitive substring match against the campaign title.status: Optional. One ofactive,paused, orcompleted.campaign_type: Optional. One ofrace,contest,leaderboard,bingo,sweepstakes, orretainer.
Response
{"data": {"results": [{"campaign_id": "6a61ef6bcc6388012e941c8f","title": "Summer Views Race","campaign_type": "race","status": "active","url": "wpasve","share_link": "https://creators.cruva.com/wpasve","start_date": "2026-07-01T00:00:00+00:00","end_date": "2026-08-01T00:00:00+00:00","timezone": "America/New_York","payout_type": "cash","content_types": ["video"],"content_type": "video","progress_metric": "views","require_approval": false,"budget": 5000,"is_recurring": false,"creators_applied": 42,"creators_joined": 37,"total_gmv": 13397.42,"total_spent": 1200,"videos": 6154,"created_at": "2026-06-02T14:11:08.213000","last_updated_at": "2026-07-20T02:15:44.001000"}],"page": 1,"page_size": 25,"total_count": 11,"has_more": false}}
Response Fields:
campaign_id: Identifier to pass to every other/community/campaigns/*endpoint.share_link: The public join URL (https://creators.cruva.com/<url>). Put this in outreach automation messages to invite creators.require_approval:truemeans the campaign is invite-only — creators apply and wait inpendinguntil approved via/community/campaigns/participants/set_status.payout_type:cash,prize, orboth— underbotheach tier pays cash, awards a prize, or does both (mixed freely across tiers).content_types / content_type:content_typesis the multi-select source of truth (any combination ofvideo,LIVE,photo— photo = TikTok photo-mode slideshow).content_typeis the legacy single-value mirror; any combination collapses toboth(historically video + LIVE).total_gmv / total_spent / videos: Lifetime campaign totals (GMV generated, payouts recorded, videos posted).
Community - Get Campaign
https://api.cruva.com/community/campaigns/getFetch a single campaign's full configuration — everything stored on the campaign document (config, tiers, branding, products, guidelines, bingo/sweepstakes config) plus the assembled share_link.
Request Body
{"campaign_id": "6a61ef6bcc6388012e941c8f"}
Parameters:
campaign_id: The campaign to fetch. Obtain from/community/campaigns/list.
Response
{"data": {"campaign": {"campaign_id": "6a61ef6bcc6388012e941c8f","campaign_type": "race","status": "active","url": "wpasve","share_link": "https://creators.cruva.com/wpasve","config": {"title": "Summer Views Race","description": "Post videos, earn from the pool.","start_date": "2026-07-01T00:00:00+00:00","end_date": "2026-08-01T00:00:00+00:00","payout_type": "cash","payout_method": "stripe","require_approval": false,"content_types": ["video"],"content_type": "video","progress_metric": "views","grace_period": 3,"budget": 0,"discord": { "assign_roles": false, "roles": [], "channel_enabled": false, "dm_enabled": false },"chat": { "channel_enabled": true, "channel_posting_policy": "brand_only", "participant_status": "dm", "payouts": "dm", "winners": "dm" },"...": "..."},"tiers": [{ "level": 1, "payout": 5000, "rate_per_view": 0.003 }],"products": { "enable_all_shop_products": true, "product_list": [] },"reminders": { "enabled": true, "reminders": [{ "days_before": 3, "message": "..." }] },"guidelines": [],"creators_joined": 37,"total_gmv": 13397.42,"total_spent": 1200}}}
Community - Create Campaign
https://api.cruva.com/community/campaigns/createCreate a community campaign and get back its public share link. The campaign appears in the dashboard and goes live at the link immediately when status is active (the default). Optionally idempotent — see the Idempotency section.
To invite creators, put the returned share_link in an outreach automation message on /automations/create.
Request Body
{"title": "Summer Views Race", // required, unique per shop"description": "Post videos, earn from the pool.", // required, shown to creators"campaign_type": "race", // required — see formats below"payout_type": "cash", // "cash" | "prize" | "both"; optional — derived from tiers when omitted"start_date": "2026-07-01", // ISO date; omit for dynamic retainers"end_date": "2026-08-01", // required for fixed schedules"tiers": [ // required — shape per type, see below{ "level": 1, "payout": 5000, "rate_per_view": 0.003 }],"content_types": ["video"], // any combination of "video" | "LIVE" | "photo""progress_metric": "views", // "GMV" | "posting" | "views""timezone": "America/New_York","require_approval": false, // true = invite-only approval queue"budget": 5000, // cash-bearing contests only"hide_budget_card": false, // hide the spend figure from creators"auto_assign_tiers": true, // contests: false = brand assigns tiers manually"grace_period": 3, // days content still counts after end"usage_rights": "30-day usage rights on posted content","product_ids": ["1800291847362058192"], // default = all shop products"guidelines": [{ "type": "do", "description": "Show the product in use" }],"inspiration_links": ["https://www.tiktok.com/@creator/video/7300000000000000001"],"reminders": { // "N days left" nudges (SMS/email)"enabled": true,"reminders": [{ "days_before": 3, "message": "Only 3 days left, [name]!" }]},"discord_reminders": { // same, posted to a Discord channel"enabled": true,"channel_id": "118033...","channel_name": "campaigns","reminders": [{ "days_before": 1, "message": "Last day to post!" }]},"lark_reminders": { // same, posted to the shop's Lark group"enabled": true,"reminders": [{ "days_before": 1, "message": "Last day to post!" }]},"chat_reminders": { // same, sent as campaign-chat messages"enabled": true,"destination": "channel", // "channel" | "dm""reminders": [{ "days_before": 2, "message": "2 days left!" }]},"status": "active", // "active" (default) | "paused"// ── Discord community ───────────────────────────────"require_discord_join": false, // creator must be in the shop's Discord server to join"discord": { // onboarding applied once the creator is IN the campaign"assign_roles": true,"roles": [{ "role_id": "118044...", "role_name": "Creator" }],"channel_enabled": true,"channel_id": "118033...","channel_name": "campaign-chat","dm_enabled": true,"dm_message": "Welcome to [campaign_name]! Details: [campaign_link]"},// ── campaign chat ───────────────────────────────────"chat": { // omit entirely to leave campaign chat off"channel_enabled": true,"channel_posting_policy": "brand_only", // "open" | "brand_only""participant_status": "dm", // "dm" | "off" — approved/rejected cards"payouts": "dm", // "dm" | "off" — payment-sent cards"winners": "dm" // "dm" | "off" — winner cards on completion},// ── retainer scheduling ─────────────────────────────"start_date_type": "fixed", // "fixed" | "recurring" | "dynamic""is_recurring": false, // rolling periods (retainer/contest)"recurrence_period": "weekly", // required when is_recurring"max_periods": 8, // optional recurring cap"duration_days": 30, // dynamic only: per-creator period length"creator_custom_settings": [ // per-creator deals (retainer){ "creator_handle": "alice", "videos_required": 4, "payout_amount": 200 }],"negotiation_enabled": false, // creators may counter-offer"is_private": false, // allowlist-only visibility"private_allowed_handles": ["alice", "bob"],// ── type-specific configs ───────────────────────────"bingo": { "grid_size": 3, "cells": [{ "field_type": "total_gmv_above", "value_x": 100 } /* ×9 */ ] },"sweepstakes": { "point_rules": [{ "metric": "gmv", "points_per_unit": 1 }] },"config_extras": { "max_payout_per_creator": 500 } // advanced overrides — see below}
Parameters:
campaign_type: What kind of contest this is — expand contest formats below. Determines the requiredtiersshape (expand tiers shape by campaign_type).payout_type:"cash"(Stripe payouts),"prize"(physical prizes), or"both"— underboth, each tier pays cash, awards a prize, or does both, mixed freely across tiers. Optional: when omitted, contest/leaderboard/sweepstakes/bingo derive it from what the tiers carry (cash only →cash, prizes only →prize, any mix →both); races and retainers default tocash.tiers: Prize/payout ladder. The shape depends oncampaign_type— expand the table below. Cash tiers setpayout; prize tiers setprizes: [{ "title": "AirPods Pro", "image_path": "..." }]; underpayout_type: "both"a tier may carry either or both (a tier with neither is rejected). A tier can also set"reward_type": "cash" | "prize" | "both"to gate its own fields (cashdrops the prize rows,prizeclears the cash amount); input-only, never stored. Blank prize rows are dropped.content_types / progress_metric:content_typesis the kinds of shoppable post the campaign counts — any combination ofvideo,LIVE,photo(TikTok photo-mode slideshow); default["video"]. The legacy single-valuecontent_typeis still accepted (both= video + LIVE). Not every combo is valid per type — expand allowed content kinds & metrics below.progress_metricis ignored for sweepstakes (scored bypoint_rules) and defaults topostingfor retainers.require_approval: Whentruethe campaign is invite-only: creators apply into apendingqueue and must be approved via /community/campaigns/participants/set_status before they count.budget / hide_budget_card / grace_period:budgetis only enforced on cash-bearing contests (cash/both; stored as0for every other type — a race's pool lives intiers[0].payout).hide_budget_cardhides the spend figure from creators (the contest budget well / race prize-pool bar) — display only, the budget still caps funding; meaningful on races and budget-enforced cash contests.grace_period(default3) is how many days after the end date posted content still counts; forced to 0 on recurring campaigns (periods are contiguous calendar windows).auto_assign_tiers: Contests only (defaulttrue). Whenfalse, tiers are never assigned automatically — the brand sets each creator's tier manually from the dashboard's participants table.require_discord_join: Creators may only join once they've linked a Discord account that is a member of the shop's server — checked live at join time on the creators platform. Needs the shop's Discord integration connected.discord: Per-campaign Discord onboarding: what a creator's Discord account gets once they're actually in the campaign (applied at join when auto-approving, at approval time whenrequire_approvalis on; every action is best-effort and never blocks a join).assign_roles+roles: [{ "role_id", "role_name" }]grants guild roles;channel_enabled+channel_idopens a channel (member permission overwrite + link on the success screen);dm_enabled+dm_messagesends a bot DM supporting[shop_name],[campaign_name],[campaign_link]. Role/channel ids come from the dashboard's Community → Discord page.chat: Community-chat block:channel_enabledauto-creates the campaign chat channel,channel_posting_policy(open|brand_only) is applied when it's first created, andparticipant_status/payouts/winners(dm|off) route the lifecycle system cards. Omit the block entirely to leave campaign chat off (legacy behavior); when present, unset fields default to the dashboard's all-on defaults.start_date_type: Retainer schedule mode — expand retainer schedule modes below. Only persisted for retainers and contests; contests supportfixed/recurringonly.duration_days / creator_custom_settings / negotiation_enabled / is_private: Retainer-only.duration_days(dynamic, default30) is each creator's personal period length.creator_custom_settingssets per-creator deals — entries needcreator_handle(orcreator_handles: [...]to fan one deal out to several creators),videos_required, andpayout_amount; incomplete entries are dropped and handles are cleaned of a leading@.negotiation_enabledlets creators propose a custom payout at join time.is_privatehides rates and blocks joining except forprivate_allowed_handles(creators with a personalized deal are always allowed).reminders / discord_reminders / lark_reminders / chat_reminders: "N days before the end" nudges over four channels.remindersgoes out via SMS/email;discord_remindersposts to a channel and needschannel_id/channel_namefrom the shop's Discord integration;lark_remindersposts to the shop's connected Lark group;chat_reminderslands as campaign-chat system messages withdestination: "channel" | "dm". Each item needsdays_before(integer ≥ 1) andmessage(blank-message rows are dropped). Messages support variables — expand reminder message variables below.inspiration_links: Example TikTok video URLs shown in the join page's inspiration section. (The dashboard can also auto-fill this section from the shop's top content —inspiration_video_mode: "auto"+inspiration_auto_video_config.)bingo: Bingo only:{ "grid_size": 3 | 4, "cells": [...] }with exactlygrid_size²cells of{ "field_type", "value_x", "value_y"? }— expand bingo challenge types below.sweepstakes: Sweepstakes only:{ "point_rules": [{ "metric": "gmv" | "views" | "posts", "points_per_unit": 1 }] }— 1-3 rules, unique metrics.config_extras: Advanced config overrides merged into the campaign config (snake_case keys, converted server-side) — expand config_extras keys below.
Limits: community campaigns require the Growth plan or higher (403 otherwise), and a shop may have at most 100 campaigns (403 at the cap). A duplicate title within the shop returns 409.
Casing: the API accepts and returns snake_case everywhere; conversion to the stored document happens server-side.
Response 201
{"data": {"message": "Campaign created.","campaign_id": "6a61ef6bcc6388012e941c8f","url": "wpasve","share_link": "https://creators.cruva.com/wpasve","status": "active"}}
Error Responses
// 400 — validation error (the specific field is named in the message){ "error": "tiers[0].rate_per_view must be > 0 for views races" }{ "error": "tiers[1] must have a payout > 0, a named prize, or both" }// 403 — plan below Growth, or 100-campaign cap reached{ "error": "Community campaigns require the Growth plan or higher" }// 409 — duplicate title{ "error": "Campaign already exists, choose a different name." }
Response Fields:
share_link: The public join URL. Reference it in an automation DM/invite message to invite creators.
Community - Update Campaign
https://api.cruva.com/community/campaigns/updatePartially update a campaign — only the fields you send change. Optionally idempotent — see the Idempotency section.
Schedule lock: once any creator has joined a retainer or contest, its schedule anchor (start_date, start_date_type, is_recurring, recurrence_period, and end_date for recurring campaigns) is pinned — those fields are silently dropped from the update and the response carries schedule_locked: true. Auto-reactivation: moving end_date into the future on a completed campaign sets it back to active.
Request Body
{"campaign_id": "6a61ef6bcc6388012e941c8f", // required"title": "Summer Views Race v2","description": "Updated description","end_date": "2026-09-01","tiers": [{ "level": 1, "payout": 7500, "rate_per_view": 0.003 }],"budget": 7500,"hide_budget_card": true,"auto_assign_tiers": false, // contests: switch to manual tier assignment"require_approval": true,"require_discord_join": true,"content_types": ["video", "photo"], // replacement content kinds"usage_rights": "60-day usage rights","guidelines": [{ "type": "dont", "description": "No competitor products" }],"inspiration_links": ["https://www.tiktok.com/@creator/video/7300000000000000001"],"max_periods": 12,"duration_days": 45,"creator_custom_settings": [{ "creator_handle": "alice", "videos_required": 4, "payout_amount": 200 }],"private_allowed_handles": ["alice", "bob"],"discord": { // replacement Discord onboarding block"assign_roles": true,"roles": [{ "role_id": "118044...", "role_name": "Creator" }],"dm_enabled": true, "dm_message": "Welcome to [campaign_name]!"},"chat": { // replacement campaign-chat block"channel_enabled": true, "channel_posting_policy": "open","participant_status": "dm", "payouts": "dm", "winners": "dm"},"reminders": {"enabled": true,"reminders": [{ "days_before": 3, "message": "3 days left, [name]!" }]},"discord_reminders": {"enabled": true, "channel_id": "118033...", "channel_name": "campaigns","reminders": [{ "days_before": 1, "message": "Last day!" }]},"lark_reminders": {"enabled": true,"reminders": [{ "days_before": 1, "message": "Last day!" }]},"chat_reminders": {"enabled": true, "destination": "channel","reminders": [{ "days_before": 2, "message": "2 days left!" }]},"config_updates": { "winner_message": "You won! Check your email." }}
Parameters:
campaign_id: The campaign to update. Obtain from/community/campaigns/list.title: Optional. New title — must stay unique within the shop (409 on duplicate).tiers: Optional. Full replacement of the tier list (same per-type shape and validation as create). On contest/leaderboard/sweepstakes/bingo, replacing tiers also re-derives `payout_type` from what the new tiers carry (cash / prize / both), same as every dashboard save.content_types: Optional. Replacement content kinds — any combination ofvideo,LIVE,photo; same per-type rules as create. The legacycontent_typemirror is kept in lockstep automatically.hide_budget_card / auto_assign_tiers / require_discord_join: Optional toggles — same semantics as create: hide the spend figure from creators; contests' automatic tier assignment; the Discord-membership join gate.discord / chat: Optional. Full replacement of the per-campaign Discord onboarding block and the campaign-chat block — same shapes and validation as create.creator_custom_settings: Optional. Full replacement of the retainer per-creator deals (pass[]to clear). Same shape and cleanup rules as create.private_allowed_handles: Optional. Full replacement of a private retainer's allowlist.reminders / discord_reminders / lark_reminders / chat_reminders: Optional. Full replacement of a reminder channel's config — same shapes, validation, and message variables as create.bingo / sweepstakes: Optional. Full replacement of the type-specific config, re-validated. A drawn sweepstakeswinneris never overwritten from the API.config_updates: Optional. Arbitrary config-field overrides (snake_case keys, converted server-side), applied asconfig.<key>— same vocabulary as create'sconfig_extras.
Response
{"data": {"message": "Campaign updated.","campaign_id": "6a61ef6bcc6388012e941c8f"}}
Response when schedule fields were pinned
{"data": {"message": "Campaign updated.","campaign_id": "6a61ef6bcc6388012e941c8f","schedule_locked": true,"note": "Schedule fields were not changed: creators have already joined."}}
Community - Set Campaign Status
https://api.cruva.com/community/campaigns/set_statusSet a campaign's lifecycle status. paused hides it from joining; completed freezes it — required before leaderboard payouts are allowed.
Request Body
{"campaign_id": "6a61ef6bcc6388012e941c8f","status": "completed"}
Parameters:
campaign_id: The campaign to update. Obtain from/community/campaigns/list.status: One ofactive,paused, orcompleted.
Response
{"data": {"message": "Campaign status set to completed"}}
Community - Delete Campaign
https://api.cruva.com/community/campaigns/deletePermanently delete a campaign. This cascades: participant records, tracked videos, and progress rows are deleted from the engagement system first; only if that succeeds is the campaign document removed. It cannot be undone — prefer set_status with completed or paused unless you truly want it gone. The body is parsed with request.get_json(silent=True) so a missing or unparseable body returns a clean 400 instead of 415. Optionally idempotent — see the Idempotency section.
Request Body
{"campaign_id": "6a61ef6bcc6388012e941c8f"}
Parameters:
campaign_id: The campaign to delete. Obtain from/community/campaigns/list.
Response
{"data": {"message": "Campaign deleted.","campaign_id": "6a61ef6bcc6388012e941c8f"}}
Error Responses
// 400 — missing or malformed campaign_id{ "error": "Invalid campaign_id" }// 404 — not found, or not owned by this shop{ "error": "Campaign not found" }// 502 — engagement-system cascade failed; the campaign was NOT deleted{ "error": "Failed to delete campaign engagement data" }
Community - List Participants
https://api.cruva.com/community/campaigns/participantsList the creators in a campaign with their progress (GMV, posts, views), participation status, payment state, and — on cash campaigns — what each creator is currently owed. amount_owed is computed by the campaign's own payout rules (tiers, per-creator deals, caps) and already excludes paid participants; it is exactly what /community/payouts/authorize will pay. Use search to find one creator by handle, status: "pending" to pull the approval queue on invite-only campaigns.
Request Body
{"campaign_id": "6a61ef6bcc6388012e941c8f","page": 1,"page_size": 25,"status": "approved","search": "sassy","period_id": "6a61ef6bcc6388012e941c8f_2"}
Parameters:
campaign_id: The campaign. Obtain from/community/campaigns/list.page / page_size: Optional. Defaults1/25, max100.status: Optional filter. One ofpending,approved,rejected,under_review,removed.search: Optional. Case-insensitive substring match on the creator's handle (a leading@is ignored). Search results return the core fields (noviews/metric_value).period_id: Optional, recurring retainers only —"<campaign_id>_<period>"scopes results (and the owed calculation) to one period.
Response
{"data": {"participants": [{"creator_id": "3f6b2c1e-9a47-4d20-b8c3-2f1a9d7e6c05","handle": "sassykittypaw","status": "approved","gmv": 1197.33,"post_count": 14,"live_count": 0,"views": 238358,"metric_value": 238358,"joined_at": "2026-06-14T18:02:11+00:00","is_paid": false,"tier": null,"amount_owed": 50.0}],"page": 1,"page_size": 25,"has_more": true,"progress_metric": "views","counts": { "approved": 37, "pending": 5, "rejected": 2, "under_review": 0 },"approved_totals": { "gmv": 13397.42, "posts": 6154, "views": 4714573 },"amount_owed_total": 160.0}}
Response Fields:
creator_id: The participant's community creator id — pass it to/community/campaigns/participants/set_statusand the payout endpoints.is_paid: Whether this participant has already been paid for the (campaign, period).amount_owed: Cash campaigns only — the outstanding payout this creator has earned under the campaign's rules (per-video rates, tier prizes, per-creator deals, caps).0for paid creators and while the campaign hasn't finished (races/bingo accrue live). /community/payouts/authorize pays exactly this.amount_owed_total / amount_owed_note: Campaign-wide outstanding total, and — when owed amounts are0for a structural reason — why (e.g."Campaign is not finished yet","Leaderboard payouts become available once the campaign is completed").metric_value: The participant's progress in the campaign'sprogress_metric(GMV, post count, or views).
Community - Approve / Reject Participant
https://api.cruva.com/community/campaigns/participants/set_statusChange a creator's participation status in a campaign — the approval workflow for invite-only campaigns (require_approval: true). Approving increments the campaign's joined count. removed kicks a creator out of the latest period of a recurring retainer and requires a period_id.
Note: approval/rejection SMS notifications are only sent when acting from the dashboard — API status changes do not text the creator.
Request Body
{"campaign_id": "6a61ef6bcc6388012e941c8f","creator_id": "3f6b2c1e-9a47-4d20-b8c3-2f1a9d7e6c05","status": "approved"}
Parameters:
campaign_id: The campaign. Obtain from/community/campaigns/list.creator_id: The participant. Obtain from/community/campaigns/participants.status: One ofapproved,rejected,pending,under_review,removed.period_id:"<campaign_id>_<period>"— required forremovedon recurring retainers.
Response
{"data": {"message": "Creator status updated","status": "approved"}}
Error Responses
// 400 — invalid status, or missing period for "removed"{ "error": "status must be one of: pending, approved, rejected, under_review, removed" }// 404 — no engagement row for this creator in this campaign{ "error": "Engagement not found" }
Community - List Campaign Videos
https://api.cruva.com/community/campaigns/videosList the videos posted in a campaign, with per-video stats. Sortable and filterable by creator, GMV, and post date.
Request Body
{"campaign_id": "6a61ef6bcc6388012e941c8f","page": 1,"page_size": 25,"handle": "","min_gmv": 10,"sort_by": "gmv","sort_direction": "desc","date_from": "2026-07-01","date_to": "2026-07-23"}
Parameters:
campaign_id: The campaign. Obtain from/community/campaigns/list.sort_by: One ofgmv,view_count,like_count,comment_count,units_sold,ctr,post_time. Defaultgmv.handle / min_gmv / min_views / product_ids / disqualified: Optional filters.date_from / date_to: Optional ISO-date bounds on post time.
Response
{"data": {"videos": [{"video_id": "7523456789012345678","handle": "babypinkcashmere","title": "my new favorite find","gmv": 4247.51,"view_count": 1318277,"like_count": 88213,"comment_count": 1204,"units_sold": 312,"post_time": "07/03/2026","disqualified": false,"products": ["1800291847362058192"],"url": "https://tiktok.com/@babypinkcashmere/video/7523456789012345678"}],"page": 1,"page_size": 25,"total_count": 6154,"has_more": true}}
Response Fields:
url: Direct TikTok link to the video (tiktok.com/@handle/video/<video_id>).
Community - List Campaign LIVEs
https://api.cruva.com/community/campaigns/livesList the LIVE streams in a campaign, with per-live stats and campaign totals.
Request Body
{"campaign_id": "6a61ef6bcc6388012e941c8f","page": 1,"page_size": 25,"sort_by": "gmv"}
Parameters:
campaign_id: The campaign. Obtain from/community/campaigns/list.sort_by: One ofgmv,views,likes,comments,units_sold,duration,start_time. Defaultgmv.handle / min_gmv / date_from / date_to: Optional filters.
Response
{"data": {"lives": [{"live_id": "7519876543210987654","handle": "sassykittypaw","title": "Friday night deals!","start_time": "2026-07-18T01:00:00+00:00","duration": 7215,"views": 48210,"likes": 15302,"comments": 2210,"gmv": 1890.4,"units_sold": 145,"products": ["1800291847362058192"],"url": "https://www.tiktok.com/@sassykittypaw/live/7519876543210987654"}],"page": 1,"page_size": 25,"total_count": 12,"total_gmv": 8123.5,"total_views": 391200,"has_more": false}}
Community - Stats
https://api.cruva.com/community/statsPer-day time series for the community program: earnings (GMV), views, videos posted, and payout spend. GMV includes both video GMV and campaign-attributed LIVE GMV. Scope to a single campaign, all campaigns, or all retainers. Series are gap-filled — quiet days appear with 0.
Request Body
{"scope": "campaign","campaign_id": "6a61ef6bcc6388012e941c8f","date_from": "2026-06-23","date_to": "2026-07-23"}
Parameters:
scope: One ofcampaign(one campaign — requirescampaign_id),campaigns(all, default), orretainers.campaign_id: Required whenscopeiscampaign.date_from / date_to: OptionalYYYY-MM-DDwindow, max 366 days. Defaults to the last 30 days.
Response
{"data": {"scope": "campaign","campaign_id": "6a61ef6bcc6388012e941c8f","date_from": "2026-06-23","date_to": "2026-07-23","series": [{"type": "gmv_per_day","stats": [{ "day": "2026-06-23", "value": 27.68 }, { "day": "2026-06-24", "value": 59.93 }],"total": 13397.42},{ "type": "views_per_day", "stats": [ ... ], "total": 4714573 },{ "type": "videos_per_day", "stats": [ ... ], "total": 5716 },{ "type": "spend_per_day", "stats": [ ... ], "total": 1200 }]}}
Response Fields:
series: Four series:gmv_per_day(video + LIVE GMV),views_per_day,videos_per_day,spend_per_day(creator payouts).series[].total: Window total. Forvideos_per_daythis is the distinct video count across the window (a video active on multiple days counts once), so it can be less than the sum of the daily points.
Community - Balance
https://api.cruva.com/community/balanceRead the shop's community balance — the funds available to pay creators, in the shop's local currency. Computed live as top-ups minus payouts already sent; there is no stored balance. Payouts via /community/payouts/authorize are rejected when they exceed this balance. Funds are added from the dashboard (Community → Payments).
Request Body
{}
Response
{"data": {"balance": 3800.0,"total_charges": 5000.0,"total_transfers": 1200.0,"charges_count": 2,"transfers_count": 6,"currency": "usd"}}
Response Fields:
balance: Spendable funds:max(0, total_charges - total_transfers), in the shop's local currency.total_charges: Sum of completed balance top-ups.total_transfers: Sum of creator payouts already sent (net of reversals).
Community - Record Manual Payout
https://api.cruva.com/community/payouts/manualRecord that a creator was paid outside the platform (PayPal, wire, etc.) — no money moves. Marks the participant paid for the campaign period and adds the amount to the campaign's spend total. Rejected if the participant is already marked paid.
Omit amount to record exactly what the campaign says is owed (same calculation as /community/payouts/authorize); pass an explicit amount only for off-platform deals at a negotiated figure — unlike the Stripe payout, this endpoint accepts one, since it only records history.
Request Body
{"campaign_id": "6a61ef6bcc6388012e941c8f","creator_id": "3f6b2c1e-9a47-4d20-b8c3-2f1a9d7e6c05","amount": 250.0,"currency": "usd","period_id": "6a61ef6bcc6388012e941c8f_2"}
Parameters:
campaign_id: The campaign the payment was for. Obtain from/community/campaigns/list.creator_id: The participant. Obtain from/community/campaigns/participants.amount: Optional. Amount that was paid, incurrency. Defaults to the creator's outstandingamount_owed.currency: Optional ISO currency code. Defaults to the shop's local currency.period_id: Optional —"<campaign_id>_<period>"targets a specific recurring-retainer period; defaults to the latest.
Response
{"data": {"message": "Manual payout recorded (no money moved).","amount": 250.0,"currency": "usd","period": 2}}
Error Responses
// 400 — already recorded as paid{ "error": "Engagement already marked as paid" }// 404 — no engagement row for this creator in this campaign{ "error": "Engagement not found" }
Enterprise Access Required
The following endpoints are part of our Enterprise API tier, available exclusively to enterprise customers. These endpoints provide access to proprietary market intelligence and creator analytics that power competitive advantages for top brands and agencies.
To get access, reach out to our sales team and we'll set you up with a custom plan tailored to your needs.
EnterpriseCreator's Brands
https://api.cruva.com/intelligence/creators/brands/listGiven a creator_id (from Brand's Creators, Product's Creators, or Creator Performance), retrieve which brands a creator has driven revenue for over a time window.
Request Body
{"region": "us", // us | uk | mx | es | de | ie | it | fr | nl | pt | gr | be | at | pl | hu | cz | br | jp"params": {"creator_id": "7493993245181314497","page_number": 1,"page_size": 10,"sort": "gmv","ts_start": "2026-03-07","ts_end": "2026-04-06"}}
Response
{"data": {"creator_id": "7493993245181314497","region": "us","ts_start": "2026-03-07","ts_end": "2026-04-06","page_number": 1,"page_size": 10,"has_more": true,"results": [{"gmv": 24595.22,"views": 1600249.31,"videos": 4,"brand_name": "Color Wow UK","brand_id": "7494530868351044030"},{"gmv": 20236.33,"views": 1347692.71,"videos": 2,"brand_name": "DR.DENT - shop","brand_id": "7495899075622504605"}]}}
EnterpriseCreator's Products
https://api.cruva.com/intelligence/creators/products/listGiven a creator_id, retrieve which products a creator has driven revenue for over a time window. Returns product-level GMV, views, video count, price, and associated brand.
Request Body
{"region": "us", // us | uk | mx | es | de | ie | it | fr | nl | pt | gr | be | at | pl | hu | cz | br | jp"params": {"creator_id": "7493993245181314497","page_number": 1,"page_size": 10,"sort": "gmv","ts_start": "2026-03-07","ts_end": "2026-04-06"}}
Response
{"data": {"creator_id": "7493993245181314497","region": "us","ts_start": "2026-03-07","ts_end": "2026-04-06","page_number": 1,"page_size": 10,"has_more": false,"results": [{"product_id": "1729635919293618333","gmv": 20236.33,"views": 1347692.71,"videos": 2,"product_name": "DRDENT Purple Teeth Whitening Strips","price_value": 13.71,"brand_id": "7495899075622504605"},{"product_id": "1729734771029678669","gmv": 17712.33,"views": 1189024.33,"videos": 3,"product_name": "Soft Glam Satin Concealer","price_value": 6.0,"brand_id": "7495889862678710861"}]}}
EnterpriseCreator's Videos
https://api.cruva.com/intelligence/creators/videos/listGiven a creator_id, list videos posted by that creator over a time window. Optionally restrict to a specific brand and/or product the creator featured.
Request Body
{"region": "us", // us | uk | mx | es | de | ie | it | fr | nl | pt | gr | be | at | pl | hu | cz | br | jp"params": {"creator_id": "7494011944972290621","brand_id": "7495957905333913613","page_size": 10,"sort": "time_posted","sort_direction": "desc","is_ad": false,"is_ai": false,"ts_start": "2026-01-01","ts_end": "2026-05-21"}}
Parameters:
creator_id: Required. The creator to slice by.creator_oecuidandauthor_idare accepted as aliases.brand_id: *(optional)* Restrict to videos for this brand.shop_idis accepted as an alias.product_id: *(optional)* Restrict to videos featuring this product.page: Page number, ≥ 1. Defaults to1.page_numberis accepted as an alias.page_size: Results per page. Defaults to10, max100.sort: One oftime_posted,views,likes,comments,gmv. Defaults togmv.sort_direction:ascordesc. Defaults todesc.is_ad: *(optional)* Filter to videos whereis_ad = true/false. Unset returns both.is_ai: *(optional)* Filter to videos whereis_ai = true/false. Unset returns both.ts_start: *(optional)*YYYY-MM-DDlower bound ontime_posted.ts_end: *(optional)*YYYY-MM-DDupper bound ontime_posted.ts_days: *(optional)* If only one ofts_start/ts_endis provided, widens the missing side by this many days. Defaults to7. Only applied when at least one ofts_start/ts_endis set.
Body accepts either the nested form shown above or a flat form where the params sit at the top level alongside region. If region is set inside params, it overrides the top-level value.
Response
{"data": {"page": 1,"page_size": 10,"has_more": true,"results": [{"video_id": "7628815395075509517","video_link": "https://tiktok.com/@dealsbyaxel/video/7628815395075509517","creator_id": "7494011944972290621","handle": "dealsbyaxel","brand_id": "7495957905333913613","product_id": "1732273880228925453","description": "Mantente fresco este verano #summerheat #towerfan ...","time_posted": "2026-04-15T03:00:12","is_ad": 0,"is_ai": 0,"views": 949400,"likes": 4977,"comments": 87,"gmv": 8399.58,"categories": ["Household Appliances", "Home Appliances"],"language": "es"}]}}
EnterpriseBrand Search
https://api.cruva.com/intelligence/brands/searchSearch for brands across TikTok Shop by name. Returns brand performance metrics over a time window including GMV, views, creator count, and top category. Supports regional filtering.
Request Body
{"region": "us", // us | uk | mx | es | de | ie | it | fr | nl | pt | gr | be | at | pl | hu | cz | br | jp"params": {"page_number": 1,"page_size": 10,"search": "Shark","category": null,"sort": "gmv","ts_start": "2026-03-07","ts_end": "2026-04-06"}}
Response
{"data": [{"brand_id": "7496047005540322107","brand_name": "Shark","creator_count": 4737,"top_category": "Household Appliances","region": "us","gmv": 3026883.79,"views": 149453235.52,"video_count": 12789,"shop_name": "Shark UK"}]}
EnterpriseBrand's Creators
https://api.cruva.com/intelligence/brands/creators/listGiven a brand_id (from Brand Search), retrieve the creators driving revenue for that brand over a time window. Returns full creator profiles including demographics, category splits, follower breakdowns, and brand-specific GMV.
Request Body
{"region": "us", // us | uk | mx | es | de | ie | it | fr | nl | pt | gr | be | at | pl | hu | cz | br | jp"params": {"brand_id": "7495899075622504605","page_number": 1,"page_size": 10,"sort": "gmv","ts_start": "2026-03-07","ts_end": "2026-04-06"}}
Response
{"data": {"results": [{"handle": "example_creator","nickname": "Example Creator","bio": "Beauty & lifestyle creator","follower_cnt": 103155,"category": ["Beauty & Personal Care", "Health"],"top_follower_ages": ["25-34", "35-44"],"top_follower_gender": "Female","video_avg_view_cnt": 695,"video_engagement": 170,"brand_collaborations": 80,"med_gmv_revenue": 142337, // total GMV earned across the platform in the last 30 days"category_splits": {"Beauty & Personal Care": 68.88,"Health": 17.07},"follower_gender_breakdown": [{ "key": "male", "value": 0.3768 },{ "key": "female", "value": 0.4879 }],"follower_age_breakdown": [{ "key": "25-34", "value": 0.304 },{ "key": "35-44", "value": 0.2674 },{ "key": "18-24", "value": 0.182 }],"regional_geography": [{ "key": "ENGLAND", "value": 7106 },{ "key": "SCOTLAND", "value": 762 }],"email": "creator@example.com","gender": "female","age": "20s","language": "english","shop_gmv": 33893.1,"video_count": 30,"creator_id": "7494974553083775074"}],"has_more": true}}
EnterpriseBrand's Videos
https://api.cruva.com/intelligence/brands/videos/listGiven a brand_id (from Brand Search), list videos posted about products belonging to that brand over a time window. Returns per-video performance (views, likes, comments, GMV), the creator who posted it, the product featured, and content metadata (categories, language, ad/AI flags).
Request Body
{"region": "us", // us | uk | mx | es | de | ie | it | fr | nl | pt | gr | be | at | pl | hu | cz | br | jp"params": {"brand_id": "7495514739648989419","page": 1,"page_size": 10,"sort": "gmv","sort_direction": "desc","is_ad": true,"ts_start": "2026-01-01","ts_end": "2026-05-21"}}
Parameters:
brand_id: Required. The brand to slice by.shop_idis accepted as an alias.page: Page number, ≥ 1. Defaults to1.page_numberis accepted as an alias.page_size: Results per page. Defaults to10, max100.sort: One oftime_posted,views,likes,comments,gmv. Defaults togmv.sort_direction:ascordesc. Defaults todesc.is_ad: *(optional)* Filter to videos whereis_ad = true/false. Unset returns both.is_ai: *(optional)* Filter to videos whereis_ai = true/false. Unset returns both.ts_start: *(optional)*YYYY-MM-DDlower bound ontime_posted.ts_end: *(optional)*YYYY-MM-DDupper bound ontime_posted.ts_days: *(optional)* If only one ofts_start/ts_endis provided, widens the missing side by this many days. Defaults to7. Only applied when at least one ofts_start/ts_endis set.
Body accepts either the nested form shown above or a flat form where the params sit at the top level alongside region (e.g. { "region": "us", "brand_id": "...", "sort": "gmv" }). If region is set inside params, it overrides the top-level value.
Response
{"data": {"page": 1,"page_size": 10,"has_more": true,"results": [{"video_id": "7628775782449433870","video_link": "https://tiktok.com/@thestephedition/video/7628775782449433870","creator_id": "7494010987393321524","handle": "thestephedition","brand_id": "7495514739648989419","product_id": "1729508370969629931","description": "Korean labs are flexin' with the stick version #medicube #volufiline #pdrn ","time_posted": "2026-04-15T00:26:26","is_ad": 1,"is_ai": 0,"views": 18700000,"likes": 110000,"comments": 142,"gmv": 18942.0,"categories": ["Beauty & Personal Care", "Skincare"],"language": "en"}]}}
EnterpriseProduct Search
https://api.cruva.com/intelligence/products/searchSearch for products across TikTok Shop by name. Returns product performance metrics over a time window including GMV, units sold, views, creator count, and associated brand.
Request Body
{"region": "us", // us | uk | mx | es | de | ie | it | fr | nl | pt | gr | be | at | pl | hu | cz | br | jp"params": {"page_number": 1,"page_size": 10,"search": "Whitening Strips","category": null,"sort": "gmv","ts_start": "2026-03-07","ts_end": "2026-04-06"}}
Response
{"data": [{"product_id": "1729635919293618333","product_name": "DRDENT Purple Teeth Whitening Strips","price_value": 13.71,"creator_count": 3597,"shop_name": "DR.DENT - shop","gmv": 400820.59,"units_sold": 29237,"views": 54625344,"video_count": 2704,"brand_id": "7495899075622504605"}]}
EnterpriseProduct's Creators
https://api.cruva.com/intelligence/products/creators/listGiven a product_id (from Product Search), retrieve the creators driving revenue for that product over a time window. Same response shape as Brand's Creators, with product_gmv instead of shop_gmv.
Request Body
{"region": "us", // us | uk | mx | es | de | ie | it | fr | nl | pt | gr | be | at | pl | hu | cz | br | jp"params": {"product_id": "1729635919293618333","page_number": 1,"page_size": 10,"sort": "gmv","ts_start": "2026-03-07","ts_end": "2026-04-06"}}
Response
{"data": {"results": [{"handle": "example_creator","nickname": "Example Creator","bio": "Beauty creator","follower_cnt": 29255,"category": ["Beauty & Personal Care", "Health"],"top_follower_ages": ["35-44", "25-34"],"top_follower_gender": "Female","video_avg_view_cnt": 1114,"video_engagement": 120,"brand_collaborations": 28,"med_gmv_revenue": 88103, // total GMV earned across the platform in the last 30 days"category_splits": {"Beauty & Personal Care": 96.48,"Health": 3.0},"follower_gender_breakdown": [{ "key": "male", "value": 0.1606 },{ "key": "female", "value": 0.7009 }],"follower_age_breakdown": [{ "key": "35-44", "value": 0.3272 },{ "key": "25-34", "value": 0.2417 }],"regional_geography": [{ "key": "ENGLAND", "value": 8189 },{ "key": "SCOTLAND", "value": 848 }],"email": "creator@example.com","gender": "female","age": "30s","language": "english","product_gmv": 22143.32,"video_count": 16,"creator_id": "7495894823766755988"}],"has_more": true}}
EnterpriseProduct's Videos
https://api.cruva.com/intelligence/products/videos/listGiven a product_id (from Product Search), list videos that feature that product over a time window. Returns the same per-video performance and metadata as Brand's Videos.
Request Body
{"region": "us", // us | uk | mx | es | de | ie | it | fr | nl | pt | gr | be | at | pl | hu | cz | br | jp"params": {"product_id": "1729508370969629931","page": 1,"page_size": 10,"sort": "views","sort_direction": "desc","is_ai": false}}
Parameters:
product_id: Required. The product to slice by.page: Page number, ≥ 1. Defaults to1.page_numberis accepted as an alias.page_size: Results per page. Defaults to10, max100.sort: One oftime_posted,views,likes,comments,gmv. Defaults togmv.sort_direction:ascordesc. Defaults todesc.is_ad: *(optional)* Filter to videos whereis_ad = true/false. Unset returns both.is_ai: *(optional)* Filter to videos whereis_ai = true/false. Unset returns both.ts_start: *(optional)*YYYY-MM-DDlower bound ontime_posted.ts_end: *(optional)*YYYY-MM-DDupper bound ontime_posted.ts_days: *(optional)* If only one ofts_start/ts_endis provided, widens the missing side by this many days. Defaults to7. Only applied when at least one ofts_start/ts_endis set.
Body accepts either the nested form shown above or a flat form where the params sit at the top level alongside region. If region is set inside params, it overrides the top-level value.
Response
{"data": {"page": 1,"page_size": 10,"has_more": true,"results": [{"video_id": "7631027139160067342","video_link": "https://tiktok.com/@thefaceglow/video/7631027139160067342","creator_id": "7496236347295828872","handle": "thefaceglow","brand_id": "7495514739648989419","product_id": "1729508370969629931","description": "After 30 we need powerful active ingredients...","time_posted": "2026-04-21T02:03:04","is_ad": 1,"is_ai": 0,"views": 9200000,"likes": 59600,"comments": 1102,"gmv": 11256.0,"categories": ["Beauty & Personal Care", "Skincare"],"language": "en"}]}}
EnterpriseCreator Data
https://api.cruva.com/affiliate/marketplace/searchRetrieve detailed creator performance metrics and demographics for a specific creator handle.
Request Body
Parameters:
region: TikTok Shop market to query. One ofus,uk,mx,es,de,ie,it,fr,nl,pt,gr,be,at,pl,hu,cz,br,jp.handle: Creator's TikTok handle (must match exactly - no fuzzy matching)Note: The handle must match exactly to avoid data abuse. Fuzzy matching is not supported.
Response