---
title: "Create DM automation"
method: POST
path: "/automations/dm"
tags: ["Automations"]
---

# Create DM automation

`POST /automations/dm`

Create a Direct Message automation. The `mode` field selects between 4 variants: `vanilla` (text-only), `with_image`, `with_product_card`, `spark_code` (collect TikTok spark codes via form). Each mode has specific addon and config requirements — see model schema.

## Headers

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

## Request body

- AutomationDmRequest — Body for POST /automations/dm. Modes (drives DB `automation_type` mapping): * `vanilla` → `Message` * `with_image` → `Message + Image` (requires image addon) * `with_product_card` → `Message + Product Card` (requires product_card addon) * `spark_code` → `Spark Code` (requires spark_code config) The legacy `refundable_sample_request` mode (`Sample Request Processing` internal type) was REMOVED in the post-frontend-audit revision — production data showed zero rows actually carry the `refundable_sample` config sub-key the model wrote, so the field was dead weight. Customers wanting that flow should use either: * `dm` mode `with_product_card` for a product-card DM, OR * the top-level `sample_request` type for full auto-approval logic.
  - `automation_name` string, required
  - `mode` 'vanilla' | 'with_image' | 'with_product_card' | 'spark_code'
  - `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.
  - `messages` MessageAddon[], required — Initial-outreach addons — text + optional image / product card / spark_code form. ALL items are sent immediately together when the automation engages a creator. Use `follow_ups` for delayed steps.
    - `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 — same shape as TC.
    - `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).
  - `dm_config` DmConfig — DM type-specific config blob, populated based on `mode`.
    - `spark_code` DmSparkCodeConfig — dm-mode=spark_code — TikTok spark code collection form.
      - `only_collect_creator_information` boolean
      - `spark_code_submission_url` string, required — Portal-hosted form URL. Auto-generated if not provided by the route.
  - `personalization` DmPersonalizationConfig — CORE-4857 — enable AI personalization of the initial-outreach DM. When set on ``POST /automations/dm``, the send engine rewrites the DM body per creator using the [Block] tokens in your message text: Content Discussion and Product Discussion are AI-generated from the creator's own signals, while Product Description / Selling Points / Offer are the fixed strings you provide here. A creator with too little signal to personalize receives ``fallback_message`` instead. All copy fields are stored verbatim — there is NO server-side AI at create time — so the create is deterministic and idempotent, and the send path's reuse hash stays stable. Resolve the product blocks beforehand via ``POST /personalization/product-blocks`` and preview any creator via ``POST /personalization/preview`` so what ships is what you approved.
    - `enabled` boolean — Master switch. Set false to persist the config without personalizing sends (e.g. staging copy for later).
    - `tone` 'engaging' | 'bold' | 'inspirational', nullable — AI rewrite tone. Null uses the engine default (engaging).
    - `fallback_message` string, required — Sent when a creator has too little signal to personalize. Required — personalization must always have a safe fallback.
    - `offer_discussion` string — [Offer Discussion] block value — your offer text.
    - `product_description` string — [Product Description] block value. Pre-resolve via POST /personalization/product-blocks.
    - `product_selling_points` string — [Product Selling Points] block value. Pre-resolve via POST /personalization/product-blocks.
    - `word_counts` object, nullable — Optional approximate word-count target per AI block, e.g. {"content_discussion": 25, "product_discussion": 20}. Only the two AI blocks (content_discussion, product_discussion) are honored; values are clamped to a sane range server-side. Null/absent uses the engine brevity default.
  - `ai_enabled` boolean
  - `end_date` string, date, nullable
  - `is_evergreen` boolean — When true, the automation continuously re-evaluates its `creators_to_include.filters` for new qualifying creators on each run. No effect when `creators_to_include.crm_group_id` is set (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. The 9–9 window is fixed; only the timezone is configurable. Distinct from `schedule.timezone`.

## 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/d4fe3e380424/schema)
