---
title: "Create Target Collab automation"
method: POST
path: "/automations/target-collab"
tags: ["Automations"]
---

# Create Target Collab automation

`POST /automations/target-collab`

Create a Target Collab (TC) invitation automation with optional follow-up DMs. Required: `read_write` scope, `Idempotency-Key` header. Pass `X-Dry-Run: true` to validate without persisting.

## Headers

- `Idempotency-Key` string, nullable
- `X-Dry-Run` string, nullable
- `X-Created-Via` string, nullable

## Request body

- AutomationTargetCollabRequest — Body for POST /automations/target-collab. Maps internally to DB `automation_type='Target Collab'` (or a TC variant if `messages` is present, in which case the route stamps `Message + Target Collab + Target Collab Card`). The TC Cleanup variant has its own top-level type (`AutomationTcCleanupRequest` → `POST /automations/tc-cleanup`) rather than living here as a flag. Reasoning: cleanup is structurally different — no messages / follow-ups, no custom invite name, products-only — and surfacing it as a TC mode forced callers to submit fields the runtime ignores. Per architecture review 2026-05-01.
  - `automation_name` string, required
  - `schedule` AutomationSchedule, required — Per-day creator-outreach caps + run window. Stored into Automations.schedule JSONB. Same shape as the existing portal payload — keeps the portal and Public API agreeing on schema.
    - `Monday_maxCreators` integer
    - `Tuesday_maxCreators` integer
    - `Wednesday_maxCreators` integer
    - `Thursday_maxCreators` integer
    - `Friday_maxCreators` integer
    - `Saturday_maxCreators` integer
    - `Sunday_maxCreators` integer
    - `start_time` string, nullable — HH:MM 24-hour, in `timezone`. Hours 00-23, minutes 00-59.
    - `end_time` string, nullable — HH:MM 24-hour, in `timezone`. Hours 00-23, minutes 00-59.
    - `timezone` string, nullable — IANA timezone name, e.g. America/Los_Angeles
  - `creators_to_include` CreatorsToInclude — Outreach-paradigm recipient inclusion. Three mutually-exclusive modes (cf. INTERNAL-design-review.md §11A): * `lists` mode — `list_upload` and/or `lists_selected` populated * `filters` mode — `filters` populated (dynamic by attribute) * `crm_group` mode — `crm_group_id` populated (saved predicate) The validator below rejects any request that mixes modes. The `creator_source_type` discriminator written to `config` is derived from which mode is set — callers don't need to set it explicitly.
    - `list_upload` string[] — Ad-hoc creator handles (TikTok @-names) to include in this automation only. NOT persisted as a Lists row.
    - `lists_selected` string[] — IDs of existing Lists (vault_files.resource_id) whose creators should be included. Use `POST /lists` to upload reusable lists.
    - `filters` CreatorFilters — Dynamic creator selection by attribute. When `is_evergreen=true` on the parent automation, the filter is re-evaluated on every run. Mirrors the portal's "Filters" tab (formDataTransformer.ts:735-824 `buildFilters` for the legacy `config.Filters` shape, and `buildAISearchFilters` for the AI-search variant). The translator splits AI-only fields out into `config.ai_search_filters` and sets `config.uses_ai_filters=true` accordingly. All fields are optional — set only the ones you care about. An empty `CreatorFilters({})` is invalid (use `lists` mode instead).
      - `product_categories` string[], nullable — TikTok Shop product category names (e.g. 'Beauty & Personal Care').
      - `follower_count` FilterRange — Inclusive numeric range. At least one of `min` / `max` must be set; the other is open-ended. Used by `CreatorFilters` for every range-style filter (GMV, follower count, average views, engagement rate, etc.). API exposes `{min, max}` objects; the translator converts to the legacy `["custom:min-max"]` string form `config.Filters` expects.
        - `min` number, nullable
        - `max` number, nullable
      - `creator_ethnicity` string[], nullable
      - `gender` string[], nullable
      - `age_range` string[], nullable — Age buckets, e.g. ['18-24', '25-34']. Buckets are platform-defined.
      - `languages` string[], nullable
      - `community` string[], nullable — Community hashtags the creators post under. Available where the community-hashtag pipeline has coverage (currently US/UK/DE/ES) — GET /ai-search/hashtags is the live source of truth and returns valid hashtags for your region (404 where unsupported); a leading '#' is optional. Resolves the same audience the portal's community filter builds — the lever behind large niche segments (e.g. fitness) that product category alone can't target.
      - `gmv` FilterRange — Inclusive numeric range. At least one of `min` / `max` must be set; the other is open-ended. Used by `CreatorFilters` for every range-style filter (GMV, follower count, average views, engagement rate, etc.). API exposes `{min, max}` objects; the translator converts to the legacy `["custom:min-max"]` string form `config.Filters` expects.
        - `min` number, nullable
        - `max` number, nullable
      - `units_sold` FilterRange — Inclusive numeric range. At least one of `min` / `max` must be set; the other is open-ended. Used by `CreatorFilters` for every range-style filter (GMV, follower count, average views, engagement rate, etc.). API exposes `{min, max}` objects; the translator converts to the legacy `["custom:min-max"]` string form `config.Filters` expects.
        - `min` number, nullable
        - `max` number, nullable
      - `average_views` FilterRange — Inclusive numeric range. At least one of `min` / `max` must be set; the other is open-ended. Used by `CreatorFilters` for every range-style filter (GMV, follower count, average views, engagement rate, etc.). API exposes `{min, max}` objects; the translator converts to the legacy `["custom:min-max"]` string form `config.Filters` expects.
        - `min` number, nullable
        - `max` number, nullable
      - `engagement_rate` FilterRange — Inclusive numeric range. At least one of `min` / `max` must be set; the other is open-ended. Used by `CreatorFilters` for every range-style filter (GMV, follower count, average views, engagement rate, etc.). API exposes `{min, max}` objects; the translator converts to the legacy `["custom:min-max"]` string form `config.Filters` expects.
        - `min` number, nullable
        - `max` number, nullable
      - `post_rate` FilterRange — Inclusive numeric range. At least one of `min` / `max` must be set; the other is open-ended. Used by `CreatorFilters` for every range-style filter (GMV, follower count, average views, engagement rate, etc.). API exposes `{min, max}` objects; the translator converts to the legacy `["custom:min-max"]` string form `config.Filters` expects.
        - `min` number, nullable
        - `max` number, nullable
      - `video_gpm` FilterRange — Inclusive numeric range. At least one of `min` / `max` must be set; the other is open-ended. Used by `CreatorFilters` for every range-style filter (GMV, follower count, average views, engagement rate, etc.). API exposes `{min, max}` objects; the translator converts to the legacy `["custom:min-max"]` string form `config.Filters` expects.
        - `min` number, nullable
        - `max` number, nullable
      - `live_gpm` FilterRange — Inclusive numeric range. At least one of `min` / `max` must be set; the other is open-ended. Used by `CreatorFilters` for every range-style filter (GMV, follower count, average views, engagement rate, etc.). API exposes `{min, max}` objects; the translator converts to the legacy `["custom:min-max"]` string form `config.Filters` expects.
        - `min` number, nullable
        - `max` number, nullable
      - `follower_age` string[], nullable
      - `follower_gender` string[], nullable
      - `face_visibility` string[], nullable
      - `content_style` string[], nullable
      - `production_quality` string[], nullable
      - `personality_tone` string[], nullable
      - `body_type` string[], nullable
      - `hair_type` string[], nullable
    - `crm_group_id` string, nullable — Single CRM group ID (UUID string). Membership is dynamic — the runtime re-evaluates on every send. Mutually exclusive with the other two modes. Use `GET /crm-groups` to discover available groups (returns the canonical IDs to pass here).
    - `crm_group_entered_after` string, date, nullable — Optional CRM-group filter — only include members who entered the group ON or AFTER this date. Ignored unless `crm_group_id` is set.
    - `crm_group_entered_before` string, date, nullable — Optional CRM-group filter — only include members who entered the group ON or BEFORE this date. Ignored unless `crm_group_id` is set.
  - `creators_to_exclude` CreatorsToExclude — Outreach-paradigm recipient exclusion. Mirrors `creators_to_omit` in the legacy DB column / frontend payload, plus the boolean `exclude_previously_messaged` flag from the portal's Exclude Creators panel.
    - `list_upload` string[]
    - `lists_selected` string[]
    - `crm_groups` string[] — UUIDs of existing CRM Groups whose current members should be excluded. Use `GET /crm-groups` to discover.
    - `exclude_previously_messaged` boolean — Skip creators this shop has DM'd before in any prior automation. Maps to config.exclude_previously_messaged.
  - `target_collab` TargetCollabConfig, required — TC-specific config — invitation metadata, commission rates, sample policy, content type, support contact. Maps to the legacy TC config blob shape (UPPERCASE root keys: BASE_INVITATION_NAME, VALID_UNTIL, TARGET_COLLAB_MESSAGE, EMAIL, PHONE_NUMBER, OFFER_FREE_SAMPLES, AUTO_APPROVE, PRODUCTS) — the worker reads from those, so the translator flattens this nested model accordingly.
    - `invitation_name` string, required — Name shown on the TC invitation card (max 30 chars). Required by the portal — without it, the card displays empty. Persists as `config.BASE_INVITATION_NAME`.
    - `valid_until` string, date, nullable — Date the TC card stops being valid. After this, the dispatcher stops the automation (see `internal/queries/automations_to_run.py`). Null = no expiry. Distinct from `end_date` (top-level automation stop date) — they can differ. Persists as `config.VALID_UNTIL` in MM/DD/YYYY format (legacy).
    - `message` string, required — TC card message text shown to the creator (max 500 chars). Different from `messages[]` (initial outreach DM addons) — this is the body on the invitation card itself. The portal rejects bodies containing the word `amazon`. Persists as `config.TARGET_COLLAB_MESSAGE`.
    - `products` TargetCollabProduct[], required
      - `product_id` string, required — TikTok Shop product ID.
      - `commission_rate` number, required — Affiliate commission rate (0.0-1.0, e.g. 0.20 for 20%).
      - `shop_min_commission` number, nullable
      - `shop_ads_commission_rate` number, nullable — Optional Shop Ads (co-funded) commission rate (0.0-1.0). Persisted into the legacy PRODUCTS blob; the TC runtime fills it on the TC form when present.
    - `content_type` 'no_preference' | 'shoppable_video' | 'shoppable_live' — Content type preference for this TC. Translates to the legacy DB enum (`No preference` / `Shoppable video` / `Shoppable LIVE`) at the persistence boundary.
    - `sample_policy` TargetCollabSamplePolicy — TC sample policy. Three modes from the portal: 1. Manual approval — `offer_free_samples=False, auto_approve=False` 2. Free samples + manual approval — `offer_free_samples=True, auto_approve=False` 3. Free samples + auto approval — `offer_free_samples=True, auto_approve=True` `offer_free_samples=False, auto_approve=True` is also accepted but rare in prod (~0% — see audit 2026-05-01).
      - `offer_free_samples` boolean
      - `auto_approve` boolean — When true, accepted invites trigger immediate sample-request auto-approval without manual review. Maps to top-level `config.AUTO_APPROVE`. Per architecture review 2026-05-01.
      - `send_to` 'brand_recipient' | 'mall_recipient'
    - `support_contact` TargetCollabContact — Brand-side contact info displayed on the TC card so the creator can reach the merchant with questions. `email` is required by the portal at TC create time; `phone` is optional. Persists at the legacy config root as `EMAIL` / `PHONE_NUMBER`.
      - `email` string, required — Contact email shown to the creator on the TC card.
      - `phone` string, nullable — E.164 format recommended.
  - `messages` MessageAddon[] — Initial-outreach addons — text, images, product cards, etc. ALL items in this list are sent immediately together when the automation engages a creator. Use `follow_ups` for delayed follow-up steps. Empty messages = invite-only TC.
    - `type` 'message' | 'image' | 'text_with_image' | 'product_card' | 'spark_code_form' | 'target_invite', required
    - `body` string, nullable — Message body (REQUIRED for type=message and type=text_with_image). Supports `{creator_name}` placeholder.
    - `image_url` string, nullable — Hosted HTTPS URL of the image (REQUIRED for type=image and type=text_with_image). Reacher fetches this URL server-side and re-hosts on the internal CDN — file uploads (multipart) are NOT accepted. Reject if `http://`, `data:`, `blob:`, or scheme-relative.
    - `product_id` string, nullable — Product ID (REQUIRED for type=product_card; OPTIONAL for type=target_invite).
    - `submission_url_slug` string, nullable — Spark code form slug (REQUIRED for type=spark_code_form).
  - `follow_ups` FollowUpStep[] — Delayed follow-up steps. Each step has `delay_days` after the prior step (or after the initial outreach for the first follow-up) and one or more addons that fire together. Use this to build a multi-touch sequence.
    - `delay_days` integer, required — Days to wait after the prior step (or after initial outreach for the first follow-up).
    - `addons` MessageAddon[], required — Addons sent together when this follow-up step fires. Only `message` and `image` addon types are supported in follow-ups; `product_card`, `spark_code_form`, and `target_invite` are initial-outreach-only — use `messages` for those.
      - `type` 'message' | 'image' | 'text_with_image' | 'product_card' | 'spark_code_form' | 'target_invite', required
      - `body` string, nullable — Message body (REQUIRED for type=message and type=text_with_image). Supports `{creator_name}` placeholder.
      - `image_url` string, nullable — Hosted HTTPS URL of the image (REQUIRED for type=image and type=text_with_image). Reacher fetches this URL server-side and re-hosts on the internal CDN — file uploads (multipart) are NOT accepted. Reject if `http://`, `data:`, `blob:`, or scheme-relative.
      - `product_id` string, nullable — Product ID (REQUIRED for type=product_card; OPTIONAL for type=target_invite).
      - `submission_url_slug` string, nullable — Spark code form slug (REQUIRED for type=spark_code_form).
  - `ai_enabled` boolean
  - `end_date` string, date, nullable
  - `auto_resolve_conflicts` 'SKIP_ALL' | 'MOVE_NOT_ACCEPTED' | 'MOVE_ALL' — How the runtime handles creators who already have a pending TC invite from another automation. SKIP_ALL leaves them untouched; MOVE_NOT_ACCEPTED (default — matches portal-created automations) re-targets creators whose prior invite was never accepted; MOVE_ALL re-targets regardless of prior invite state. Maps to `config.AUTO_RESOLVE_CONFLICTS`.
  - `is_evergreen` boolean — When true, the automation continuously re-evaluates its `creators_to_include.filters` and queues newly-qualifying creators on each run. Has no effect for `lists` mode (lists are static) or `crm_group` mode (CRM groups are inherently evergreen). Maps to top-level `Automations.is_evergreen` column.
  - `business_hours_timezone` string, nullable — When set, the runtime restricts outbound message delivery (initial outreach + follow-ups) to 9am–9pm in this IANA timezone (e.g. `America/Los_Angeles`). Null disables the restriction — messages can fire at any time within the schedule's daily window. The 9–9 window is fixed; only the timezone is configurable. Distinct from `schedule.timezone`, which governs *when the automation evaluates its queue* (per CORE-3031).

## Response `201`

Successful Response

- AutomationWriteResponseV2
  - `data` AutomationResourceV2, required — Canonical write-endpoint response. Replaces the v1 AutomationResource (which mirrored the legacy single-shape body). Now reflects the four-type taxonomy — `automation_type` is the public-API type, not the DB string. The DB string is in `_internal_type` for debugging only.
    - `automation_id` integer, nullable — None on dry-run.
    - `shop_id` integer, required
    - `automation_name` string, required
    - `automation_type` string, required — One of: target_collab | dm | email | sample_request.
    - `automation_status` string, required
    - `state` string, required
    - `config` object — Full config as stored. Type-specific.
    - `schedule` object, nullable
    - `creators_to_include` object, nullable
    - `creators_to_exclude` object, nullable
    - `crm_group_id` string, nullable
    - `ai_enabled` boolean
    - `end_date` string, nullable
    - `created_at` string, nullable
    - `updated_at` string, nullable
    - `created_via` string, nullable — Surface that created the automation. Always populated on response: rows with NULL or empty `created_via` (predating migration 020) are surfaced as `portal` since the portal was historically the only writer. Common values: portal | api | api_direct | mcp | agent | slack | internal | campaign_tc_auto (set is not closed — other internal surfaces may appear). `api_direct` marks single-creator invites created by `POST /public/v1/target-collabs`.
    - `side_effects` object, nullable — Backend side-effects, e.g. {'crm_group_created': '<uuid>'} on sample_request creates.
  - `dry_run` boolean
  - `side_effects` object, nullable — Backend side-effects, e.g. {'crm_group_created': 'uuid-...'} for sample_request creates.

## Other responses

- `400` — Validation, missing/invalid Idempotency-Key, or forbidden update field.
- `403` — API key lacks `read_write` scope (`WRITE_NOT_PERMITTED`).
- `404` — Automation/list/group not in this shop.
- `409` — Idempotency conflict or archived row.
- `422` — Validation Error
- `429` — Write-tier rate limit (10/min, 50/hr, 100/day) exceeded.

---

[API](https://skmtc.dev/reacherapp/apis/reacher-data-api.md) · [All operations](https://skmtc.dev/reacherapp/apis/reacher-data-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/reacherapp/reacher-data-api/revisions/5eefff009b5b/schema)
