# DansUGC - Complete AI Integration Guide > Authentic UGC reaction videos from real human creators, not AI. REST API + MCP server for programmatic access. This document is designed for LLMs and AI agents integrating with DansUGC. ## Quick Reference - Base URL: `https://dansugc.com/api/v1` - MCP Endpoint: `https://dansugc.com/api/mcp` - Auth: `Authorization: Bearer dsk_your_key` - Rate Limit: 60 req/min per key - OpenAPI Spec: `https://dansugc.com/api/openapi.json` - Interactive Docs: `https://dansugc.com/docs` --- ## What is DansUGC? DansUGC is a B2B marketplace connecting brands with verified UGC (User Generated Content) creators who film authentic reaction videos. Two products: 1. **B-Roll Library** — 2,000+ pre-recorded reactions and images. Instant download. From $3/video. 2. **Custom Video Orders** — 35+ verified creators film to your brief. Delivered in 2-3 business days. From $8/video. Every video is filmed by a real, verified human. No AI-generated content, deepfakes, or synthetic voices. --- ## REST API — Complete Reference ### Authentication All requests require a Bearer token in the `Authorization` header. Keys are prefixed with `dsk_` and carry permission scopes. ``` Authorization: Bearer dsk_abc123... ``` **Scopes:** - `broll:read` — Browse and search videos - `broll:purchase` — Purchase videos and get download URLs - `broll:billing` — View purchase history and billing - `keys:manage` — Create, list, and revoke per-customer API keys - `orders:read` — List and read custom orders (creator catalog, format catalog, order detail) - `orders:write` — Create custom orders and open Stripe Checkout sessions - `pricing:read` — Read the canonical tier table and compute stateless quotes - `deliverables:read` — Fetch signed-URL delivery manifests for completed orders Get your API key at: https://dansugc.com/dashboard/api-keys ### Endpoints | Method | Endpoint | Scope | Description | |--------|----------|-------|-------------| | GET | /broll | broll:read | List & search B-roll videos | | GET | /broll/:id | broll:read | Get single video details with pricing | | POST | /broll/purchase | broll:purchase | Purchase videos, returns download URLs | | GET | /broll/purchases | broll:billing | List purchases for billing reconciliation | | GET | /broll/filters | broll:read | Get all unique filterable values for building filter UIs | | POST | /keys | keys:manage | Create per-customer API key | | GET | /keys | keys:manage | List customer keys | | DELETE | /keys?id=:id | keys:manage | Revoke a customer key | | GET | /models | orders:read | List verified creators (custom orders) | | GET | /models/:id | orders:read | Single creator detail | | GET | /formats | orders:read | List TikTok-style format templates | | GET | /pricing/tiers | pricing:read | Canonical tier table + multipliers + add-on rates | | POST | /pricing/quote | pricing:read | Stateless quote — per-creator subtotals and grand total | | POST | /orders | orders:write | Create one or more custom orders, returns Stripe Checkout URL | | GET | /orders | orders:read | List custom orders scoped to your API key | | GET | /orders/:id | orders:read | Single custom order with status, payment state, items | | GET | /orders/:id/files | deliverables:read | Delivery manifest with 15-min signed URLs | --- ### GET /broll — List & Search Videos **Search Parameters (mutually exclusive):** - `search` — Full-text search: `?search=happy+reaction` - `semantic_search` — AI-powered natural language search: `?semantic_search=woman+laughing+indoors` - You CANNOT use both `search` and `semantic_search` in the same request (returns 400) **Filter Parameters:** - `emotion` — happy, surprised, excited, crying, angry, confused, shocked, laughing, sad, neutral, scared, disgusted - `gender` — male, female, mixed, non-binary, other - `location` — indoor, outdoor, office, beach, kitchen, bathroom, car, gym, bedroom, living_room - `age_range` — 18-24, 25-34, 35-44, 45-54, 55+ - `hair_color` — blonde, brunette, black, red, gray, other - `outfit` — casual, formal, athletic, swimwear, other - `media_type` — video, image - `model_id` — Filter by specific creator UUID - `difficulty` — Exact difficulty level (1-10) - `min_difficulty` / `max_difficulty` — Difficulty range - `min_virality` — Minimum virality score (0-100) - `featured` — true/false - `is_daily_drop` — true/false **Pagination & Sorting:** - `page` — Page number (default: 1) - `limit` — Results per page (default: 24, max: 100) - `sort_by` — created_at, virality_score, download_count, difficulty_level, purchase_count - `sort_order` — asc, desc (default: desc) #### Example Request ``` GET /api/v1/broll?semantic_search=excited+woman+opening+package&gender=female&media_type=video&limit=5 Authorization: Bearer dsk_abc123 ``` #### Example Response ```json { "videos": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Excited Unboxing Reaction", "description": "Woman excitedly opens a package with genuine surprise", "preview_url": "https://storage.example.com/preview/watermarked-550e8400.mp4", "thumbnail_url": "https://storage.example.com/thumb/550e8400.jpg", "emotion": "excited", "gender": "female", "location": "indoor", "age_range": "25-34", "hair_color": "brunette", "outfit": "casual", "media_type": "video", "difficulty_level": 3, "virality_score": 78, "purchase_count": 42, "price": 20, "tier_name": "Script Reading / App Demo", "similarity": 0.89, "model": { "id": "creator-uuid", "name": "Sarah M.", "slug": "sarah-m" }, "created_at": "2026-02-15T10:30:00Z" } ], "pagination": { "page": 1, "limit": 5, "total_pages": 12, "has_more": true }, "total": 58, "search_type": "hybrid", "filters": { "emotions": ["happy", "surprised", "excited", "crying", "angry", "confused"], "genders": ["male", "female", "mixed"], "locations": ["indoor", "outdoor", "office", "beach"], "age_ranges": ["18-24", "25-34", "35-44", "45-54", "55+"], "media_types": ["video", "image"] }, "pricing_note": "Prices shown are per-video. Purchase via POST /broll/purchase." } ``` **Important notes for AI agents:** - `preview_url` is watermarked — for preview only, not for final use - `download_url` is only returned after purchase - `similarity` field only present in semantic/hybrid search results - `price` reflects the video's difficulty tier pricing - Images are always $3 flat regardless of tier --- ### GET /broll/:id — Single Video Details ``` GET /api/v1/broll/550e8400-e29b-41d4-a716-446655440000 Authorization: Bearer dsk_abc123 ``` Returns the same video object as the list endpoint but with full details. --- ### POST /broll/purchase — Purchase Videos Purchase one or more videos by ID. Returns full-quality download URLs. #### Request ```json { "video_ids": [ "550e8400-e29b-41d4-a716-446655440000", "660e8400-e29b-41d4-a716-446655440001" ] } ``` #### Response ```json { "purchases": [ { "video_id": "550e8400-e29b-41d4-a716-446655440000", "download_url": "https://storage.example.com/full/550e8400.mp4", "price_paid": 20, "currency": "USD", "purchased_at": "2026-03-15T14:30:00Z" }, { "video_id": "660e8400-e29b-41d4-a716-446655440001", "download_url": "https://storage.example.com/full/660e8400.mp4", "price_paid": 8, "currency": "USD", "purchased_at": "2026-03-15T14:30:00Z" } ], "total_charged": 28, "currency": "USD", "balance_remaining": 472 } ``` **Important:** - Purchases are idempotent — re-purchasing the same video returns the existing download URL at no extra cost - `video_ids` array max: 50 per request - Credits are deducted atomically — if insufficient balance, no videos are purchased - Download URLs are permanent and do not expire --- ### GET /broll/purchases — Purchase History For billing reconciliation and purchase tracking. **Parameters:** - `start_date` / `end_date` — Date range filter (ISO 8601) - `search` — Text search across purchased video titles - `semantic_search` — Semantic search across purchased videos - `emotion`, `gender`, `location` — Filter purchased videos by attributes - `sort_by` — purchased_at, price_paid (default: purchased_at) - `sort_order` — asc, desc (default: desc) - `api_key_id` — Filter by specific customer key (root keys only) - `page` / `limit` — Pagination (max limit: 500) #### Example Request ``` GET /api/v1/broll/purchases?start_date=2026-03-01&end_date=2026-03-31&limit=100 Authorization: Bearer dsk_abc123 ``` #### Example Response ```json { "purchases": [ { "id": "purchase-uuid", "video_id": "550e8400-e29b-41d4-a716-446655440000", "title": "Excited Unboxing Reaction", "download_url": "https://storage.example.com/full/550e8400.mp4", "price_paid": 20, "purchased_at": "2026-03-15T14:30:00Z", "emotion": "excited", "media_type": "video" } ], "summary": { "total_purchases": 47, "total_amount": 892, "currency": "USD", "period_start": "2026-03-01", "period_end": "2026-03-31" }, "pagination": { "page": 1, "limit": 100, "total_pages": 1, "has_more": false } } ``` --- ### GET /broll/filters — Available Filter Values Returns all unique values for every filterable field in the B-roll library. Call this once to know exactly which filter values exist, then use them in `/broll` queries. Useful for building dynamic filter UIs. **No parameters required** — just authentication. #### Example Request ``` GET /api/v1/broll/filters Authorization: Bearer dsk_abc123 ``` #### Example Response ```json { "filters": { "emotions": ["angry", "confused", "crying", "excited", "happy", "laughing", "neutral", "sad", "scared", "shocked", "surprised"], "genders": ["female", "male", "mixed", "non-binary"], "locations": ["bathroom", "beach", "bedroom", "car", "gym", "indoor", "kitchen", "living_room", "office", "outdoor"], "age_ranges": ["18-24", "25-34", "35-44", "45-54", "55+"], "hair_colors": ["black", "blonde", "brunette", "gray", "red"], "outfits": ["athletic", "casual", "formal", "swimwear"], "media_types": ["image", "video"], "difficulty_levels": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } } ``` **For AI agents:** Call this endpoint first to discover what filter values are available, then use those exact values when calling `/broll` with filters. This avoids passing invalid filter values that would return empty results. --- ### Per-Customer API Keys (Platform Integration) If you are a platform serving multiple customers, you MUST use per-customer keys. This ensures: - Purchase isolation: each customer sees only their own purchases - Deduplication: re-purchases are tracked per customer, not per root key - Billing: reconcile usage per customer via `api_key_id` filter #### POST /keys — Create Customer Key ```json { "name": "Customer: Acme Corp", "scopes": ["broll:read", "broll:purchase"], "customer_id": "acme-123" } ``` Response: ```json { "id": "key-uuid", "key": "dsk_cust_abc123...", "name": "Customer: Acme Corp", "scopes": ["broll:read", "broll:purchase"], "customer_id": "acme-123", "created_at": "2026-03-15T10:00:00Z" } ``` **IMPORTANT:** The `key` value is only returned once at creation. Store it securely. #### GET /keys — List Customer Keys ``` GET /api/v1/keys Authorization: Bearer dsk_root_key ``` #### DELETE /keys — Revoke a Key ``` DELETE /api/v1/keys?id=key-uuid Authorization: Bearer dsk_root_key ``` --- ## Custom Ordering API — Complete Reference The Custom Ordering API places real custom UGC video orders against our roster of 35+ verified human creators — the same surface that powers `dansugc.com/order/new`. You catalogue creators and formats, build a quote, submit an order with one or more `model_ids`, and receive a Stripe Checkout URL to hand to your end-user. Once paid, the order enters our fulfillment pipeline and your platform polls for status and grabs deliverables via signed URLs. This is the partner-facing surface for **commissioned video** (filmed to your brief over 2–3 business days). For the instant-download library of pre-recorded clips, use the B-Roll API above. ### Authentication Same `dsk_` Bearer token as the B-Roll API — DansUGC uses a single unified key model across both products. ``` Authorization: Bearer dsk_abc123... ``` **Scopes used by this API:** - `orders:read` — list models, formats, and orders - `orders:write` — create orders - `pricing:read` — read tiers and compute quotes - `deliverables:read` — fetch delivery manifests with signed URLs A single key with `orders:read orders:write pricing:read deliverables:read` covers the full integration. The same 60 req/min rate-limit bucket applies — driving both B-Roll and Custom Orders from one key shares that budget. ### Endpoints | Method | Endpoint | Scope | Description | |--------|----------|-------|-------------| | GET | /models | orders:read | List verified creators with filters and pagination | | GET | /models/:id | orders:read | Single creator detail including `wont_do`, `has_devices`, and full `video_examples[]` | | GET | /formats | orders:read | List TikTok-style format templates with tier and base price | | GET | /pricing/tiers | pricing:read | Canonical tier table, creator-type multipliers, add-on rates | | POST | /pricing/quote | pricing:read | Stateless quote — `creators[]` per creator + `grand_total` | | POST | /orders | orders:write | Create one or more orders, returns shared Stripe Checkout URL | | GET | /orders | orders:read | List orders scoped to your API key | | GET | /orders/:id | orders:read | Single order with status, payment state, items, deliverables | | GET | /orders/:id/files | deliverables:read | Delivery manifest with 15-min signed URLs | All endpoints are prefixed with `https://dansugc.com/api/v1`. All responses are JSON. Money fields are **decimal strings** (`"199.50"`) — never floats. Preserve them as strings to avoid IEEE-754 drift. --- ### GET /v1/models — List Creators **Scope:** `orders:read` List verified creators. Use this to populate your catalog UI. **Query parameters:** | Param | Type | Description | |-------|------|-------------| | `creator_type` | enum | `standard` \| `viral` \| `couples` | | `gender` | string | `female` \| `male` \| `couple` \| `other` | | `niche` | string | Filter by niche slug (e.g. `beauty`, `wellness`, `tech`) | | `language` | string | ISO 639-1 code (e.g. `en`, `es`) | | `country` | string | ISO 3166-1 alpha-2 code (e.g. `US`, `GB`) | | `search` | string | Full-text search across name and bio | | `page` | int | 1-indexed, default `1` | | `limit` | int | Default `24`, max `100` | | `cursor` | string | Reserved for cursor-paginated v1.1 — use `page` in v1 | #### Example request ``` GET /api/v1/models?creator_type=viral&gender=female&limit=2 Authorization: Bearer dsk_abc123 ``` #### Example response ```json { "data": [ { "id": "9c3f8f80-1f6a-4b7e-9e7c-2c2a9d2a6b0e", "name": "Sarah M.", "slug": "sarah-m", "creator_type": "viral", "multiplier": "1.50", "gender": "female", "age_range": "25-34", "languages": ["en"], "niches": ["beauty", "wellness"], "cover_photo_url": "https://cdn.dansugc.com/models/sarah-m/cover.jpg", "video_examples": [ { "id": "ex-1", "video_url": "https://cdn.dansugc.com/models/sarah-m/ex1.mp4", "thumbnail_url": "https://cdn.dansugc.com/models/sarah-m/ex1.jpg", "featured": true, "format_id": 412 } ], "photo_unit_price": "35.00" } ], "pagination": { "page": 1, "limit": 2, "total_pages": 9, "has_more": true } } ``` **Fields to know:** - `multiplier` — multiply this against any tier base price to get the unit price for videos by this creator. - `photo_unit_price` — the creator's own photo rate. Photos do **not** use the creator-type multiplier; this field already encodes the per-creator photo price. - `video_examples[]` — preview clips you can show in your UI to help end-users pick a creator. --- ### GET /v1/models/{id} — Single Creator **Scope:** `orders:read` Returns one creator with the full `video_examples` array, plus partner-only fields (`wont_do`, `has_devices`) used to filter briefs against creator policy. #### Example response ```json { "data": { "id": "9c3f8f80-1f6a-4b7e-9e7c-2c2a9d2a6b0e", "name": "Sarah M.", "slug": "sarah-m", "creator_type": "viral", "multiplier": "1.50", "gender": "female", "age_range": "25-34", "languages": ["en"], "niches": ["beauty", "wellness"], "cover_photo_url": "https://cdn.dansugc.com/models/sarah-m/cover.jpg", "video_examples": [ { "id": "ex-1", "video_url": "https://cdn.dansugc.com/models/sarah-m/ex1.mp4", "thumbnail_url": "https://cdn.dansugc.com/models/sarah-m/ex1.jpg", "featured": true, "format_id": 412 } ], "photo_unit_price": "35.00", "wont_do": ["alcohol", "gambling"], "has_devices": ["iphone_15_pro", "ring_light", "lavalier_mic"] } } ``` `wont_do` lists categories the creator has opted out of. Submitting a brief in one of these categories will be rejected at order-create time. `has_devices` is informational — useful if your brief requires a specific phone model or piece of gear. **404 response:** ```json { "error": { "code": "not_found", "message": "Model not found" } } ``` --- ### GET /v1/formats — List Format Templates **Scope:** `orders:read` List the curated catalog of TikTok-style format templates. Each format has a `tier` that drives base pricing and a `min_order_videos` floor. **Query parameters:** | Param | Type | Description | |-------|------|-------------| | `difficulty` | int 1–10 | Exact tier level | | `min_difficulty` / `max_difficulty` | int | Tier range | | `niche` | string | Filter by niche slug | | `category` | string | Filter by category slug | | `search` | string | Full-text search across format name and TikTok caption | | `page` / `limit` | int | Pagination — default 24, max 100 | #### Example request ``` GET /api/v1/formats?difficulty=5&limit=1 Authorization: Bearer dsk_abc123 ``` #### Example response ```json { "data": [ { "id": 412, "display_name": "Honest Skincare Reaction", "title": "you HAVE to try this serum", "username": "creator_handle", "tiktok_url": "https://www.tiktok.com/@creator_handle/video/...", "thumbnail_url": "https://cdn.dansugc.com/formats/412.jpg", "video_url": "https://cdn.dansugc.com/formats/412.mp4", "difficulty": 5, "tier": { "level": 5, "name": "The \"Problem/Solution\"", "base_price": "55.00" }, "min_order_videos": 5, "niche": "beauty", "category": "reaction" } ], "pagination": { "page": 1, "limit": 1, "total_pages": 4, "has_more": true } } ``` `min_order_videos` defaults to 5 when not set on the format. Orders that submit fewer than this for a given format are rejected with `format_minimum_not_met`. --- ### GET /v1/pricing/tiers — Static Pricing Payload **Scope:** `pricing:read` Returns the canonical pricing tier table, creator-type multipliers, and add-on rates. Cache this client-side — values change at most a few times per year and are version-stamped via the `ETag` response header. #### Example response ```json { "data": { "tiers": [ { "level": 1, "name": "Basic Face Reaction", "base_price": "8.00", "earnings": "4.00" }, { "level": 2, "name": "Lipsync / Singing", "base_price": "15.00", "earnings": "7.50" }, { "level": 3, "name": "Script Reading / App Demo", "base_price": "20.00", "earnings": "10.00" }, { "level": 4, "name": "Reaction + App Demo", "base_price": "30.00", "earnings": "15.00" }, { "level": 5, "name": "The \"Problem/Solution\"", "base_price": "55.00", "earnings": "27.50" }, { "level": 6, "name": "Aesthetic Lifestyle", "base_price": "85.00", "earnings": "42.50" }, { "level": 7, "name": "High-Conversion Ad", "base_price": "120.00", "earnings": "60.00" }, { "level": 8, "name": "Product Deep Dive", "base_price": "160.00", "earnings": "80.00" }, { "level": 9, "name": "Brand Storytelling", "base_price": "205.00", "earnings": "102.50" }, { "level": 10, "name": "Premium Campaign", "base_price": "250.00", "earnings": "125.00" } ], "creator_types": [ { "slug": "standard", "name": "Standard Creator", "multiplier": "1.00" }, { "slug": "viral", "name": "Viral Creator", "multiplier": "1.50" }, { "slug": "couples", "name": "Couples", "multiplier": "2.00" } ], "addons": { "video_editing": { "price_per_video": "20.00" }, "video_demo": { "price_per_video": "20.00" } }, "talking_head_words_per_minute": 150 } } ``` `earnings` is the creator payout at the base multiplier — exposed for transparency, not load-bearing for partner pricing. --- ### POST /v1/pricing/quote — Stateless Quote **Scope:** `pricing:read` Returns a fully-computed quote with one `sub_quote` per creator. Use this to render a price preview before committing the user to an order. **Result is not reserved** — it's a pure calculation. Re-quote before submitting if anything in your UI changes. #### Request body ```json { "model_ids": [ "9c3f8f80-1f6a-4b7e-9e7c-2c2a9d2a6b0e", "a1b2c3d4-e5f6-7890-1234-567890abcdef" ], "items": [ { "format_id": 412, "quantity": 5, "scripts": [] } ], "addons": { "video_editing": true, "video_demo": false, "photos": 3 } } ``` #### Example response ```json { "data": { "creators": [ { "model_id": "9c3f8f80-1f6a-4b7e-9e7c-2c2a9d2a6b0e", "model_name": "Sarah M.", "multiplier": "1.50", "format_lines": [ { "format_id": 412, "display_name": "Honest Skincare Reaction", "quantity": 5, "unit_price": "82.50", "subtotal": "412.50" } ], "editing_subtotal": "100.00", "demo_subtotal": "0.00", "photo_unit_price": "35.00", "photo_subtotal": "105.00", "total": "617.50" }, { "model_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "model_name": "Jane D.", "multiplier": "1.00", "format_lines": [ { "format_id": 412, "display_name": "Honest Skincare Reaction", "quantity": 5, "unit_price": "55.00", "subtotal": "275.00" } ], "editing_subtotal": "100.00", "demo_subtotal": "0.00", "photo_unit_price": "25.00", "photo_subtotal": "75.00", "total": "450.00" } ], "total_videos": 10, "grand_total": "1067.50", "currency": "USD" } } ``` **Tier 3 talking-head billing.** For formats at difficulty level 3 (script reading / app demo), if you include a `scripts` array on the line item the order is billed by the **longest** script in the bundle: ``` billed_minutes = max(1, ceil(max(word_count(s)) for s in scripts) / 150) unit_price = base_price[3] × multiplier × billed_minutes ``` 150 words per minute is the standard talking-head delivery rate. A 220-word script becomes a 2-minute video billed at `$20 × multiplier × 2`. Scripts under 150 words bill at the 1-minute floor. When `scripts` is present, the response includes `billed_minutes` on each Tier-3 format line. **400 — minimum not met:** ```json { "error": { "code": "format_minimum_not_met", "message": "Format \"Honest Skincare Reaction\" requires at least 5 videos per creator (got 3)", "format_id": 412, "min_required": 5 } } ``` --- ### POST /v1/orders — Create Order **Scope:** `orders:write` Creates one fulfillment row per `model_ids` entry, then opens a **single shared Stripe Checkout session** that covers all rows. The response includes the payment URL. #### Request headers - `Authorization: Bearer dsk_...` (required) - `Content-Type: application/json` (required) - `Idempotency-Key: ` (strongly recommended — see Idempotency below) #### Request body ```json { "model_ids": ["9c3f8f80-1f6a-4b7e-9e7c-2c2a9d2a6b0e"], "customer_email": "buyer@acme.com", "promotion_details": "Honest reaction to our new skincare serum. Mention 'glow' once.", "additional_instructions": "Vertical 9:16. Hook in first 1.5s. No music — voiceover only.", "items": [ { "format_id": 412, "quantity": 5, "scripts": [] } ], "addons": { "video_editing": false, "video_editing_notes": null, "video_demo": false, "video_demo_notes": null, "photos": 0, "photo_notes": null }, "external_reference": "order_acme_2026_001", "payment_method": "stripe_checkout", "expected_total": "617.50" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `model_ids` | string[] | yes | One or more creator UUIDs. Length ≥ 1. Each creates a separate order row. | | `customer_email` | string | yes | End-user email — used for Stripe receipt and creator-side comms. | | `promotion_details` | string | yes | Free-form brief shown to the creator. | | `additional_instructions` | string | no | Format/style notes (aspect ratio, hook timing, etc.). | | `items` | object[] | yes | Format line items. Each entry needs `format_id` and `quantity`. Optional `scripts[]` for Tier-3. | | `addons.video_editing` | boolean | no | `$20/video` editing add-on. | | `addons.video_demo` | boolean | no | `$20/video` product-demo add-on. | | `addons.photos` | int | no | Photo quantity. Billed at each creator's `photo_unit_price`. | | `addons.video_editing_notes` | string \| null | no | Editing direction. | | `addons.video_demo_notes` | string \| null | no | Demo direction. | | `addons.photo_notes` | string \| null | no | Photo direction. | | `external_reference` | string | no | Your reconciliation handle, ≤ 128 chars. Stored on every created order row. | | `payment_method` | string | no | Only `"stripe_checkout"` in v1. Defaults to that. | | `expected_total` | string | no | If provided, the server compares to the computed `grand_total` and rejects with `total_mismatch` if they differ. Use to guard against silent pricing drift. | #### Sample response (multi-model — 2 creators, 1 payment) ```json { "data": { "orders": [ { "id": "ord_01HW3KJ8E9K0YR6Z3T1F8XQ2A0", "model_id": "9c3f8f80-1f6a-4b7e-9e7c-2c2a9d2a6b0e", "model_name": "Sarah M.", "status": "pending_payment", "payment_status": "unpaid", "total_amount": "617.50", "total_videos": 5, "items": [ { "format_id": 412, "video_type_name": "Honest Skincare Reaction", "quantity": 5, "unit_price": "82.50" } ], "external_reference": "order_acme_2026_001", "created_at": "2026-05-29T14:00:00Z" }, { "id": "ord_01HW3KJ8E9K0YR6Z3T1F8XQ2A1", "model_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "model_name": "Jane D.", "status": "pending_payment", "payment_status": "unpaid", "total_amount": "450.00", "total_videos": 5, "items": [ { "format_id": 412, "video_type_name": "Honest Skincare Reaction", "quantity": 5, "unit_price": "55.00" } ], "external_reference": "order_acme_2026_001", "created_at": "2026-05-29T14:00:00Z" } ], "checkout_session_id": "cs_test_a1B2c3...", "payment_url": "https://checkout.stripe.com/c/pay/cs_test_a1B2c3...", "expires_at": "2026-05-30T14:00:00Z", "grand_total": "1067.50", "currency": "USD" } } ``` **Order ID format.** Order IDs are returned with the `ord_` prefix (e.g. `ord_01HW3KJ8E9K0YR6Z3T1F8XQ2A0`). The `GET /v1/orders/{id}` endpoint accepts either the prefixed form or the raw UUID — pick one and stay consistent. **Multi-model = one payment.** Every `model_id` you pass receives the **same** `items`, `promotion_details`, and `additional_instructions`. The orders share one Stripe Checkout session — your end-user clicks pay once and all rows advance together. You cannot ask creator A for format 412 and creator B for format 587 in a single POST — submit two orders. (Mixed-format-across-creators is on the v1.2 roadmap.) **Until paid:** rows sit at `status: "pending_payment"`. Abandoned checkouts auto-cancel after 24 hours. #### Validation errors ```json { "error": { "code": "missing_field", "message": "customer_email is required" } } { "error": { "code": "invalid_model", "message": "Model 9c3f... not found or not bookable" } } { "error": { "code": "invalid_format", "message": "Format 999 not found" } } { "error": { "code": "format_minimum_not_met", "message": "...", "format_id": 412, "min_required": 5 } } { "error": { "code": "total_mismatch", "message": "expected_total 617.50 does not match computed grand_total 1067.50" } } ``` --- ### GET /v1/orders/{id} — Single Order **Scope:** `orders:read` Accepts the `ord_` prefixed form or the raw UUID. Returns the order with current `status` (partner-facing 4-state), payment state, items, and — once delivered — the `deliverables` summary. #### Example response ```json { "data": { "id": "ord_01HW3KJ8E9K0YR6Z3T1F8XQ2A0", "model_id": "9c3f8f80-1f6a-4b7e-9e7c-2c2a9d2a6b0e", "model_name": "Sarah M.", "status": "in_progress", "payment_status": "paid", "total_amount": "617.50", "total_videos": 5, "items": [ { "format_id": 412, "video_type_name": "Honest Skincare Reaction", "quantity": 5, "unit_price": "82.50", "creator_earnings_per_video": "41.25" } ], "addons": { "video_editing": true, "video_editing_notes": "Match cuts to beat drops", "video_demo": false, "video_demo_notes": null, "photos": 3, "photo_notes": null }, "external_reference": "order_acme_2026_001", "customer_email": "buyer@acme.com", "promotion_details": "Honest reaction to our new skincare serum...", "files_ready": false, "deliverables": null, "created_at": "2026-05-29T14:00:00Z", "paid_at": "2026-05-29T14:02:11Z", "delivered_at": null } } ``` `files_ready` flips to `true` and `deliverables` populates the moment the order enters status `delivered`. Use `files_ready` as a cheap polling signal before hitting `/files`. --- ### GET /v1/orders — List Orders **Scope:** `orders:read` List orders created by your API key. **Query parameters:** | Param | Type | Description | |-------|------|-------------| | `status` | enum | `pending_payment` \| `processing` \| `in_progress` \| `delivered` \| `canceled` | | `external_reference` | string | Exact match — useful for reconciliation | | `created_after` / `created_before` | ISO 8601 | Date range | | `page` / `limit` | int | Default 24, max 100 | | `cursor` | string | Reserved for cursor-paginated v1.1 — use `page` in v1 | **Sample response:** Same single-order envelope as `GET /v1/orders/{id}`, wrapped in `{ "data": [...], "pagination": {...} }`. --- ### GET /v1/orders/{id}/files — Deliverables **Scope:** `deliverables:read` Returns the delivery manifest for an order, with short-lived signed URLs to the underlying R2 files. **Query parameters:** | Param | Type | Description | |-------|------|-------------| | `ttl` | int | Signed-URL lifetime in seconds. Default `900` (15 min). Min `60`, max `3600`. | #### Example response (status = delivered) ```json { "data": { "order_id": "ord_01HW3KJ8E9K0YR6Z3T1F8XQ2A0", "delivered_at": "2026-06-01T19:24:00Z", "files": [ { "filename": "creator_sarah_clip_01.mp4", "kind": "video", "size_bytes": 14592120, "content_type": "video/mp4", "signed_url": "https://pub-70f9e589b1c640b49218874baf1c733f.r2.dev/custom-orders/ord_.../clip_01.mp4?X-Amz-Signature=...", "expires_at": "2026-06-01T19:39:00Z" }, { "filename": "photo_01.jpg", "kind": "image", "size_bytes": 2841120, "content_type": "image/jpeg", "signed_url": "https://pub-70f9e589b1c640b49218874baf1c733f.r2.dev/custom-orders/ord_.../photo_01.jpg?X-Amz-Signature=...", "expires_at": "2026-06-01T19:39:00Z" } ] } } ``` #### Pre-delivery behaviour For orders not yet delivered, the endpoint returns `200` with an empty `files: []`: ```json { "data": { "order_id": "ord_01HW3KJ8E9K0YR6Z3T1F8XQ2A0", "delivered_at": null, "files": [] } } ``` This lets you poll the files endpoint as your single "is it ready yet" check. Once `files[]` is non-empty, the order is delivered. Webhooks (v1.1) will push the same signal. **Signed URL handling:** - TTL defaults to **15 minutes** to balance security against client retry latency. Override with `?ttl=` (60–3600s) when your downloader needs longer windows. - To download server-side: fetch the manifest, download immediately, done. - To hand a URL to an end-user's browser: re-mint by re-calling `/files` just before serving the page, or proxy the file through your own server with your own access control. --- ### Status lifecycle Internally, DansUGC tracks several fine-grained creator-side lanes (`creator_to_accept`, `pending_video_editing`, `editing_in_progress`, `completed`, etc.). The partner-facing API collapses these into four normalized statuses, plus a terminal cancellation state: ``` pending_payment → processing → in_progress → delivered │ └── canceled (manual or refund) ``` | Partner status | What it means | Internal lanes covered | |-----------------|---------------|------------------------| | `pending_payment` | Order created, Stripe Checkout not yet completed | `not_started` + `payment_status=unpaid` | | `processing` | Paid, assigned creator(s) haven't started filming yet | `creator_to_accept`, `accepted` | | `in_progress` | Creator is filming, editing, or uploading | `filming`, `pending_video_editing`, `editing_in_progress` | | `delivered` | All files uploaded, signed URLs available via `/files` | `completed` | | `canceled` | Refunded, abandoned, or manually canceled | `canceled` | **Polling guidance:** - Once paid, expect: payment confirm (seconds) → `processing` → `in_progress` (hours) → `delivered` (1–3 business days). - Recommended poll interval: 60 seconds while `pending_payment` or `processing`, 5 minutes while `in_progress`. - v1.1 will add outbound webhooks (`order.paid`, `order.in_progress`, `order.delivered`, `order.canceled`). Build your listener now and route the polled status into it — swapping to webhooks at v1.1 will be a single config change. --- ### Idempotency Every `POST /v1/orders` accepts an `Idempotency-Key` header. Same key + same body within 24 hours returns the **original** response (same order IDs, same `payment_url`). Same key + a different body within 24 hours returns: ```http HTTP/1.1 409 Conflict ``` ```json { "error": { "code": "idempotency_conflict", "message": "An order was already created with this Idempotency-Key but the request body differs.", "original_order_ids": ["ord_01HW3KJ8E9K0YR6Z3T1F8XQ2A0"] } } ``` **Recommended pattern:** - Use UUIDv4 for one-off "user clicked submit" flows. - Use a deterministic key (e.g. `customer_id:isoweek`) for scheduled or cron-driven orders. A re-fire of the same cron job becomes a no-op. `POST /v1/pricing/quote` is naturally idempotent (pure calculation) and does not require the header. --- ### Errors All error responses share a single envelope: ```json { "error": { "code": "machine_readable_string", "message": "Human-readable explanation" } } ``` Some errors include extra fields (e.g. `format_id` on `format_minimum_not_met`, `current_status` on `files_not_ready`, `original_order_ids` on `idempotency_conflict`). They're additive — clients can safely ignore unknown fields. | HTTP | Code | Meaning | |------|------|---------| | 400 | `missing_field` | A required field is null or absent | | 400 | `invalid_field` | A field's value is malformed (e.g. unparseable email) | | 400 | `invalid_format` | `format_id` not found | | 400 | `invalid_model` | `model_id` not found or not bookable | | 400 | `format_minimum_not_met` | Quantity for a format is below `min_order_videos` | | 400 | `total_mismatch` | `expected_total` does not match computed `grand_total` | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks required scope | | 404 | `not_found` | Order / model / format does not exist | | 409 | `idempotency_conflict` | Same `Idempotency-Key`, different body | | 429 | `rate_limited` | Exceeded 60 req/min for this key | | 500 | `internal_error` | Server-side fault — retry with exponential backoff | The rate-limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `Retry-After` on 429) are identical to the B-Roll API. The 60 req/min budget is shared across all products. --- ### Integration patterns All examples use a single `dsk_` key with `orders:read orders:write pricing:read deliverables:read`. Assume `DSK_API_KEY` is set in the environment. #### Pattern A — Quote → confirm → order Render a price preview, let the user confirm, then create the order. ```javascript const BASE = "https://dansugc.com/api/v1"; const headers = { Authorization: `Bearer ${process.env.DSK_API_KEY}`, "Content-Type": "application/json", }; // 1. User picked a format + creator in your UI const formatId = 412; const modelId = "9c3f8f80-1f6a-4b7e-9e7c-2c2a9d2a6b0e"; // 2. Show a live quote const quote = await fetch(`${BASE}/pricing/quote`, { method: "POST", headers, body: JSON.stringify({ model_ids: [modelId], items: [{ format_id: formatId, quantity: 5 }], addons: { video_editing: true, video_demo: false, photos: 0 }, }), }).then((r) => r.json()); renderPriceSummary(quote.data); // your UI // 3. On confirm, submit the order const order = await fetch(`${BASE}/orders`, { method: "POST", headers: { ...headers, "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ model_ids: [modelId], customer_email: currentUser.email, promotion_details: form.brief, items: [{ format_id: formatId, quantity: 5 }], addons: { video_editing: true, video_demo: false, photos: 0 }, external_reference: `acme_${currentUser.id}_${Date.now()}`, expected_total: quote.data.grand_total, }), }).then((r) => r.json()); // 4. Send the user to Stripe window.location.href = order.data.payment_url; ``` #### Pattern B — Poll + download deliverables A robust poll-and-download loop. `files_ready` on `GET /orders/{id}` is the cheapest signal; once true, hit `/files` for signed URLs. ```javascript async function waitAndDownload(orderId, saveFile) { // 1. Poll until delivered or canceled while (true) { const { data } = await fetch(`${BASE}/orders/${orderId}`, { headers }) .then((r) => r.json()); if (data.status === "delivered") break; if (data.status === "canceled") throw new Error("Order canceled"); await new Promise((r) => setTimeout(r, 60_000)); // 1 min — well under 60 rpm } // 2. Fetch the manifest. Signed URLs default to 15 min — extend if needed. const { data: manifest } = await fetch( `${BASE}/orders/${orderId}/files?ttl=1800`, { headers } ).then((r) => r.json()); // 3. Stream each file out within the TTL window for (const f of manifest.files) { await saveFile(f.filename, f.signed_url); } } ``` #### Pattern C — Cron-driven recurring orders A scheduled job places one fresh weekly order per customer. Deterministic `Idempotency-Key` makes re-fires safe. ```javascript // Run weekly via cron / scheduled function async function placeWeeklyOrder(customer) { const idempotencyKey = `acme_${customer.id}_${isoWeek(new Date())}`; // 1. Quote — show the customer their cost before confirming const quote = await fetch(`${BASE}/pricing/quote`, { method: "POST", headers, body: JSON.stringify({ model_ids: customer.preferred_model_ids, items: customer.weekly_items, addons: customer.addons, }), }).then((r) => r.json()); await notifyCustomer(customer, quote.data); // email/Slack with grand_total // 2. On their confirmation, create the order. Re-fires within 24h are no-ops. const order = await fetch(`${BASE}/orders`, { method: "POST", headers: { ...headers, "Idempotency-Key": idempotencyKey }, body: JSON.stringify({ model_ids: customer.preferred_model_ids, customer_email: customer.email, promotion_details: customer.weekly_brief, items: customer.weekly_items, addons: customer.addons, external_reference: idempotencyKey, expected_total: quote.data.grand_total, }), }).then((r) => r.json()); return order.data.payment_url; } ``` `Idempotency-Key` doubling as `external_reference` means cron jitter doesn't create duplicate orders, and you can look up the order later with `GET /orders?external_reference=acme__`. --- ### Pricing Custom-order pricing follows the same canonical tier table documented under [Pricing](#pricing) below: ``` unit_price = pricing_tiers.base_price[level] × creator_types.multiplier[slug] ``` …plus the flat add-ons (`$20/video` editing, `$20/video` demo) and per-creator photo rates from `models.photo_unit_price`. Add-on rates are **flat** — they do not scale with the creator multiplier. Photos do not use the creator multiplier either (the per-creator rate already encodes whatever premium that creator commands). **Worked example.** Two creators, 5 videos each at Tier 5 ($55 base), editing on: ``` Creator A (standard, 1.00×): format subtotal = 55.00 × 1.00 × 5 = 275.00 editing = 20.00 × 5 = 100.00 total = 375.00 Creator B (viral, 1.50×): format subtotal = 55.00 × 1.50 × 5 = 412.50 editing = 20.00 × 5 = 100.00 (flat — multiplier does NOT apply) total = 512.50 grand_total = 887.50 ``` See the [Pricing](#pricing) section below for the full tier table and creator-type multipliers — same numbers, no separate custom-order pricing surface. --- ### What's not in v1 Plan around these. They are deliberately not in scope for v1.0: - **Outbound webhooks.** Coming in v1.1 (`order.paid`, `order.in_progress`, `order.delivered`, `order.canceled`). Poll `GET /v1/orders/{id}` until then. - **Balance / credit payment via API.** v1 only supports Stripe Checkout via `payment_url`. End-users with an existing DansUGC balance can still pay via `dansugc.com`, but the API will not deduct credits. - **Partial refunds via API.** Handled manually by DansUGC support. - **Customer self-serve key issuance.** v1 keys are admin-issued only. Email `dan@dansugcmodels.com` for sub-keys; programmatic issuance is planned for v1.2. - **Sandbox / test mode.** The `dsu_test_...` prefix is reserved but not yet wired. Use small real orders (one Tier-1 video at $8) for end-to-end verification. - **Order modification / cancellation via API.** Items lock once status is `processing`. Cancellation pre-fulfillment requires emailing support; automated cancel is planned for v1.1. - **Mixed-format-across-creators in one POST.** All `model_ids` you pass receive the same `items` and `promotion_details`. Submit two orders for two briefs. On the v1.2 roadmap. For the full partner contract — including all worked pricing examples and onboarding instructions — see the integration guide at https://dansugc.com/developers/platform. --- ## MCP Server — AI Assistant Integration DansUGC provides a Model Context Protocol (MCP) server so AI assistants can search, browse, and purchase B-roll directly. ### Connection Details - Endpoint: `https://dansugc.com/api/mcp` - Transport: Stateless Streamable HTTP - Auth: Same `dsk_` Bearer token as REST API ### Configuration for AI Clients **Claude Desktop** (`claude_desktop_config.json`): ```json { "mcpServers": { "dansugc": { "url": "https://dansugc.com/api/mcp", "headers": { "Authorization": "Bearer dsk_your_key" } } } } ``` **Claude Code** (`.mcp.json` in project root): ```json { "mcpServers": { "dansugc": { "type": "http", "url": "https://dansugc.com/api/mcp", "headers": { "Authorization": "Bearer dsk_your_key" } } } } ``` **Cursor** (`.cursor/mcp.json`): ```json { "mcpServers": { "dansugc": { "url": "https://dansugc.com/api/mcp", "headers": { "Authorization": "Bearer dsk_your_key" } } } } ``` ### MCP Tools #### B-Roll Tools - `search_videos` — Search & browse B-roll with all filters (emotion, gender, location, etc.), text search, and semantic search. Returns paginated results with preview URLs and pricing. - `get_video` — Get full details for a single video by ID including pricing and preview URL. - `purchase_videos` — Purchase one or more videos by ID. Returns permanent download URLs. Credits deducted atomically. - `list_purchases` — View purchase history with date range, filters, and billing summary. - `get_balance` — Check account credit balance and total spending. #### Key Management Tools - `manage_customer_keys` — Create, list, or revoke per-customer API keys. Actions: "create", "list", "revoke". #### Posting Tools - `check_posting_subscription` — Plan status and social set usage / limit. - `list_social_sets` — Groups of TikTok/Instagram accounts that publish together. - `create_social_set` — Create a new social set (accounts are then connected via the dashboard). - `list_posting_accounts` — Connected TikTok/Instagram accounts with follower counts. Returns the UUIDs needed by `create_post`. - `get_media_upload_url` — Get a 5-minute presigned R2 PUT URL for one video or image. Args: `content_type` (one of the allowlisted MIMEs), `size_bytes`. Returns `upload_url` (HTTP PUT target) and `public_url` (use in `create_post`). Caps: 200 MB video, 25 MB image. Per-key rolling 24h quotas apply. - `create_post` — Create draft / scheduled / immediate post. `media_urls` MUST be `public_url`s from `get_media_upload_url` — external URLs are rejected. Args: `account_ids` (required), `caption`, `media_urls`, `publish_now`, `scheduled_for`, `timezone`, `platform_settings`. - `list_posts` — View posts and their status. Filter by `status` (draft|scheduled|published|failed). - `update_post` — Update caption, status, or scheduled time. - `delete_post` — Permanently delete a post. #### Social Media Research Tools (ScrapCreators Proxy) - `tiktok_search_videos` — Search TikTok by keyword. Returns video descriptions, stats (plays, likes, comments, shares), author info, cover images. Cost: $0.02/request. - `tiktok_user_videos` — Get a TikTok user's videos sorted by most popular. Cost: $0.02/request. - `tiktok_search_users` — Find TikTok creators by keyword. Cost: $0.02/request. - `instagram_search_reels` — Search Instagram Reels by keyword. Returns captions, stats, author info. Cost: $0.02/request. - `instagram_user_reels` — Get a specific Instagram user's reels. Cost: $0.02/request. ### MCP Usage Examples **Example 1: Find and purchase happy reaction videos** ``` User: "Find me 3 happy female reaction videos filmed indoors, then buy the best ones" AI should: 1. Call search_videos with filters: emotion="happy", gender="female", location="indoor", limit=10 2. Present the results to the user with preview URLs and prices 3. After user confirms, call purchase_videos with the selected video IDs 4. Return the download URLs ``` **Example 2: Semantic search for specific content** ``` User: "I need videos of someone excitedly opening a package, like an unboxing reaction" AI should: 1. Call search_videos with semantic_search="excited person opening package unboxing reaction" 2. Results will be ranked by AI relevance (hybrid search with reranking) 3. Present top matches with similarity scores ``` **Example 3: Research TikTok trends then find matching B-roll** ``` User: "What skincare UGC formats are trending on TikTok? Find me matching B-roll." AI should: 1. Call tiktok_search_videos with query="skincare routine UGC" to see trends 2. Analyze the trending formats (reaction, GRWM, before/after, etc.) 3. Call search_videos with semantic_search matching the trending format style 4. Present both: the TikTok trends and matching DansUGC B-roll ``` **Example 4: Platform integration — manage customer keys** ``` User: "Set up API access for our customer Acme Corp" AI should: 1. Call manage_customer_keys with action="create", name="Acme Corp", scopes=["broll:read", "broll:purchase"] 2. Return the generated key to the user (remind them to store it securely) 3. Explain that Acme's purchases will be isolated and trackable ``` **Example 5: Billing reconciliation** ``` User: "How much did we spend on B-roll this month?" AI should: 1. Call get_balance to check overall balance and spending 2. Call list_purchases with start_date and end_date for the current month 3. Present the summary: total purchases, total amount, breakdown by type ``` **Example 6: Upload a locally-generated video and schedule a post** ``` User: "I made out.mp4 locally. Post it to my TikTok at 6pm ET tomorrow with the caption 'try this'." AI should: 1. stat out.mp4 — capture size in bytes and confirm content-type is video/mp4 2. Call get_media_upload_url(content_type="video/mp4", size_bytes=) 3. HTTP PUT the file body to the returned upload_url with header Content-Type: video/mp4 (the content-type is signed into the URL — any mismatch fails) 4. Call list_posting_accounts and pick the TikTok account UUID 5. Call create_post({ account_ids: [], caption: "try this", media_urls: [], scheduled_for: "2026-04-02T22:00:00Z", timezone: "America/New_York" }) Note: the agent CANNOT pass an arbitrary URL (e.g. a Dropbox link) as media_urls — create_post only accepts public_urls returned by get_media_upload_url. This is a deliberate security boundary, not a limitation. ``` --- ## Posting API — Complete Reference Connect TikTok and Instagram accounts to DansUGC, upload and schedule content, and read cross-platform analytics programmatically. ### Authentication The Posting API uses the same `dsk_` Bearer token as the B-Roll API — no extra scopes required. Any valid API key works. ``` Authorization: Bearer dsk_your_key ``` Base URL for all Posting API routes: `https://dansugc.com/api/v1/posting` --- ### Social Sets A **social set** groups multiple platform accounts together (e.g. one TikTok + one Instagram for the same brand). Each account belongs to exactly one social set. #### GET /subscription — Check Subscription Status ``` GET https://dansugc.com/api/v1/posting/subscription Authorization: Bearer dsk_your_key ``` Response: ```json { "subscription": { "plan": "growth", "status": "active", "social_sets_limit": 10, "current_period_end": "2026-04-25T00:00:00Z", "canceled_at": null }, "usage": { "social_sets_used": 3, "social_sets_limit": 10, "is_unlimited": false, "can_create_more": true, "overage_count": 0, "extra_set_price_cents": null } } ``` Returns `{ "subscription": null, "usage": null }` if no active subscription. --- #### GET /social-sets — List Social Sets ``` GET https://dansugc.com/api/v1/posting/social-sets Authorization: Bearer dsk_your_key ``` Response: ```json { "data": [ { "id": "set-uuid", "name": "Main Brand", "description": "Primary brand accounts", "color": "#8B5CF6", "icon": "star", "zernio_profile_id": "zernio-profile-id", "status": "active", "created_at": "2026-01-01T00:00:00Z", "account_count": 2, "accounts": [ { "username": "mybrand", "platform": "tiktok", "profile_picture_url": "https://..." }, { "username": "mybrand", "platform": "instagram", "profile_picture_url": "https://..." } ] } ] } ``` #### POST /social-sets — Create Social Set ``` POST https://dansugc.com/api/v1/posting/social-sets Authorization: Bearer dsk_your_key Content-Type: application/json ``` ```json { "name": "Main Brand", "description": "Primary brand TikTok + Instagram", "color": "#8B5CF6", "icon": "star" } ``` Only `name` is required. Response: `{ "data": { "id": "set-uuid", "name": "Main Brand", ... } }` After creating a social set, connect TikTok/Instagram accounts via the dashboard at https://dansugc.com/dashboard/posting/accounts #### PATCH /social-sets — Update a Social Set ``` PATCH https://dansugc.com/api/v1/posting/social-sets Authorization: Bearer dsk_your_key Content-Type: application/json ``` ```json { "id": "set-uuid", "name": "Updated Brand Name", "description": "New description", "color": "#3B82F6", "icon": "zap", "project_id": "project-uuid" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `id` | string (UUID) | yes | Social set to update | | `name` | string | no | New name | | `description` | string | no | New description | | `color` | string | no | Hex color | | `icon` | string | no | Icon name | | `project_id` | string (UUID) or null | no | Assign to a project. Pass `null` to unassign. | Response: `{ "data": { "id": "set-uuid", "name": "Updated Brand Name", ... } }` --- ### Accounts #### GET /accounts — List Connected Accounts Returns all active TikTok and Instagram accounts with their latest analytics snapshot. ``` GET https://dansugc.com/api/v1/posting/accounts Authorization: Bearer dsk_your_key ``` Response: ```json { "data": [ { "id": "account-uuid", "platform": "tiktok", "username": "mybrand", "display_name": "My Brand", "profile_picture_url": "https://...", "status": "active", "scraping_active": true, "social_set_id": "set-uuid", "zernio_account_id": "zernio-acc-id", "connected_at": "2026-01-15T10:00:00Z", "latest_analytics": { "followers": 12400, "following": 230, "posts_count": 87 }, "total_views": 840000 } ] } ``` **Note:** Use the `id` (UUID) field when referencing accounts in `POST /posts`. Accounts are connected via the dashboard at https://dansugc.com/dashboard/posting/accounts — OAuth flows are not available via the API. --- ### Media Upload — Secure 3-Step Workflow To attach a video or image to a post, you must first upload it to DanSUGC's R2 storage via a presigned URL. **Posts will not accept arbitrary external URLs** — this is the gate that protects every user on the platform. #### POST /media/presign — Issue Presigned Upload URL ``` POST https://dansugc.com/api/v1/posting/media/presign Authorization: Bearer dsk_your_key Content-Type: application/json ``` ```json { "content_type": "video/mp4", "size_bytes": 14592120 } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `content_type` | string | yes | One of: `video/mp4`, `video/quicktime`, `video/webm`, `image/jpeg`, `image/png`, `image/webp` | | `size_bytes` | integer | yes | Exact byte length of the file. Used for quota accounting. | Response: ```json { "data": { "upload_id": "9c3f8f80-1f6a-4b7e-9e7c-2c2a9d2a6b0e", "upload_url": "https://.r2.cloudflarestorage.com/dansugc/posting///1717000000-abc.mp4?X-Amz-Signature=...", "public_url": "https://pub-70f9e589b1c640b49218874baf1c733f.r2.dev/posting///1717000000-abc.mp4", "expires_at": "2026-04-01T18:05:00Z", "max_bytes": 209715200, "content_type": "video/mp4", "kind": "video" } } ``` #### Step 2 — PUT the file ``` PUT Content-Type: video/mp4 # MUST exactly match the content_type you requested Body: ``` The content type is signed into the URL. R2 will reject the upload if the `Content-Type` header doesn't match the type you presigned for. #### Step 3 — Reference the public_url in a post ```json POST /posting/posts { "caption": "...", "media_urls": [""], "account_ids": [...] } ``` The posts endpoint cross-checks each URL against the uploads table. An external URL or a URL belonging to a different user returns `400 media_url not recognized`. #### Security model | Layer | Enforcement | |-------|-------------| | Authentication | Bearer API key (`dsk_*`); key must be linked to a user | | Content type allowlist | Strict server-side allowlist (no octet-stream, no SVG, no executables) | | Size cap (declared) | 200 MB videos, 25 MB images — enforced at presign time | | Size cap (actual) | Re-verified via HEAD during the scan — defends against clients that lie at presign time | | Path scoping | R2 key is server-generated: `posting/{user_id}/{api_key_id}/{ts}-{rand}.{ext}` — no client input, no traversal | | URL TTL | Presigned URLs expire in 5 minutes | | Signature binding | content-type is bound into the SigV4 signature — uploader cannot swap MIME | | Per-key quotas | Rolling 24h cap: 1 GB / 100 files (default); raise per key via `api_keys.daily_upload_bytes` / `daily_upload_files` | | Reference validation | `POST /posts` refuses any media_url not issued to the calling user | | Magic-byte sniff | Layer 1 of the scan: actual file bytes are inspected (first 32 KB) and the family must match the declared MIME (`image/*` or `video/*`). Catches disguised-extension attacks (`.exe` renamed to `.mp4`). | | VirusTotal hash lookup | Layer 2 of the scan: SHA-256 the file (≤ 25 MB), query VirusTotal's hash-only endpoint. Hash-only — file content never leaves our infrastructure. Known-malicious hashes block the post. Requires `VIRUSTOTAL_API_KEY` env var; gracefully degrades if not set. | | Verdict caching | Re-uploads with the same SHA-256 reuse the prior verdict — no duplicate VT calls | | Storage lifecycle | R2 bucket lifecycle deletes `posting/` objects older than 30 days (configure in Cloudflare dashboard) | | Audit trail | Every issued URL is logged in `posting_media_uploads` with content type, size, sha256, sniffed MIME, and scan timestamp | #### When is scanning run? Scanning runs on the first reference of an uploaded URL in `POST /posting/posts`. The result is persisted to `posting_media_uploads.scan_status` and reused for any subsequent post that attaches the same URL. Scan latency is typically: - Magic-byte only (≥ 25 MB or VT not configured): ~300 ms - Full Layer 1 + Layer 2: 1–5 s for small clips; up to ~10 s near the 25 MB cap If the scanner can't make a determination (network flake, R2 unreachable), the upload row stays in `pending` and `POST /posts` returns `503` — the agent should retry. #### Errors | Status | Reason | |--------|--------| | 400 | Invalid `content_type` or `size_bytes`, or media_url failed the safety scan (mismatched magic bytes, oversize actual upload, known-malicious hash) | | 401 | Missing or invalid API key | | 403 | API key not linked to a user | | 413 | File exceeds the per-kind size cap | | 429 | Daily byte or file-count quota exceeded | | 503 | Scanner could not reach the file — retry the request | --- ### Posts #### GET /posts — List Posts Returns posts scoped to your API key's user, with account associations. ``` GET https://dansugc.com/api/v1/posting/posts Authorization: Bearer dsk_your_key ``` Optional query params: `?status=scheduled` (draft|scheduled|published|failed), `?limit=50` ``` Response: ```json { "data": [ { "id": "post-uuid", "caption": "Check out this amazing product! #ugc #viral", "media_urls": ["https://media.dansugc.com/posting/clip.mp4"], "status": "scheduled", "scheduled_at": "2026-03-25T18:00:00Z", "published_at": null, "created_at": "2026-03-24T10:00:00Z", "posting_post_accounts": [ { "id": "post-account-uuid", "zernio_post_id": "ext-post-id", "posting_accounts": { "id": "account-uuid", "platform": "tiktok", "username": "mybrand" } } ] } ] } ``` **Status values:** `draft`, `scheduled`, `published`, `failed` #### POST /posts — Create, Schedule, or Publish a Post ``` POST https://dansugc.com/api/v1/posting/posts Authorization: Bearer dsk_your_key Content-Type: application/json ``` ```json { "caption": "Check out this amazing product! #ugc #viral", "media_urls": ["https://media.dansugc.com/posting/clip.mp4"], "account_ids": ["account-uuid-1", "account-uuid-2"], "publish_now": false, "scheduled_for": "2026-03-25T18:00:00Z", "timezone": "America/New_York" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `caption` | string | no | Caption / post text | | `media_urls` | string[] | no | Publicly accessible video or image URLs | | `account_ids` | string[] | yes | Account UUIDs from `GET /accounts` | | `publish_now` | boolean | no | `true` = publish immediately | | `scheduled_for` | ISO 8601 string | no | Future publish time. Omit for draft. | | `timezone` | string | no | IANA timezone (e.g. `"America/New_York"`). Defaults to UTC. | **Media:** Pass publicly accessible URLs. B-Roll download URLs from `POST /broll/purchase` work directly. Response: ```json { "data": { "id": "post-uuid", "caption": "Check out this amazing product! #ugc #viral", "status": "scheduled", "scheduled_at": "2026-03-25T18:00:00Z" } } ``` **Status mapping:** `publishNow: true` → `"published"` · `scheduledFor` set → `"scheduled"` · neither → `"draft"` #### PATCH /posts/:id — Update a Post ``` PATCH https://dansugc.com/api/v1/posting/posts/post-uuid Authorization: Bearer dsk_your_key Content-Type: application/json ``` ```json { "status": "scheduled", "caption": "Updated caption #ugc", "scheduled_at": "2026-03-26T20:00:00Z" } ``` | Field | Type | Description | |-------|------|-------------| | `status` | string | New status: `draft`, `scheduled`, `published`, `failed` | | `caption` | string | Updated caption | | `scheduled_at` | ISO 8601 | New scheduled time | Setting `status: "published"` automatically sets `published_at` to the current time. Response: `{ "data": { "id": "post-uuid", "status": "scheduled", ... } }` #### DELETE /posts/:id — Delete a Post ``` DELETE https://dansugc.com/api/v1/posting/posts/post-uuid Authorization: Bearer dsk_your_key ``` Response: `{ "success": true }` --- ### Analytics #### GET /analytics — Cross-Platform Analytics Returns summary metrics, per-account stats, and top posts. ``` GET https://dansugc.com/api/v1/posting/analytics?range=30d&platform=tiktok Authorization: Bearer dsk_your_key ``` **Query parameters:** - `range` — `7d`, `30d` (default), `90d` - `platform` — `tiktok`, `instagram`, or omit for all platforms combined Response: ```json { "data": { "summary": { "total_followers": 24800, "followers_change": 1200, "followers_change_pct": 5.1, "avg_engagement_rate": 4.23, "engagement_change": 0.3, "total_views": 2840000, "views_change": 340000, "views_change_pct": 13.6, "total_likes": 48200, "likes_change_pct": 8.2, "total_posts_published": 14, "posts_change": 3, "accounts_tracked": 2, "last_scrape_at": "2026-03-25T08:00:00Z", "range": "30d" }, "accounts": [ { "id": "account-uuid", "platform": "tiktok", "username": "mybrand", "followers": 18000, "followers_change_pct": 6.2, "engagement_rate": 4.8, "latest_engagement_rate": 5.1, "likes_total": 36000, "sparkline": [4.2, 4.5, 4.8, 5.0, 5.1, 4.9, 5.1] } ], "top_posts": [ { "post_id": "ext-post-id", "description": "Honest reaction to this skincare serum", "cover_url": "https://cdn.example.com/cover.jpg", "web_url": "https://www.tiktok.com/@mybrand/video/...", "plays": 284000, "likes": 18200, "comments": 430, "shares": 1200, "engagement_rate": 7.0, "create_time": 1742400000 } ], "chart_data": [ { "date": "2026-03-01", "tiktok_views": 84000, "instagram_views": 21000, "tiktok_engagement": 4.8, "instagram_engagement": 3.2, "tiktok_likes": 3200, "instagram_likes": 810, "tiktok_comments": 140, "instagram_comments": 55, "tiktok_followers": 17800, "instagram_followers": 6800 } ] } } ``` **`chart_data`** contains one entry per day in the selected range. Fields are `null` for platforms with no data that day. --- ### ReelClaw Integration Patterns All examples use a single `dsk_` API key for both B-Roll and Posting. #### Pattern A: Source a B-Roll Clip and Schedule It ```javascript const BASE = "https://dansugc.com"; const headers = { Authorization: "Bearer dsk_your_key", "Content-Type": "application/json" }; // 1. Get connected accounts const { data: accounts } = await fetch(`${BASE}/api/v1/posting/accounts`, { headers }).then(r => r.json()); const tiktokAccount = accounts.find(a => a.platform === "tiktok"); const igAccount = accounts.find(a => a.platform === "instagram"); // 2. Buy a UGC B-Roll clip const { purchases } = await fetch(`${BASE}/api/v1/broll/purchase`, { method: "POST", headers, body: JSON.stringify({ video_ids: ["broll-uuid"] }), }).then(r => r.json()); // 3. Schedule post using the download URL directly as media const scheduledFor = new Date(); scheduledFor.setDate(scheduledFor.getDate() + 1); scheduledFor.setHours(18, 0, 0, 0); const { data: post } = await fetch(`${BASE}/api/v1/posting/posts`, { method: "POST", headers, body: JSON.stringify({ caption: "This skincare serum is UNREAL 😱 #ugc #skincare #viral", media_urls: [purchases[0].download_url], account_ids: [tiktokAccount.id, igAccount.id], scheduled_for: scheduledFor.toISOString(), timezone: "America/New_York", }), }).then(r => r.json()); console.log(`Scheduled: ${post.id} for ${post.scheduled_at}`); // 4. Later: read analytics to measure performance const { data: analytics } = await fetch( `${BASE}/api/v1/posting/analytics?range=7d`, { headers } ).then(r => r.json()); console.log(`Top post: "${analytics.top_posts[0]?.description}" — ${analytics.top_posts[0]?.plays} plays`); ``` #### Pattern B: Batch Schedule a Week of Content ```javascript // Source 5 b-roll clips and schedule one per day, Mon–Fri at 6pm ET const { videos } = await fetch(`${BASE}/api/v1/broll?emotion=excited&media_type=video&limit=5`, { headers }).then(r => r.json()); const { purchases } = await fetch(`${BASE}/api/v1/broll/purchase`, { method: "POST", headers, body: JSON.stringify({ video_ids: videos.map(v => v.id) }), }).then(r => r.json()); const captions = [ "POV: you just discovered the product everyone's talking about 🔥 #ugc", "I wasn't ready for this reaction 😂 #viral #ugc", "Okay this is actually insane 👀 #honest #review", "Me when the product actually works ✨ #ugc #skincare", "The look says it all 💯 #authentic #ugc", ]; for (let i = 0; i < purchases.length; i++) { const scheduledFor = new Date(); scheduledFor.setDate(scheduledFor.getDate() + i + 1); scheduledFor.setHours(18, 0, 0, 0); await fetch(`${BASE}/api/v1/posting/posts`, { method: "POST", headers, body: JSON.stringify({ caption: captions[i], media_urls: [purchases[i].download_url], account_ids: [tiktokAccount.id], scheduled_for: scheduledFor.toISOString(), timezone: "America/New_York", }), }); console.log(`Day ${i + 1} scheduled: ${captions[i]}`); } ``` #### Pattern C: Analytics-Driven Content Decisions ```javascript const { data: analytics } = await fetch( `${BASE}/api/v1/posting/analytics?range=30d`, { headers } ).then(r => r.json()); const { summary, top_posts } = analytics; console.log(`Engagement: ${summary.avg_engagement_rate}%`); console.log(`Best post: "${top_posts[0]?.description}" — ${top_posts[0]?.plays?.toLocaleString()} plays`); // Find more content similar to the best-performing post if (top_posts[0]) { const { videos } = await fetch(`${BASE}/api/v1/broll?` + new URLSearchParams({ semantic_search: top_posts[0].description, media_type: "video", limit: "5", }), { headers }).then(r => r.json()); console.log(`Found ${videos.length} similar clips — double down on what's working`); } ``` #### Pattern D: Update or Delete a Post ```javascript // Reschedule a post await fetch(`${BASE}/api/v1/posting/posts/post-uuid`, { method: "PATCH", headers, body: JSON.stringify({ scheduled_at: "2026-04-01T18:00:00Z", caption: "Updated caption with new hook 🔥 #ugc", }), }).then(r => r.json()); // Delete a draft that didn't make the cut await fetch(`${BASE}/api/v1/posting/posts/post-uuid`, { method: "DELETE", headers, }); ``` --- ## Integration Patterns for AI Applications ### Pattern 1: Content Creation Pipeline Build an AI-powered content creation tool that automatically sources B-roll: ```python import requests API_KEY = "dsk_your_key" BASE = "https://dansugc.com/api/v1" headers = {"Authorization": f"Bearer {API_KEY}"} # Step 1: AI generates a video script with B-roll cues # Step 2: For each B-roll cue, semantic search for matching clips def find_broll(description: str, count: int = 3): res = requests.get(f"{BASE}/broll", headers=headers, params={ "semantic_search": description, "media_type": "video", "limit": count, }) return res.json()["videos"] # Step 3: Purchase the selected clips def purchase_clips(video_ids: list[str]): res = requests.post(f"{BASE}/broll/purchase", headers=headers, json={ "video_ids": video_ids, }) return res.json()["purchases"] # Example: script has cues like "happy reaction to product" and "surprised face" clips = find_broll("happy woman reacting to skincare product") purchased = purchase_clips([c["id"] for c in clips[:2]]) for p in purchased: print(f"Download: {p['download_url']}") ``` ### Pattern 2: Multi-Tenant SaaS Platform Give each of your customers their own isolated B-roll access: ```javascript const API_KEY = "dsk_root_key"; // Your root key with keys:manage scope const BASE = "https://dansugc.com/api/v1"; const headers = { Authorization: `Bearer ${API_KEY}` }; // When a new customer signs up, create their key async function onboardCustomer(customerId, customerName) { const res = await fetch(`${BASE}/keys`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ name: `Customer: ${customerName}`, scopes: ["broll:read", "broll:purchase"], customer_id: customerId, }), }); const { key } = await res.json(); // Store key securely — it's only returned once await saveCustomerKey(customerId, key); return key; } // Monthly billing: check each customer's usage async function getCustomerUsage(apiKeyId, month) { const res = await fetch( `${BASE}/broll/purchases?api_key_id=${apiKeyId}&start_date=${month}-01&end_date=${month}-31`, { headers } ); const { summary } = await res.json(); return summary; // { total_purchases, total_amount, currency } } ``` ### Pattern 3: AI Agent with MCP (Claude Code / Cursor) When an AI agent has DansUGC MCP configured, it can directly help users: ``` // The AI agent can autonomously: 1. SEARCH — "Find happy reaction videos" → calls search_videos tool 2. FILTER — Apply emotion, gender, location, age filters based on user needs 3. PREVIEW — Show watermarked preview URLs to the user for approval 4. PURCHASE — Buy approved videos → calls purchase_videos tool 5. DELIVER — Return permanent download URLs 6. TRACK — Check balance and purchase history → calls get_balance, list_purchases 7. RESEARCH — Search TikTok/Instagram for trending formats → calls tiktok_search_videos ``` ### Pattern 4: Automated Ad Creative Pipeline ```python import requests API_KEY = "dsk_your_key" BASE = "https://dansugc.com/api/v1" headers = {"Authorization": f"Bearer {API_KEY}"} def build_ad_creative(product_description: str, target_audience: str): """AI-powered pipeline: analyze product → find B-roll → purchase → assemble""" # Step 1: Search for reaction videos matching the product vibe reactions = requests.get(f"{BASE}/broll", headers=headers, params={ "semantic_search": f"person reacting to {product_description}", "gender": "female" if "women" in target_audience.lower() else None, "emotion": "excited", "media_type": "video", "sort_by": "virality_score", "sort_order": "desc", "limit": 10, }).json()["videos"] # Step 2: Pick the top 3 by virality score top_clips = reactions[:3] # Step 3: Purchase purchased = requests.post(f"{BASE}/broll/purchase", headers=headers, json={ "video_ids": [v["id"] for v in top_clips], }).json()["purchases"] return { "clips": purchased, "total_cost": sum(p["price_paid"] for p in purchased), "download_urls": [p["download_url"] for p in purchased], } # Usage creative = build_ad_creative( product_description="premium skincare serum", target_audience="women 25-34 interested in beauty" ) ``` ### Pattern 5: Iterate Through Full Library ```javascript async function getAllVideos(filters = {}) { const allVideos = []; let page = 1; let hasMore = true; while (hasMore) { const params = new URLSearchParams({ ...filters, page, limit: 100 }); const res = await fetch(`${BASE}/broll?${params}`, { headers }); const data = await res.json(); allVideos.push(...data.videos); hasMore = data.pagination.has_more; page++; // Respect rate limits if (hasMore) await new Promise(r => setTimeout(r, 1100)); } return allVideos; } // Get all happy videos const happyVideos = await getAllVideos({ emotion: "happy", media_type: "video" }); ``` --- ## Pricing ### B-Roll Library Pricing (by difficulty tier) | Tier | Level | Base Price | Description | |------|-------|-----------|-------------| | Basic Face Reaction | 1 | $8 | No talking; raw reaction | | Lipsync / Singing | 2 | $15 | Matching audio; single-take | | Script Reading / App Demo | 3 | $20 | Speaking to camera or screen recording | | Reaction + App Demo | 4 | $30 | Picture-in-picture; face + screen | | Problem/Solution | 5 | $55 | Script + demo + captions + music | | Aesthetic Lifestyle | 6 | $85 | Product in environment; 5+ cuts | | High-Conversion Ad | 7 | $120 | Hook testing; professional lighting | | Product Deep Dive | 8 | $160 | Unboxing + demo + voiceover + 4K | | Brand Storytelling | 9 | $205 | Creative concepting + VFX | | Premium Campaign | 10 | $250 | Full rights + 3 hook variations | - Images: $3 flat (regardless of tier) - Credit-based system with auto top-up available - ScrapCreators proxy tools: $0.02 per request ### Custom Order Pricing Custom orders placed via `/api/v1/orders` use the **same** tier table above. The unit price for a video is: ``` unit_price = pricing_tiers.base_price[level] × creator_types.multiplier[slug] ``` Creator-type multipliers apply to format base prices: | Creator Type | Multiplier | Example (Level 5 = $55) | |-------------|-----------|------------------------| | Standard | 1.0x | $55 | | Viral | 1.5x | $82.50 | | Couples | 2.0x | $110 | Add-ons (`video_editing`, `video_demo`) are flat at $20/video and do **not** scale with the multiplier. Photos bill at the per-creator `photo_unit_price` returned by `GET /v1/models` — also not multiplied. See the Custom Ordering API reference above for full worked examples. --- ## Error Handling All errors follow a consistent format: ```json { "error": "Human-readable error message", "code": "ERROR_CODE" } ``` | Status | Code | Meaning | |--------|------|---------| | 400 | BAD_REQUEST | Invalid parameters (e.g., both search and semantic_search provided) | | 401 | UNAUTHORIZED | Missing or invalid API key | | 403 | FORBIDDEN | API key lacks required scope | | 404 | NOT_FOUND | Video or resource not found | | 429 | RATE_LIMITED | Exceeded 60 req/min. Check `Retry-After` header | | 500 | INTERNAL_ERROR | Server error — retry with exponential backoff | Rate limit headers on every response: - `X-RateLimit-Limit: 60` - `X-RateLimit-Remaining: 45` - `X-RateLimit-Reset: 1679900000` --- ## Products ### B-Roll Library - 2,000+ pre-recorded UGC reaction videos and images - Instant download after purchase - AI-powered semantic search + 10+ attribute filters - Preview videos before buying (watermarked) - Full commercial rights included ### Custom Video Orders - Choose from 35+ verified human creators - 100% exclusive content, never resold - Delivered within 2-3 business days - Full commercial rights included - Creators film to your specific brief --- ## Key Differentiators - **100% Real Humans**: Every video is filmed by verified human creators. No AI-generated content. - **Semantic Search**: Find exactly what you need with natural language queries, powered by AI embeddings with reranking. - **Developer-First**: REST API, MCP server, OpenAPI spec, per-customer keys, webhooks. - **Instant Access**: B-Roll is available for immediate download after purchase. - **Full Rights**: Lifetime commercial rights on all content. --- ## Developer Resources - Interactive API Docs: https://dansugc.com/docs - Developer Guide: https://dansugc.com/developers - Platform Integration Guide: https://dansugc.com/developers/platform - OpenAPI Spec: https://dansugc.com/api/openapi.json - API Key Management: https://dansugc.com/dashboard/api-keys - This file (llms-full.txt): https://dansugc.com/llms-full.txt - Summary file (llms.txt): https://dansugc.com/llms.txt ## Contact - Website: https://dansugc.com - Email: dan@dansugcmodels.com - Twitter/X: @dansugc ## Additional Resources - B-Roll Library: https://dansugc.com/broll - Pricing: https://dansugc.com/pricing - Creator Packages: https://dansugc.com/packages - Blog: https://dansugc.com/blog - Become a Creator: https://dansugc.com/apply