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:

  1. Go to cruva.com/dashboard/my-shops
  2. Click the dropdown on the right of the shop, then View Info
  3. 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 key
curl -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

GEThttps://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 as Idempotency-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: processing if a concurrent write holds the key, completed if the cached response is available.
  • idempotency.response_status: HTTP status code of the original response. Only present when status is completed.
  • idempotency.response_body: The exact body returned by the original request. Only present when status is completed.

List Shops

GEThttps://api.cruva.com/account/shops

Retrieve 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

POSThttps://api.cruva.com/shop/products

List 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_plantrue for open plan only, false for non-open plan only, null for all. sort_byprice or units_sold (default units_sold). sort_directionasc 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

POSThttps://api.cruva.com/shop/skus

List 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_byprice or stock (default stock). sort_directionasc 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

POSThttps://api.cruva.com/timeseries/products

Search 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 controls
  • date_range: Object with from and to dates (YYYY-MM-DD)
  • sort_by: One of total_gmv, affiliate_gmv, video_gmv, live_gmv, shop_tab_gmv, shop_tab_impressions, units_sold, subscription_gmv, new_subscriptions, active_subscriptions, recurring_subscriptions
  • sort_direction: ASC or DESC
  • product_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

POSThttps://api.cruva.com/timeseries/skus

Search 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 controls
  • date_range: Object with from and to dates (YYYY-MM-DD)
  • sort_by: One of gmv, units_sold, orders, gross_sales
  • sort_direction: ASC or DESC
  • product_id: *(optional)* Filter to a single product's SKUs
  • sku_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

POSThttps://api.cruva.com/shop/stats

Retrieve 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 with from and to dates (YYYY-MM-DD)
  • include_charts: If true, includes daily_counts array for each stat
  • stats: Array of stat keys to retrieve (see table below)
  • product_id: *(optional)* Filter results to a specific product
  • cpm: *(optional)* CPM value for earned media value calculation. Defaults to 5
  • timezone: *(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 KeyChartDescription
affiliate_gmvYesAffiliate GMV driven by brand in time period
total_gmvYesTotal GMV including affiliate, ads, and organic
affiliate_units_soldYesAffiliate units sold in time period
total_units_soldYesTotal units sold including affiliate, ads, and organic
videos_postedYesAll videos posted (affiliate + brand)
affiliate_videos_postedYesAffiliate-only videos posted
video_viewsYesVideo views driven in time period
likesYesLikes from affiliate videos
commentsYesComments from affiliate videos
gpmYesGMV per 1,000 views
emvYesEarned media value = (views / 1000) × CPM
commissionYesAffiliate commission paid
distinct_creatorsYesUnique creators who posted
first_time_postersYesAffiliates who made their first ever post
daily_active_affiliatesYesCreators 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_sentYesDMs sent from Cruva (includes open plan cards)
open_plan_cards_sentYesOpen plan cards sent from Cruva (subset of dms_sent)
followup_dms_sentYesFollowup DMs sent from Cruva (not included in dms_sent)
repliesYesCreator replies received
tc_invites_sentYesTarget Collaboration invites sent from Cruva
sample_requestsYesSample requests received
tc_sample_requestsYesSample requests via Target Collaboration (subset of sample_requests)
open_sample_requestsYesSample requests via Open Collaboration (subset of sample_requests)
samples_approvedYesSamples approved
samples_deliveredYesSamples delivered
gmv_per_sample_deliveredYesGMV per sample delivered
live_gmvYesLIVE stream GMV. Unfiltered requests use authoritative shop-wide totals; campaign_id/product_id filters only cover tracked lives
lives_postedYesNumber of LIVE streams
slideshow_gmvYesGMV driven by shoppable slideshows (photo-mode posts). Tracked separately from video_gmv — the two do not overlap
slideshow_postsYesNumber of slideshows posted. Counted on the day each slideshow was published
slideshow_viewsYesViews on shoppable slideshows during the period
video_gmvYesGMV driven by short-form videos
shop_tab_gmvYesGMV driven from the shop tab
shop_tab_impressionsYesImpressions on the shop tab
ad_spendYesTotal ad spend during the period
ad_roiYesReturn on ad spend (GMV / ad spend)
refund_amountYesTotal refunds (currency). Shop-wide — ignores campaign_id; product_id returns 0
gmv_with_cofundingYesGMV including cofunding (currency). Shop-wide — ignores campaign_id; product_id returns 0
platform_ordersYesTotal platform orders. Shop-wide — ignores campaign_id; product_id returns 0
aovYesAverage order value (currency). Total is the average across non-zero days, not a sum. Respects product_id
new_content_gmvYesGMV from videos posted within the period (currency). Respects campaign_id and product_id
emails_sentYesEmails sent from Cruva. Respects campaign_id
samples_shippedYesSamples shipped. Respects campaign_id, product_id, and only_show_campaign_stats
refundable_sample_requestsYesRefundable samples requested (creator buys and is refunded after posting). Respects campaign_id and product_id
refundable_samples_shippedYesRefundable samples shipped. Respects campaign_id and product_id
refundable_samples_deliveredYesRefundable samples delivered/received. Respects campaign_id and product_id
samples_refundedYesRefundable samples refunded, bucketed by refund date. Respects campaign_id and product_id
sample_refund_amountYesTotal refund amount paid for refundable samples (currency), bucketed by refund date. Respects campaign_id and product_id
customer_countYesUnique customers. Whole-shop sum unless product_id is set
add_to_cart_countYesAdd to cart events. Whole-shop sum unless product_id is set
shipping_feesYesShipping fees (currency). Whole-shop sum unless product_id is set
product_impressionsYesProduct impressions. Whole-shop sum unless product_id is set
subscription_revenueYesRevenue from subscription orders (currency). Ignores campaign_id; respects product_id
one_time_purchase_gmvYesGMV from one-time, non-subscription purchases (currency). Shop-wide — ignores campaign_id and product_id
recurrent_order_gmvYesGMV from repeat/recurring subscription orders (currency). Shop-wide — ignores campaign_id and product_id
first_subscription_order_gmvYesGMV from the first order of each new subscription (currency). Shop-wide — ignores campaign_id and product_id
non_recurring_gmvYesOne-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_subscriptionsYesSubscriptions started during the period. Ignores campaign_id; respects product_id
active_subscribersYesDistinct 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_subscriptionsYesActive subscriptions (a subscriber may hold several). Point-in-time — latest reported day, not a sum. Ignores campaign_id; respects product_id
subscription_aovYesAverage 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_rateYesPercent 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_rateYesPercent 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_creatorNoAverage videos posted per creator
avg_gmv_per_videoNoAverage GMV per video
sample_ratioNoSample requests / Target Collabs sent
reply_ratioNoReplies / 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

GEThttps://api.cruva.com/shop/sps

Retrieve 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

POSThttps://api.cruva.com/affiliate/crm/list

Retrieve 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 within date_range when 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; email falls 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; null when 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

POSThttps://api.cruva.com/affiliate/videos/list

Retrieve 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

POSThttps://api.cruva.com/timeseries/affiliates

Search 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 controls
  • date_range: Object with from and to dates (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_gmv is not sortable — see the response field notes below.
  • sort_direction: ASC or DESC
  • handle: *(optional)* Filter by creator handle
  • campaign_id: *(optional)* Filter by campaign
  • product_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, not handle.
  • 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 to date_range. video_avg_view_cnt is TikTok's trailing-30-day average views per video; 0 means 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_gmv and live_gmv are 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 to gmv.
  • new_video_gmv: The subset of video_gmv earned by videos posted inside the same `date_range` — i.e. GMV from fresh content, not the back catalogue. With no date_range supplied there is nothing to scope by and this equals video_gmv. Clamped to gmv.
  • 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 a sort_by field.
  • 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

POSThttps://api.cruva.com/timeseries/videos

Search 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 controls
  • date_range: Object with from and to dates (YYYY-MM-DD)
  • sort_by: Field to sort by (e.g. gmv, views, units_sold)
  • sort_direction: ASC or DESC
  • handle: *(optional)* Filter by creator handle
  • product_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

POSThttps://api.cruva.com/affiliate/slideshows/list

Retrieve 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: ASC or DESC
  • handle: *(optional)* Filter by creator handle
  • product_filter: *(optional)* Filter by product ID
  • campaign_id: *(optional)* Filter by campaign
  • date_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

POSThttps://api.cruva.com/timeseries/slideshows

Search 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 controls
  • date_range: Object with from and to dates (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: ASC or DESC
  • handle: *(optional)* Filter by creator handle
  • product_id: *(optional)* Filter by product ID
  • campaign_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

POSThttps://api.cruva.com/affiliate/lives/list

Retrieve 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

POSThttps://api.cruva.com/spark/list

Retrieve 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_time is the canonical alias of created_at.
  • video_id: TikTok video id, or null if not yet linked.
  • auth_start_time / auth_end_time: Authorization window (MM/DD/YYYY), or null.
  • 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. (Both NOT_DELIVERYING and the correctly-spelled NOT_DELIVERING exist upstream as distinct values.) null when 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, or null when the code isn't running as a GMV Max ad. cpa is cost per acquisition. Sortable via sort_by.
  • video_link: https://tiktok.com/@{handle}/video/{video_id} (literal None when video_id is null).
  • video_data: Joined content row (matched on shop_id + video_id) with analytics fields, or null when no matching content row exists. Timestamps are ISO YYYY-MM-DDTHH:MM:SS.
  • total_count: Total matching rows; null when include_total is false.

GMV Max - List Campaigns

POSThttps://api.cruva.com/ads/campaigns

List 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-DD bounds. Returns 400 "Missing date_range (from/to)" if absent.
  • page: *(optional)* 1-based page number. Defaults to 1.
  • page_size: *(optional)* Results per page. Defaults to 10, clamped to 1100.
  • sort_by: *(optional)* One of ad_spend, ad_revenue, orders, campaign_name, operation_status, budget, roas_target. Invalid values fall back to ad_spend.
  • sort_direction: *(optional)* ASC or DESC (nulls sort last). Defaults to DESC.

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 (from shop_performance): ad_spend (total ad spend), ad_revenue (total ad-attributed revenue), and ad_roi (ad_revenue / ad_spend, 0 if 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, or null.
  • campaigns[].operation_status: Campaign state, e.g. ENABLE, DISABLE. May be null.
  • campaigns[].budget: Campaign budget.
  • campaigns[].roas_target: Target return on ad spend.
  • campaigns[].schedule_type: e.g. SCHEDULE_FROM_NOW, SCHEDULE_START_END. May be null.
  • campaigns[].schedule_start_time / schedule_end_time: Schedule window as datetime strings, or null.
  • campaigns[].product_specific_type: e.g. CUSTOMIZED_PRODUCTS. May be null.
  • campaigns[].auto_budget_enabled: Whether auto-budget is enabled.
  • campaigns[].store_id: TikTok store id, or null.
  • campaigns[].item_group_ids: Targeted product/item group id(s). Optionally pass as product_ids to GMV Max - List Creatives — though campaign_id is 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 (0 if no spend).
  • campaigns[].cost_per_order: ad_spend / orders (0 if no orders).
  • total_count: Counts all campaigns for the shop, independent of the date range.

GMV Max - List Creatives

POSThttps://api.cruva.com/ads/creatives

List 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-DD bounds of the metrics window — all ad metrics are aggregated over these days. Either bound may be sent alone (from defaults to 6 months ago, to to today). Omit entirely for the trailing 6 months.
  • posted_date.from / posted_date.to: *(optional)* YYYY-MM-DD bounds filtering videos by post date; videos with an unknown post date are excluded when set. Either bound may be sent alone. The legacy date_range key is an alias of this field.
  • page: *(optional)* 1-based page number. Defaults to 1.
  • page_size: *(optional)* Results per page. Defaults to 5, clamped to 11000.
  • sort_by: *(optional)* See the sortable list in the notes. Invalid values fall back to ad_spend.
  • sort_direction: *(optional)* ASC or DESC (nulls last). Defaults to DESC.
  • campaign_id: *(optional)* string. Restrict to a single GMV Max campaign (its campaign_id from GMV Max - List Campaigns). Combines with the product/video filters.
  • product_ids: *(optional)* string[] or string. Restrict to these product IDs (e.g. a campaign's item_group_ids).
  • video_ids: *(optional)* string[] or string. Look up specific creative video IDs.
  • statuses: *(optional)* string[] or string. 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, or null.
  • creatives[].status: Ad status (see the status list in the notes) as last observed within the metrics window, or null.
  • creatives[].date_posted: Video post date YYYY-MM-DD, or null. Not affected by date_range.
  • creatives[].ad_spend / gross_revenue / roi / cpa: Ad performance over the metrics window. roi and cpa are 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_order equals cpa at window grain.
  • creatives[].click_rate / ad_click_rate / conversion_rate / view_rate_2s / view_rate_6s: Percentages (e.g. 1.87 = 1.87%). click_rate is exact over the window (clicks ÷ impressions); the others are impression/click-weighted averages of the daily values.
  • creatives[].handle: Creator handle, or null if no matching content row.
  • creatives[].title: Video caption/title, or null.
  • 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_link is an alias of url.
  • available_statuses: Distinct statuses present in the base scope (shop + campaign + products + videos + date) before the statuses filter — 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

POSThttps://api.cruva.com/meta/campaigns

List 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-DD bounds for the metrics window. Returns 400 "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) and ASC/DESC. Defaults: spend DESC, 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, or null for spend rows the mirror couldn't attribute to a campaign.
  • campaigns[].campaign_name: Campaign name as stamped at push time, or null when 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_value is Meta-attributed (Pixel/CAPI); TikTok Shop sales don't appear here.
  • campaigns[].ctr: clicks / impressions over the window (a fraction, e.g. 0.0152 = 1.52%).
  • campaigns[].roas: purchase_value / spend (0 if no spend).
  • campaigns[].cost_per_purchase: spend / purchases (0 if no purchases).
  • total_count: Total campaigns with activity in the window (within the filters).

Meta Ads - List Ad Sets

POSThttps://api.cruva.com/meta/adsets

List 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-DD metrics 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; defaults 1 / 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 (null name 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

POSThttps://api.cruva.com/meta/ads

List 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-DD metrics window.
  • campaign_id / adset_id: *(optional)* Restrict to one campaign and/or one ad set (arrays accepted via campaign_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; defaults 1 / 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 (null when unknown).
  • ads[].program_id: Meta Ads program the push was attributed to, or null.
  • ads[].handle: Creator's TikTok handle from the push registry (falls back to the content row), or null.
  • 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, or null.
  • ads[].video_link: TikTok URL of the source video, or null when handle/video are unknown.
  • ads[].spend … cost_per_purchase: Ad metrics over the window — same semantics as Meta Ads - List Campaigns. purchase_value is Pixel/CAPI-attributed.
  • total_count: Total ads with activity in the window (within the filters).

Meta Ads - List Programs

POSThttps://api.cruva.com/meta/programs

List 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 of active, paused, archived. Defaults to active + paused (archived = deleted programs, kept for history).
  • date_range.from / date_range.to: *(optional)* YYYY-MM-DD window for the spend/sales rollup. Omit for all-time.
  • page / page_size / sort_by / sort_direction: *(optional)* Pagination and sorting; defaults 1 / 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 as program_id on 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), or archived (deleted).
  • programs[].approval_mode: manual — joining creators wait for brand approval; auto — approved immediately.
  • programs[].cadence: Payout trigger: daily, weekly, biweekly, or monthly. Payouts are queued on this schedule and only sent after brand approval.
  • programs[].commission_rules: How members earn. type is one of gmv_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 carry pct; flat rules carry amount_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 requested date_range). sales is 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

POSThttps://api.cruva.com/meta/programs/creators

List 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 of pending, approved, rejected, removed. Defaults to pending + approved.
  • handle: *(optional)* Case-insensitive substring match on the creator's TikTok handle.
  • page / page_size / sort_by / sort_direction: *(optional)* Pagination and sorting; defaults 1 / 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, or null.
  • creators[].status: Membership status: pending (awaiting brand approval), approved, rejected, or removed.
  • creators[].program_id / program_name: The program this membership belongs to.
  • creators[].commission_pct: The GMV % snapshotted on the membership, or null (older memberships only carry this; see commission_rules for 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_at null while 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

POSThttps://api.cruva.com/affiliate/samples/funnel

Aggregate 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 whose time_field falls in the window, after the product/campaign/source filters.
  • by_status[]: Current state: where each request sits now, ordered by count descending. 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 of total_count.
  • by_status[].is_open: true while the request is still moving (To Review, Ready to Ship, Shipped, Content Pending, Overdue, …). Ageing fields are present only on these — the age of a Rejected row 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, old To Review queue is a review backlog; a large, old Content Pending queue is creators sitting on product. Open statuses only.
  • funnel[]: Progression through requestedapprovedshippeddeliveredposted. 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. null on the entry stage.
  • funnel[].dropped_from_previous: Requests that reached the previous stage but not this one. null on the entry stage.
  • timing[].median_days: Median days for that transition, over requests carrying both timestamps. null when no request has made the transition. Slow is not the same as leaky — a stage can convert well and still take weeks, so read timing alongside funnel.
  • biggest_bottleneck: The transition losing the largest *share* of the funnel (not the largest absolute count, which would always name the first step). null when nothing drops. Note a large drop at approved is 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

POSThttps://api.cruva.com/affiliate/samples/list

Retrieve 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

POSThttps://api.cruva.com/affiliate/samples/refundable/list

Retrieve 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

POSThttps://api.cruva.com/affiliate/samples/approve

Approve 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 of apply_id values from the Sample Request Search response

Response

{
"data": {
"message": "Successfully approved 2 sample requests",
"success_count": 2
}
}

Reject Sample Requests

POSThttps://api.cruva.com/affiliate/samples/reject

Reject 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 of apply_id values from the Sample Request Search response

Response

{
"data": {
"message": "Successfully rejected 2 sample requests",
"success_count": 2
}
}

Outreach - Send Direct Message

POSThttps://api.cruva.com/affiliate/message/dm

Send 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 unless conversation_id is 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 unless handle is 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: false if TikTok rejected the send — check message for the reason. Note that the endpoint still returns 200 in 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 when success is false (e.g. an IM quota limit based on your 30-day affiliate GMV).

Outreach - List Messages

POSThttps://api.cruva.com/affiliate/message/list

List 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 handle
  • page_size: Number of messages to return. Max 20.
  • page_token: Optional. Omit on the first request. To fetch the next page, pass back the next_page_token returned in the previous response. Continue until has_more is false.

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, or SYSTEM. Only TEXT messages are guaranteed to have a content string. TikTok NOTIFICATION events are filtered out.
  • messages[].sender: creator or brand.
  • has_more: true if additional pages are available.
  • next_page_token: Opaque cursor for the next page. Pass this as page_token on the subsequent request. Absent (or empty) once has_more is false.

Pagination Example

# Loop until has_more is false, threading next_page_token forward.
page_token = None
while True:
body = {"handle": "example_creator", "page_size": 20}
if page_token:
body["page_token"] = page_token
resp = 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"):
break
page_token = data.get("next_page_token")
if not page_token:
break

Outreach - List Inbox

POSThttps://api.cruva.com/affiliate/message/inbox

List 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 of ALL, UNREPLIED, READ, or UNREAD
  • page_size: Number of conversations to return. Max 50.
  • page_token: Optional. Omit on the first request. To fetch the next page, pass back the next_page_token returned in the previous response. Continue until has_more is false.

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 as conversation_id to 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: true if additional pages are available.
  • next_page_token: Opaque cursor for the next page. Pass this as page_token on the subsequent request. Absent (or empty) once has_more is false.

Pagination Example

# Loop until has_more is false, threading next_page_token forward.
page_token = None
while True:
body = {"conversation_status": "ALL", "page_size": 20}
if page_token:
body["page_token"] = page_token
resp = 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"):
break
page_token = data.get("next_page_token")
if not page_token:
break

Outreach - List Activity Logs

POSThttps://api.cruva.com/outreach/logs/list

Search 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 automation campaign_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 an error_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: success or failfail rows carry the failure reason in error_code.
  • results[].sender_email: The mailbox the message was sent from (email campaigns; null on rows written before this field existed).
  • total_count: Total rows matching the filters.

Automations - List Automations

POSThttps://api.cruva.com/automations/list

List 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 to 1.
  • page_size: Optional. Number of results per page. Defaults to 25, max 100.
  • 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 of active or stopped.
  • message_type: Optional. Filter by outreach mode. One of dm, invite, or invite+dm.
  • sort_by: Optional. Sort field — expand Sortable fields below. Defaults to created_at.
  • sort_direction: Optional. asc or desc (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 of invite, dm, or invite+dm.
  • dm_messages: Ordered sequence of messages the DM automation sends. Each item has a message_type (e.g. message, image, product, batch_product, batch_image, followup) and a payload that varies by type. Empty when message_type is invite.
  • invite_details: Configuration for invite automations: invite_title, invite_message, contact_email, the expiry (expire_time/expire_grain for relative, or expiration_type: "date" + expiration_date), resolve_conflicts, the products offered (with commission rates), and sample_policy. null when message_type is dm.
  • outreach_audience: Which creators the automation targets (list, new_affiliates, or groups).
  • outreach_filters: Filters applied on top of the audience — see the Creator Filters Reference.
  • lists: For list audiences: every uploaded-list title the campaign targets (legacy single-list bots surface as a one-element array).
  • send_to_all: false means the campaign skips creators already messaged by the shop's other campaigns.
  • content_type: Preferred creator content type: any, live, or video.
  • 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: For groups audiences: the CRM group_id the automation targets. null otherwise.
  • 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. null when the estimator hasn't run for this campaign.
  • has_more: true if additional pages are available.
  • total_count: Total number of automations matching the query.

Automations - Create Automation

POSThttps://api.cruva.com/automations/create

Create 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. invite sends Target Collab invitations only, dm sends direct messages only, invite+dm combines both (recommended — the invite card rides inside the DM thread).
  • outreach_audience: Who to target. new_affiliates = the platform-wide creator pool narrowed by outreach_filters; groups = a saved CRM segment (requires group_id); list = one or more saved lists (requires list_ids — see the Lists endpoints).
  • outreach_filters: Optional filter object narrowing the new_affiliates pool. Full field vocabulary and allowed values in the Creator Filters Reference below.
  • list_ids: Required for list audiences: an array of up to 10 list_id values, as returned by POST /lists/create and POST /lists/list.
  • group_id: For groups audiences: a group_id from /groups/list.
  • invite_details: Required when message_type is invite or invite+dm. Must include title (≤29 chars), message (≤500 chars), contact_email, offer_free_samples (explicit boolean), at least one entry in products, 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_samples and resolve_conflicts are optional booleans.
  • invite_details.products[]: Products offered on the invite. Each entry: product_id (string), commission (percent, 1-80), optional shop_ads_commission (percent, 0.01-80).
  • dm_messages: Ordered list of DM steps — required for `dm` / `invite+dm`. Ignored for pure invite. `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. Plain dm needs at least one non-followup step and never contains an invite_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 return 400. Each item is { "type": ... } plus a type-specific payload — expand DM message item types below. message/followup content is capped at 2,500 characters. Follow-up delays must be unique integers ≥ 1. Server assigns each step a UUID.
  • status: Initial status. One of active or stopped. Defaults to stopped so you can review before launch.
  • send_to_all: Default true — message every matching creator. Set false to 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), or video (shoppable video).
  • time_limits: Daily send window: from/to as 24-hour HH:MM strings plus an IANA timezone (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 (mondaysunday), values are integers 0-3000. A missing day is uncapped; 0 pauses that day entirely.
  • daily_limits_timezone: IANA timezone whose midnight resets the daily caps.
  • filter_by_entry_date / entry_date_threshold: When filter_by_entry_date is true, only creators whose audience entry date passes entry_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/toggle or /automations/delete.
  • messages_remaining: Estimated audience size for new_affiliates / list. null for groups or when the estimator failed soft.

Email Campaigns - List Sender Emails

GEThttps://api.cruva.com/emails/senders/list

List 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, or ses (custom domain address — no mailbox behind it).
  • sender_emails[].sender_name: From display name (custom domain addresses only; null otherwise).
  • 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, or failed. 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

POSThttps://api.cruva.com/emails/senders/create-custom

Create 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 with status: "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

POSThttps://api.cruva.com/emails/senders/update-sender-name

Set 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

POSThttps://api.cruva.com/emails/campaigns/create

Create 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 (400 names 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 (400 past 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 by outreach_filters; only creators with a known email address receive anything), groups (requires group_id), or list (requires list_ids).
  • outreach_filters: Optional filter object narrowing the new_affiliates pool. Full vocabulary in the Creator Filters Reference.
  • list_ids: Required for list audiences: an array of up to 10 list_id values.
  • group_id: For groups audiences: a group_id from /groups/list.
  • status: Initial status. active starts sending immediately; default stopped so you can review before launch.
  • send_to_all: Default true — email every matching creator. false skips creators already contacted by the shop's other campaigns.
  • filter_by_entry_date / entry_date_threshold: When filter_by_entry_date is true, only creators whose audience entry date passes entry_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 for new_affiliates / list. null for groups or when the estimator failed soft.

Email Campaigns - List Email Campaigns

POSThttps://api.cruva.com/emails/campaigns/list

List 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: When true, each row also carries the full HTML email_body (up to 2 MB per campaign — request narrow pages). Every row always includes a tag-stripped 300-char body_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; null otherwise. One-off failures appear only in List Activity Logs.
  • email_views: Opens recorded via the tracking pixel.
  • email_body: Full HTML body — present only when include_body: true.

Email Campaigns - Update Email Campaign

POSThttps://api.cruva.com/emails/campaigns/update

Partial 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 stored daily_limit, the limit is clamped automatically (or pass daily_limit explicitly).
  • status: Optionally also set active / stopped in 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

POSThttps://api.cruva.com/emails/campaigns/toggle

Start (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

DELETEhttps://api.cruva.com/emails/campaigns/delete

Permanently 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

POSThttps://api.cruva.com/automations/update

Partial 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. just contact_email). products replaces the whole product list when provided (each entry re-validated). Pass expire_date to switch to fixed-date expiry, or expire_time + expire_grain for relative.
  • dm_messages: Replaces the entire message sequence when provided, re-validated against the final message_typeinvite+dm requires [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 {} (or null) to clear the send window or the daily caps; pass a full object to change them.
  • status: Optional. active or stopped — saves a separate /automations/toggle call.

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 (null if the estimator failed soft).

Automations - Toggle Automation

POSThttps://api.cruva.com/automations/toggle

Start 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: Either active (starts the automation) or stopped (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

DELETEhttps://api.cruva.com/automations/delete

Hard-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

GEThttps://api.cruva.com/automations/categories/list

Return 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

POSThttps://api.cruva.com/groups/list

List 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 to 1.
  • page_size: Optional. Number of results per page. Defaults to 25, max 100.
  • 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 (with outreach_audience: "groups") or to /groups/delete.
  • creator_count: Cached affiliate count for the group's filter. May be 0 if 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

POSThttps://api.cruva.com/groups/create

Create 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); set products_and: true to require ALL, products_exclusive: true for 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 of campaign_ids from /automations/list: creators messaged (or not) by those campaigns; messaged_and: true requires ALL, messaged_exclusive: true means 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 arrays gender, 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

DELETEhttps://api.cruva.com/groups/delete

Hard-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

POSThttps://api.cruva.com/lists/create

Create 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 the list_id.
  • list_id: Append to this existing list. Pass either list_id or title.
  • 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 in invalid.
  • include_unmatched: Default false: handles that don't resolve to a known creator are skipped (reported in unmatched). Set true to 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's list_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 unless include_unmatched.
  • invalid: Entries dropped by normalization (illegal characters or over 30 chars).

Lists - List Lists

POSThttps://api.cruva.com/lists/list

Browse 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 to 1.
  • page_size: Optional. Results per page. Defaults to 25, max 100.
  • title: Optional. Exact list name, case-insensitive — this is how you resolve a name to the list_id that every other list endpoint and an automation's list_ids take. 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 with title or use on its own.
  • sort_by: Optional. One of created_at (default), title, affiliate_count, or email_count.
  • sort_direction: Optional. asc or desc (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's list_ids.
  • title: The list's name, as shown in the dashboard. Editable via POST /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

POSThttps://api.cruva.com/lists/get

Fetch 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. 404 when the list does not exist or belongs to another shop.
  • identity: Optional. Narrows to the members a channel can reach: handle for DM/invite outreach, email for email campaigns. Defaults to every member.
  • page / page_size: Optional. Defaults 1 / 25, max page size 100.
  • sort_by: Optional. One of affiliate (handle), email, followers, gmv, engagement, or created_at (default).
  • sort_direction: Optional. asc or desc (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: Always false. 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

POSThttps://api.cruva.com/lists/rename

Rename 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. 404 when 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

POSThttps://api.cruva.com/lists/merge

Union 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_id values 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

POSThttps://api.cruva.com/lists/remove

Remove 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, pass handles) or email (pass emails).
  • handles / emails: Entries to remove. Handles are normalized the same way as on create, so @CreatorOne matches creatorone.

Response

{
"data": {
"message": "Entries removed.",
"removed_count": 2
}
}

Lists - Delete List

DELETEhttps://api.cruva.com/lists/delete

Hard-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/list or /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.

Tags - List Tags

POSThttps://api.cruva.com/tags/list

List the shop's tags with how many creators carry each, or pass a handle to get one creator's tags.

Request Body

{
"search": "tier" // optional, case-insensitive substring
}

Parameters:

  • search: Optional. Case-insensitive substring to narrow tag names.
  • handle: Optional. A TikTok handle (with or without @); when given, the response is that creator's tags instead of the shop summary. 404 if the creator is not in the CRM.

Response

{
"data": {
"results": [
{ "tag": "Gold Tier 30%", "creator_count": 42 },
{ "tag": "Silver Tier 25%", "creator_count": 118 }
],
"total_count": 2
}
}

Response

{
"data": {
"handle": "creatorone",
"creator_oecuid": "7400000000000000001",
"tags": ["Gold Tier 30%", "VIP"]
}
}

Tags - Tag Creators

POSThttps://api.cruva.com/tags/add

Add one or more tags to one or more creators. Re-tagging a creator that already has the tag is a no-op, so the call is safe to repeat.

Request Body

{
"tags": ["VIP"],
"handles": ["@creatorone", "creator.two"] // up to 10,000 per call
}

Parameters:

  • tags: Tag names to add, 1–60 characters each. Case is kept as given.
  • handles: TikTok handles to tag, with or without @. Must be in the shop's CRM — unknown handles come back in unmatched.

Response

{
"data": {
"message": "Tags added.",
"tags": ["VIP"],
"tagged_count": 2, // creators matched in the CRM
"added_count": 1, // new tag rows; the rest already had the tag
"unmatched": [], // handles not in the CRM
"invalid_handles": [],
"invalid_tags": []
}
}

Tags - Remove Tags

POSThttps://api.cruva.com/tags/remove

Take tags off specific creators, or — when handles is omitted — delete the tags from every creator in the shop.

Request Body

{
"tags": ["VIP"],
"handles": ["@creatorone"] // omit entirely to remove the tag from everyone
}

Parameters:

  • tags: Tag names to remove (exact, case-sensitive).
  • handles: Optional. Creators to untag. Omit to delete the tag shop-wide; an empty array is rejected with 400 so a shop-wide delete is always explicit.

Response

{
"data": {
"message": "Tags removed.",
"tags": ["VIP"],
"removed_count": 1,
"unmatched": [],
"invalid_handles": []
}
}

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

POSThttps://api.cruva.com/workflows/list

List 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 to 1.
  • page_size: Optional. Defaults to 25, max 100.
  • 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

POSThttps://api.cruva.com/workflows/get

One 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

POSThttps://api.cruva.com/workflows/create

Create 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) or active. 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}. next is the id of the first step (or null for an empty workflow). For creator_matches, config is 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/get can be passed as-is; triggered counters 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

POSThttps://api.cruva.com/workflows/toggle

Set 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: active or paused.

Response

{
"data": { "message": "Workflow set to paused" }
}

Workflows - Delete Workflow

DELETEhttps://api.cruva.com/workflows/delete

Delete 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. 404 if 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

POSThttps://api.cruva.com/creator-briefs/create

Create 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. name is the internal label; headline is the page title creators see.
  • brief_type: static (fixed page, default) or dynamic (per-product live content — creators pick a product and see its top videos/hooks, always current).
  • video_mode: Static briefs only: manual (you provide video_urls) or auto (the page features the shop's top videos per dynamic_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 (default true) or a products id array (required for dynamic when `all_products` is false); video_count 1-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

POSThttps://api.cruva.com/creator-briefs/update

Partial 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 to video_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

POSThttps://api.cruva.com/creator-briefs/list

List 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. Defaults 1 / 25, max page size 100. 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, or dynamic_content.video_count for auto/dynamic briefs.

Creator Briefs - Delete Brief

DELETEhttps://api.cruva.com/creator-briefs/delete

Hard-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

POSThttps://api.cruva.com/community/campaigns/list

List 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 to 1.
  • page_size: Optional. Number of results per page. Defaults to 25, max 100.
  • search: Optional. Case-insensitive substring match against the campaign title.
  • status: Optional. One of active, paused, or completed.
  • campaign_type: Optional. One of race, contest, leaderboard, bingo, sweepstakes, or retainer.

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: true means the campaign is invite-only — creators apply and wait in pending until approved via /community/campaigns/participants/set_status.
  • payout_type: cash, prize, or both — under both each tier pays cash, awards a prize, or does both (mixed freely across tiers).
  • content_types / content_type: content_types is the multi-select source of truth (any combination of video, LIVE, photo — photo = TikTok photo-mode slideshow). content_type is the legacy single-value mirror; any combination collapses to both (historically video + LIVE).
  • total_gmv / total_spent / videos: Lifetime campaign totals (GMV generated, payouts recorded, videos posted).

Community - Get Campaign

POSThttps://api.cruva.com/community/campaigns/get

Fetch 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

POSThttps://api.cruva.com/community/campaigns/create

Create 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 required tiers shape (expand tiers shape by campaign_type).
  • payout_type: "cash" (Stripe payouts), "prize" (physical prizes), or "both" — under both, 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 to cash.
  • tiers: Prize/payout ladder. The shape depends on campaign_type — expand the table below. Cash tiers set payout; prize tiers set prizes: [{ "title": "AirPods Pro", "image_path": "..." }]; under payout_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 (cash drops the prize rows, prize clears the cash amount); input-only, never stored. Blank prize rows are dropped.
  • content_types / progress_metric: content_types is the kinds of shoppable post the campaign counts — any combination of video, LIVE, photo (TikTok photo-mode slideshow); default ["video"]. The legacy single-value content_type is still accepted (both = video + LIVE). Not every combo is valid per type — expand allowed content kinds & metrics below. progress_metric is ignored for sweepstakes (scored by point_rules) and defaults to posting for retainers.
  • require_approval: When true the campaign is invite-only: creators apply into a pending queue and must be approved via /community/campaigns/participants/set_status before they count.
  • budget / hide_budget_card / grace_period: budget is only enforced on cash-bearing contests (cash/both; stored as 0 for every other type — a race's pool lives in tiers[0].payout). hide_budget_card hides 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 (default 3) 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 (default true). When false, 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 when require_approval is on; every action is best-effort and never blocks a join). assign_roles + roles: [{ "role_id", "role_name" }] grants guild roles; channel_enabled + channel_id opens a channel (member permission overwrite + link on the success screen); dm_enabled + dm_message sends 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_enabled auto-creates the campaign chat channel, channel_posting_policy (open | brand_only) is applied when it's first created, and participant_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 support fixed/recurring only.
  • duration_days / creator_custom_settings / negotiation_enabled / is_private: Retainer-only. duration_days (dynamic, default 30) is each creator's personal period length. creator_custom_settings sets per-creator deals — entries need creator_handle (or creator_handles: [...] to fan one deal out to several creators), videos_required, and payout_amount; incomplete entries are dropped and handles are cleaned of a leading @. negotiation_enabled lets creators propose a custom payout at join time. is_private hides rates and blocks joining except for private_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. reminders goes out via SMS/email; discord_reminders posts to a channel and needs channel_id/channel_name from the shop's Discord integration; lark_reminders posts to the shop's connected Lark group; chat_reminders lands as campaign-chat system messages with destination: "channel" | "dm". Each item needs days_before (integer ≥ 1) and message (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 exactly grid_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

POSThttps://api.cruva.com/community/campaigns/update

Partially 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 of video, LIVE, photo; same per-type rules as create. The legacy content_type mirror 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 sweepstakes winner is never overwritten from the API.
  • config_updates: Optional. Arbitrary config-field overrides (snake_case keys, converted server-side), applied as config.<key> — same vocabulary as create's config_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

POSThttps://api.cruva.com/community/campaigns/set_status

Set 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 of active, paused, or completed.

Response

{
"data": {
"message": "Campaign status set to completed"
}
}

Community - Delete Campaign

DELETEhttps://api.cruva.com/community/campaigns/delete

Permanently 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

POSThttps://api.cruva.com/community/campaigns/participants

List 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. Defaults 1 / 25, max 100.
  • status: Optional filter. One of pending, 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 (no views/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_status and 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). 0 for 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 are 0 for 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's progress_metric (GMV, post count, or views).

Community - Approve / Reject Participant

POSThttps://api.cruva.com/community/campaigns/participants/set_status

Change 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 of approved, rejected, pending, under_review, removed.
  • period_id: "<campaign_id>_<period>" — required for removed on 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

POSThttps://api.cruva.com/community/campaigns/videos

List 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 of gmv, view_count, like_count, comment_count, units_sold, ctr, post_time. Default gmv.
  • 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

POSThttps://api.cruva.com/community/campaigns/lives

List 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 of gmv, views, likes, comments, units_sold, duration, start_time. Default gmv.
  • 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

POSThttps://api.cruva.com/community/stats

Per-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 of campaign (one campaign — requires campaign_id), campaigns (all, default), or retainers.
  • campaign_id: Required when scope is campaign.
  • date_from / date_to: Optional YYYY-MM-DD window, 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. For videos_per_day this 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

POSThttps://api.cruva.com/community/balance

Read 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 - Authorize Payout

POSThttps://api.cruva.com/community/payouts/authorize

Moves real money. Pays a creator exactly what they are owed for a campaign — the server computes the amount from the campaign's own payout rules (the amount_owed shown per creator on /community/campaigns/participants), sends a Stripe transfer to the creator's connected account in the shop's local currency, and marks the participant paid. You never choose the amount: partial or inflated payouts are impossible through this endpoint. Guarded by: nothing-owed rejection (including already-paid creators), the shop's community balance, the creator's Stripe onboarding state, and a once-per-(campaign, creator, period) idempotency check — repeating the call for the same trio returns 400 already paid instead of double-paying. When money becomes payable: races and bingo accrue live; leaderboards require the campaign to be completed (via /community/campaigns/set_status); everything else pays after the campaign/period ends plus the grace window. For payments made outside the platform, use /community/payouts/manual.

Request Body

{
"campaign_id": "6a61ef6bcc6388012e941c8f",
"creator_id": "3f6b2c1e-9a47-4d20-b8c3-2f1a9d7e6c05",
"creator_handle": "sassykittypaw",
"period_id": "6a61ef6bcc6388012e941c8f_2",
"amount": 50.0
}

Parameters:

  • campaign_id: The campaign the payout is for. Obtain from /community/campaigns/list.
  • creator_id: The participant to pay. Obtain from /community/campaigns/participants (which also shows their amount_owed).
  • creator_handle: Optional. Recorded on the transfer for bookkeeping.
  • period_id: Optional — "<campaign_id>_<period>" targets a specific recurring-retainer period; defaults to the latest.
  • amount: Optional, confirmation only. If provided it must equal the owed amount or the request is rejected with the correct figure — it never overrides what's paid. Omit it to pay exactly what's owed.

Response

{
"data": {
"message": "Payout initiated. Balance is deducted when the transfer completes.",
"transfer_id": "tr_3RkQ8x2eZvKYlo2C0aB1cD2e",
"amount": 50.0,
"currency": "usd",
"period": 2,
"marked_paid": true
}
}

Error Responses

// 400 — nothing outstanding for this creator (never earned, or already paid)
{ "error": "Nothing is currently owed to this creator for this campaign period" }
// 400 — campaign hasn't reached its payout window yet
{ "error": "Campaign is not finished yet" }
// 400 — caller-supplied amount doesn't match the owed amount
{ "error": "amount does not match: this creator is owed 50.00. Omit amount to pay exactly what is owed." }
// 400 — balance too low
{ "error": "Insufficient balance. Current balance: 120.00 USD" }
// 400 — replay of an already-sent payout
{ "error": "This creator has already been paid for this campaign" }
// 400 — creator's Stripe account not ready
{ "error": "Creator does not have a Stripe account set up" }
// 400 — leaderboard still live
{ "error": "Leaderboard payouts become available once the campaign is completed" }

Response Fields:

  • amount: What was actually paid — the server-computed owed amount.
  • marked_paid: Whether the participant was recorded as paid after the transfer. If false, the money moved but bookkeeping failed — verify in the dashboard before retrying.

Community - Record Manual Payout

POSThttps://api.cruva.com/community/payouts/manual

Record 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, in currency. Defaults to the creator's outstanding amount_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 API

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 Data

POSThttps://api.cruva.com/affiliate/marketplace/search

Retrieve detailed creator performance metrics and demographics for a specific creator handle.

Request Body

{
"region": "us", // us | uk | mx | es | de | ie | it | fr | nl | pt | gr | be | at | pl | hu | cz | br | jp
"handle": "thysamus"
}

Parameters:

  • region: TikTok Shop market to query. One of us, 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

{
"data": {
"creator_id": "7171717171717171717",
"category": [
"Menswear & Underwear",
"Phones & Electronics",
"Beauty & Personal Care"
],
"top_follower_ages": [
"18-24",
"25-34"
],
"handle": "thysamus",
"nickname": "sam",
"email": null,
"follower_cnt": 16983,
"is_fast_growing": false,
"med_gmv_revenue_range": 1,
"units_sold_range": 1,
"top_follower_gender": "Female",
"video_avg_view_cnt": 0,
"video_engagement": 0,
"spanish": false,
"post_rate": 33.02,
"gender": "male",
"med_gmv_revenue": 0, // total GMV earned across the platform in the last 30 days
"brand_collaborations": 0,
"gmv_range": 0,
"race": "none",
"body_type": "average",
"economic_status": "none",
"age": "teen",
"face_visibility": "frequently",
"tone": "humorous",
"embedding_attempts": 2,
"bio": null,
"language": "english",
"live_gmv_30d": 0,
"pps": null,
"msg_name": "Sam"
}
}

EnterpriseCreator's Brands

POSThttps://api.cruva.com/intelligence/creators/brands/list

Given 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

POSThttps://api.cruva.com/intelligence/creators/products/list

Given 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

POSThttps://api.cruva.com/intelligence/creators/videos/list

Given 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_oecuid and author_id are accepted as aliases.
  • brand_id: *(optional)* Restrict to videos for this brand. shop_id is accepted as an alias.
  • product_id: *(optional)* Restrict to videos featuring this product.
  • page: Page number, ≥ 1. Defaults to 1. page_number is accepted as an alias.
  • page_size: Results per page. Defaults to 10, max 100.
  • sort: One of time_posted, views, likes, comments, gmv. Defaults to gmv.
  • sort_direction: asc or desc. Defaults to desc.
  • is_ad: *(optional)* Filter to videos where is_ad = true/false. Unset returns both.
  • is_ai: *(optional)* Filter to videos where is_ai = true/false. Unset returns both.
  • ts_start: *(optional)* YYYY-MM-DD lower bound on time_posted.
  • ts_end: *(optional)* YYYY-MM-DD upper bound on time_posted.
  • ts_days: *(optional)* If only one of ts_start/ts_end is provided, widens the missing side by this many days. Defaults to 7. Only applied when at least one of ts_start/ts_end is 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's Creators

POSThttps://api.cruva.com/intelligence/brands/creators/list

Given 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

POSThttps://api.cruva.com/intelligence/brands/videos/list

Given 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_id is accepted as an alias.
  • page: Page number, ≥ 1. Defaults to 1. page_number is accepted as an alias.
  • page_size: Results per page. Defaults to 10, max 100.
  • sort: One of time_posted, views, likes, comments, gmv. Defaults to gmv.
  • sort_direction: asc or desc. Defaults to desc.
  • is_ad: *(optional)* Filter to videos where is_ad = true/false. Unset returns both.
  • is_ai: *(optional)* Filter to videos where is_ai = true/false. Unset returns both.
  • ts_start: *(optional)* YYYY-MM-DD lower bound on time_posted.
  • ts_end: *(optional)* YYYY-MM-DD upper bound on time_posted.
  • ts_days: *(optional)* If only one of ts_start/ts_end is provided, widens the missing side by this many days. Defaults to 7. Only applied when at least one of ts_start/ts_end is 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's Creators

POSThttps://api.cruva.com/intelligence/products/creators/list

Given 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

POSThttps://api.cruva.com/intelligence/products/videos/list

Given 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 to 1. page_number is accepted as an alias.
  • page_size: Results per page. Defaults to 10, max 100.
  • sort: One of time_posted, views, likes, comments, gmv. Defaults to gmv.
  • sort_direction: asc or desc. Defaults to desc.
  • is_ad: *(optional)* Filter to videos where is_ad = true/false. Unset returns both.
  • is_ai: *(optional)* Filter to videos where is_ai = true/false. Unset returns both.
  • ts_start: *(optional)* YYYY-MM-DD lower bound on time_posted.
  • ts_end: *(optional)* YYYY-MM-DD upper bound on time_posted.
  • ts_days: *(optional)* If only one of ts_start/ts_end is provided, widens the missing side by this many days. Defaults to 7. Only applied when at least one of ts_start/ts_end is 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"
}
]
}
}