# Auto-DM

7 endpoints.

## List Auto-DM campaigns

```http
GET https://orbitwrite.com/api/v1/auto-dms
```

The workspace's Auto-DM campaigns, newest first, each with its rule, window and send counts. Requires `autoDm:read`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `status` (string, optional) — Comma-separated: active, paused, completed, stopped
- `accountId` (string, optional)
- `groupId` (string, optional) — One post's campaigns — one per channel it went out on.
- `cursor` (string, optional)
- `limit` (integer, optional)

### Responses

#### 200

A page of campaigns

- `campaigns` (AutoDmCampaign[], required)
  - `id` (string, required)
  - `status` (string, required, one of `active`, `paused`, `completed`, `stopped`)
  - `groupId` (string | null, required)
  - `postId` (string, required)
  - `postText` (string, required)
  - `platformUrl` (string | null, required)
  - `channel` (object, required)
    - `id` (string, required)
    - `platform` (string, required)
    - `handle` (string | null, required)
    - `displayName` (string | null, required)
    - `avatarUrl` (string | null, required)
  - `rule` (AutoDmRule, required)
    - `message` (string, required) — The DM. `{first_name}` (first name), `{name}` (full name), `{username}` (username), `{action}` (action), `{post_url}` (post url) are filled in per recipient.
    - `triggers` (string[], required) — What earns the message: reposting the post, replying to it, or both.
    - `match` (string, required, one of `all`, `any`) — `all`: every trigger must have happened. `any`: one is enough.
    - `requireFollow` (boolean, required) — Only message people who follow the sending channel.
    - `replyKeyword` (string, optional) — With the `reply` trigger, the reply must contain this text (case-insensitive).
    - `durationDays` (integer, required) — How many days after it starts the campaign keeps looking (1–7).
  - `startsAt` (string, required)
  - `endsAt` (string, required)
  - `lastPolledAt` (string | null, required)
  - `nextPollAt` (string | null, required)
  - `lastError` (string | null, required) — Why the last look for engagers or the last send didn't go through (a reconnect needed, a rate limit, the daily cap). Cleared by the next good poll.
  - `completedAt` (string | null, required)
  - `createdAt` (string, required)
  - `stats` (object, required)
    - `waiting` (number, required) — Engaged, but haven't met the whole rule yet.
    - `sent` (number, required)
    - `failed` (number, required)
  - `dailySendLimit` (number, required)
- `nextCursor` (string | null, required) — Pass as `cursor` to fetch the next page; null when exhausted

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

## Start an Auto-DM on a published post

```http
POST https://orbitwrite.com/api/v1/auto-dms
```

For `durationDays` from now, message everyone who reposts and/or replies to a post that has **already been published** — including people who engaged before it started. To set one up on a post that hasn't gone out yet, put it on the post instead (`autoEngagement.dm` on `POST /posts`) and it starts when the post publishes.

Sent by the channel the post went out on; pass `accountId` when the post went out on several. Requires `autoDm:create` **and** `post:publish` — nothing reviews a campaign started on a live post, so it takes the same authority as publishing. 409 when the post isn't published yet or already has a campaign.

**Authentication:** bearer token or session cookie.

### Request body (`application/json`)

- `message` (string, optional)
- `triggers` (string[], optional)
- `match` (string, optional, one of `all`, `any`)
- `requireFollow` (boolean, optional)
- `replyKeyword` (string, optional)
- `durationDays` (integer, optional)
- `groupId` (string, required)
- `accountId` (string, optional)
- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The campaign

- `id` (string, required)
- `status` (string, required, one of `active`, `paused`, `completed`, `stopped`)
- `groupId` (string | null, required)
- `postId` (string, required)
- `postText` (string, required)
- `platformUrl` (string | null, required)
- `channel` (object, required)
  - `id` (string, required)
  - `platform` (string, required)
  - `handle` (string | null, required)
  - `displayName` (string | null, required)
  - `avatarUrl` (string | null, required)
- `rule` (AutoDmRule, required)
  - `message` (string, required) — The DM. `{first_name}` (first name), `{name}` (full name), `{username}` (username), `{action}` (action), `{post_url}` (post url) are filled in per recipient.
  - `triggers` (string[], required) — What earns the message: reposting the post, replying to it, or both.
  - `match` (string, required, one of `all`, `any`) — `all`: every trigger must have happened. `any`: one is enough.
  - `requireFollow` (boolean, required) — Only message people who follow the sending channel.
  - `replyKeyword` (string, optional) — With the `reply` trigger, the reply must contain this text (case-insensitive).
  - `durationDays` (integer, required) — How many days after it starts the campaign keeps looking (1–7).
- `startsAt` (string, required)
- `endsAt` (string, required)
- `lastPolledAt` (string | null, required)
- `nextPollAt` (string | null, required)
- `lastError` (string | null, required) — Why the last look for engagers or the last send didn't go through (a reconnect needed, a rate limit, the daily cap). Cleared by the next good poll.
- `completedAt` (string | null, required)
- `createdAt` (string, required)
- `stats` (object, required)
  - `waiting` (number, required) — Engaged, but haven't met the whole rule yet.
  - `sent` (number, required)
  - `failed` (number, required)
- `dailySendLimit` (number, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 409

Conflicts with an existing resource

- `error` (string, required)
- `issues` (any[], optional)

## Which channels can send Auto-DMs

```http
GET https://orbitwrite.com/api/v1/auto-dms/channels
```

Every connected channel on a platform that supports Auto-DM (Bluesky, Mastodon), with whether it can send as connected today. `needs_reconnect` means the connection predates the direct-message permission — reconnecting the channel grants it. Also carries each platform's message length and daily send limit. Requires `autoDm:read` and `account:read`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The workspace's Auto-DM-capable channels

- `channels` (AutoDmChannel[], required)
  - `accountId` (string, required)
  - `platform` (string, required, one of `bluesky`, `mastodon`)
  - `handle` (string | null, required)
  - `avatarUrl` (string | null, required)
  - `status` (string, required, one of `ready`, `unsupported`, `needs_reconnect`)
  - `reason` (string, optional)
  - `maxMessageChars` (number, required)
  - `dailySendLimit` (number, required)
  - `repostLabel` (string, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

## Who an Auto-DM campaign has seen and messaged

```http
GET https://orbitwrite.com/api/v1/auto-dms/{campaignId}/recipients
```

Everyone who engaged with the post, newest first: `sent` (with the message as it went out), `failed` (with why — usually the person doesn't accept DMs), `waiting` (engaged but hasn't met the whole rule yet) and `sending` (in flight). Requires `autoDm:read`.

**Authentication:** bearer token or session cookie.

### Path parameters

- `campaignId` (string, required)

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `status` (string, optional) — Comma-separated: waiting, sending, sent, failed
- `cursor` (string, optional)
- `limit` (integer, optional)

### Responses

#### 200

A page of recipients

- `recipients` (AutoDmRecipient[], required)
  - `id` (string, required)
  - `platformUserId` (string, required)
  - `handle` (string, required)
  - `displayName` (string | null, required)
  - `avatarUrl` (string | null, required)
  - `status` (string, required, one of `waiting`, `sending`, `sent`, `failed`)
  - `reposted` (boolean, required)
  - `replied` (boolean, required)
  - `follows` (boolean | null, required)
  - `message` (string | null, required)
  - `error` (string | null, required)
  - `sentAt` (string | null, required)
  - `firstSeenAt` (string, required)
- `nextCursor` (string | null, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

## Stop an Auto-DM campaign for good

```http
POST https://orbitwrite.com/api/v1/auto-dms/{campaignId}/stop
```

Nothing more is sent. The campaign and its recipients stay readable. Idempotent — stopping a finished campaign returns it unchanged. Requires `autoDm:delete`.

**Authentication:** bearer token or session cookie.

### Path parameters

- `campaignId` (string, required)

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The campaign

- `id` (string, required)
- `status` (string, required, one of `active`, `paused`, `completed`, `stopped`)
- `groupId` (string | null, required)
- `postId` (string, required)
- `postText` (string, required)
- `platformUrl` (string | null, required)
- `channel` (object, required)
  - `id` (string, required)
  - `platform` (string, required)
  - `handle` (string | null, required)
  - `displayName` (string | null, required)
  - `avatarUrl` (string | null, required)
- `rule` (AutoDmRule, required)
  - `message` (string, required) — The DM. `{first_name}` (first name), `{name}` (full name), `{username}` (username), `{action}` (action), `{post_url}` (post url) are filled in per recipient.
  - `triggers` (string[], required) — What earns the message: reposting the post, replying to it, or both.
  - `match` (string, required, one of `all`, `any`) — `all`: every trigger must have happened. `any`: one is enough.
  - `requireFollow` (boolean, required) — Only message people who follow the sending channel.
  - `replyKeyword` (string, optional) — With the `reply` trigger, the reply must contain this text (case-insensitive).
  - `durationDays` (integer, required) — How many days after it starts the campaign keeps looking (1–7).
- `startsAt` (string, required)
- `endsAt` (string, required)
- `lastPolledAt` (string | null, required)
- `nextPollAt` (string | null, required)
- `lastError` (string | null, required) — Why the last look for engagers or the last send didn't go through (a reconnect needed, a rate limit, the daily cap). Cleared by the next good poll.
- `completedAt` (string | null, required)
- `createdAt` (string, required)
- `stats` (object, required)
  - `waiting` (number, required) — Engaged, but haven't met the whole rule yet.
  - `sent` (number, required)
  - `failed` (number, required)
- `dailySendLimit` (number, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

## Read one Auto-DM campaign

```http
GET https://orbitwrite.com/api/v1/auto-dms/{campaignId}
```

Its rule, window, status and send counts. Requires `autoDm:read`.

**Authentication:** bearer token or session cookie.

### Path parameters

- `campaignId` (string, required)

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The campaign

- `id` (string, required)
- `status` (string, required, one of `active`, `paused`, `completed`, `stopped`)
- `groupId` (string | null, required)
- `postId` (string, required)
- `postText` (string, required)
- `platformUrl` (string | null, required)
- `channel` (object, required)
  - `id` (string, required)
  - `platform` (string, required)
  - `handle` (string | null, required)
  - `displayName` (string | null, required)
  - `avatarUrl` (string | null, required)
- `rule` (AutoDmRule, required)
  - `message` (string, required) — The DM. `{first_name}` (first name), `{name}` (full name), `{username}` (username), `{action}` (action), `{post_url}` (post url) are filled in per recipient.
  - `triggers` (string[], required) — What earns the message: reposting the post, replying to it, or both.
  - `match` (string, required, one of `all`, `any`) — `all`: every trigger must have happened. `any`: one is enough.
  - `requireFollow` (boolean, required) — Only message people who follow the sending channel.
  - `replyKeyword` (string, optional) — With the `reply` trigger, the reply must contain this text (case-insensitive).
  - `durationDays` (integer, required) — How many days after it starts the campaign keeps looking (1–7).
- `startsAt` (string, required)
- `endsAt` (string, required)
- `lastPolledAt` (string | null, required)
- `nextPollAt` (string | null, required)
- `lastError` (string | null, required) — Why the last look for engagers or the last send didn't go through (a reconnect needed, a rate limit, the daily cap). Cleared by the next good poll.
- `completedAt` (string | null, required)
- `createdAt` (string, required)
- `stats` (object, required)
  - `waiting` (number, required) — Engaged, but haven't met the whole rule yet.
  - `sent` (number, required)
  - `failed` (number, required)
- `dailySendLimit` (number, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

## Pause, resume or change an Auto-DM campaign

```http
PATCH https://orbitwrite.com/api/v1/auto-dms/{campaignId}
```

`status: "paused"` stops sending until `status: "active"` resumes it. Rule fields you send replace the stored ones and apply to the next send — nobody already messaged is messaged again. `durationDays` is measured from the campaign's start. Requires `autoDm:update`. 409 once the campaign has finished or been stopped.

**Authentication:** bearer token or session cookie.

### Path parameters

- `campaignId` (string, required)

### Request body (`application/json`)

- `status` (string, optional, one of `active`, `paused`)
- `message` (string, optional)
- `triggers` (string[], optional)
- `match` (string, optional, one of `all`, `any`)
- `requireFollow` (boolean, optional)
- `replyKeyword` (string | null, optional)
- `durationDays` (integer, optional)
- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The campaign

- `id` (string, required)
- `status` (string, required, one of `active`, `paused`, `completed`, `stopped`)
- `groupId` (string | null, required)
- `postId` (string, required)
- `postText` (string, required)
- `platformUrl` (string | null, required)
- `channel` (object, required)
  - `id` (string, required)
  - `platform` (string, required)
  - `handle` (string | null, required)
  - `displayName` (string | null, required)
  - `avatarUrl` (string | null, required)
- `rule` (AutoDmRule, required)
  - `message` (string, required) — The DM. `{first_name}` (first name), `{name}` (full name), `{username}` (username), `{action}` (action), `{post_url}` (post url) are filled in per recipient.
  - `triggers` (string[], required) — What earns the message: reposting the post, replying to it, or both.
  - `match` (string, required, one of `all`, `any`) — `all`: every trigger must have happened. `any`: one is enough.
  - `requireFollow` (boolean, required) — Only message people who follow the sending channel.
  - `replyKeyword` (string, optional) — With the `reply` trigger, the reply must contain this text (case-insensitive).
  - `durationDays` (integer, required) — How many days after it starts the campaign keeps looking (1–7).
- `startsAt` (string, required)
- `endsAt` (string, required)
- `lastPolledAt` (string | null, required)
- `nextPollAt` (string | null, required)
- `lastError` (string | null, required) — Why the last look for engagers or the last send didn't go through (a reconnect needed, a rate limit, the daily cap). Cleared by the next good poll.
- `completedAt` (string | null, required)
- `createdAt` (string, required)
- `stats` (object, required)
  - `waiting` (number, required) — Engaged, but haven't met the whole rule yet.
  - `sent` (number, required)
  - `failed` (number, required)
- `dailySendLimit` (number, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 409

Conflicts with an existing resource

- `error` (string, required)
- `issues` (any[], optional)
