# Organization API: Org Staff Marketing: Staff

> The 45 Organization API operations for staff.

Source: https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/

Part of [Org Staff Marketing](/docs/api/reference/organizations/org-staff-marketing/).

## Operations

| Method | Path | Summary |
| --- | --- | --- |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/ads`](#op-get-api-v1-orgs-org-slug-staff-marketing-ads) | Get ads |
| DELETE | [`/api/v1/orgs/{org_slug}/staff/marketing/ads/connections/{connection_id}`](#op-delete-api-v1-orgs-org-slug-staff-marketing-ads-connections-connection-id) | Disconnect one ad account |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/ads/connections/{connection_id}/sync`](#op-post-api-v1-orgs-org-slug-staff-marketing-ads-connections-connection-id-sync) | Read one ad account now |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/ads/return`](#op-get-api-v1-orgs-org-slug-staff-marketing-ads-return) | Get ad return |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/ads/spend`](#op-get-api-v1-orgs-org-slug-staff-marketing-ads-spend) | What was spent, grouped by platform, campaign or day |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/ads/spend/manual`](#op-get-api-v1-orgs-org-slug-staff-marketing-ads-spend-manual) | Spend entered by hand, newest first |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/ads/spend/manual`](#op-post-api-v1-orgs-org-slug-staff-marketing-ads-spend-manual) | Enter spend for a channel that has no connection |
| DELETE | [`/api/v1/orgs/{org_slug}/staff/marketing/ads/spend/manual/{entry_id}`](#op-delete-api-v1-orgs-org-slug-staff-marketing-ads-spend-manual-entry-id) | Remove an entry of spend entered by hand |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/ads/{platform}/accounts`](#op-post-api-v1-orgs-org-slug-staff-marketing-ads-platform-accounts) | Pick which of the platform's accounts to read |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/ads/{platform}/connect`](#op-post-api-v1-orgs-org-slug-staff-marketing-ads-platform-connect) | Start signing in to an ad platform |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/announcements`](#op-get-api-v1-orgs-org-slug-staff-marketing-announcements) | Read the Discord webhook settings for announcements |
| PUT | [`/api/v1/orgs/{org_slug}/staff/marketing/announcements`](#op-put-api-v1-orgs-org-slug-staff-marketing-announcements) | Save or remove the Discord webhook for announcements |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/announcements/history`](#op-get-api-v1-orgs-org-slug-staff-marketing-announcements-history) | Read the announcements that were posted |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/announcements/post`](#op-post-api-v1-orgs-org-slug-staff-marketing-announcements-post) | Post an announcement to Discord |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/campaigns`](#op-get-api-v1-orgs-org-slug-staff-marketing-campaigns) | List the campaign links |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/campaigns`](#op-post-api-v1-orgs-org-slug-staff-marketing-campaigns) | Save a campaign link |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/campaigns/preview`](#op-post-api-v1-orgs-org-slug-staff-marketing-campaigns-preview) | Check a campaign link and build its address without saving it |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/campaigns/results`](#op-get-api-v1-orgs-org-slug-staff-marketing-campaigns-results) | Read what each campaign brought in |
| PATCH | [`/api/v1/orgs/{org_slug}/staff/marketing/campaigns/{campaign_id}`](#op-patch-api-v1-orgs-org-slug-staff-marketing-campaigns-campaign-id) | Change or archive a campaign link |
| DELETE | [`/api/v1/orgs/{org_slug}/staff/marketing/campaigns/{campaign_id}`](#op-delete-api-v1-orgs-org-slug-staff-marketing-campaigns-campaign-id) | Delete a campaign link |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/campaigns/{campaign_id}/results`](#op-get-api-v1-orgs-org-slug-staff-marketing-campaigns-campaign-id-results) | Read what one campaign brought in |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/conversions`](#op-get-api-v1-orgs-org-slug-staff-marketing-conversions) | List the ad platforms purchases are sent to, and how each is doing |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/conversions/deliveries`](#op-get-api-v1-orgs-org-slug-staff-marketing-conversions-deliveries) | Read what each platform was sent, and why anything was not |
| PUT | [`/api/v1/orgs/{org_slug}/staff/marketing/conversions/{kind}`](#op-put-api-v1-orgs-org-slug-staff-marketing-conversions-kind) | Turn an ad platform on or off and change its settings |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/conversions/{kind}/rotate-feed-token`](#op-post-api-v1-orgs-org-slug-staff-marketing-conversions-kind-rotate-feed-token) | Replace the token in the Microsoft Ads file address |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/conversions/{kind}/test`](#op-post-api-v1-orgs-org-slug-staff-marketing-conversions-kind-test) | Send a made-up purchase to an ad platform in test mode |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/flows`](#op-get-api-v1-orgs-org-slug-staff-marketing-flows) | List the automatic email flows, with their settings and how many emails each sent |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/flows/messages`](#op-get-api-v1-orgs-org-slug-staff-marketing-flows-messages) | Read the log of flow emails, newest first |
| PUT | [`/api/v1/orgs/{org_slug}/staff/marketing/flows/{key}`](#op-put-api-v1-orgs-org-slug-staff-marketing-flows-key) | Turn an email flow on or off and change its settings |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/flows/{key}/test`](#op-post-api-v1-orgs-org-slug-staff-marketing-flows-key-test) | Send a flow's sample email to yourself |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/referrals`](#op-get-api-v1-orgs-org-slug-staff-marketing-referrals) | List the organization's referrals and what the program has paid |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/referrals/codes`](#op-get-api-v1-orgs-org-slug-staff-marketing-referrals-codes) | List referral codes, customers' and partners' |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/referrals/codes`](#op-post-api-v1-orgs-org-slug-staff-marketing-referrals-codes) | Make a partner code for a customer |
| PATCH | [`/api/v1/orgs/{org_slug}/staff/marketing/referrals/codes/{code_id}`](#op-patch-api-v1-orgs-org-slug-staff-marketing-referrals-codes-code-id) | Stop or restart a code, or change a partner's commission or label |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/referrals/{referral_id}/void`](#op-post-api-v1-orgs-org-slug-staff-marketing-referrals-referral-id-void) | Void a referral that has not been paid |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/reviews`](#op-get-api-v1-orgs-org-slug-staff-marketing-reviews) | Read the Trustpilot review settings and how the invitations are doing |
| PUT | [`/api/v1/orgs/{org_slug}/staff/marketing/reviews`](#op-put-api-v1-orgs-org-slug-staff-marketing-reviews) | Change the Trustpilot review settings |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/subscribers`](#op-get-api-v1-orgs-org-slug-staff-marketing-subscribers) | List the newsletter's leads and count who can be mailed |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/subscribers/export.csv`](#op-get-api-v1-orgs-org-slug-staff-marketing-subscribers-export-csv) | Download the addresses that can be mailed as a CSV file |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/subscribers/suppressions`](#op-get-api-v1-orgs-org-slug-staff-marketing-subscribers-suppressions) | List the addresses that get no marketing email |
| POST | [`/api/v1/orgs/{org_slug}/staff/marketing/subscribers/suppressions`](#op-post-api-v1-orgs-org-slug-staff-marketing-subscribers-suppressions) | Add an address to the suppression list |
| DELETE | [`/api/v1/orgs/{org_slug}/staff/marketing/subscribers/suppressions/{suppression_id}`](#op-delete-api-v1-orgs-org-slug-staff-marketing-subscribers-suppressions-suppression) | Take an address off the suppression list |
| DELETE | [`/api/v1/orgs/{org_slug}/staff/marketing/subscribers/{lead_id}`](#op-delete-api-v1-orgs-org-slug-staff-marketing-subscribers-lead-id) | Delete a lead |
| GET | [`/api/v1/orgs/{org_slug}/staff/marketing/tags`](#op-get-api-v1-orgs-org-slug-staff-marketing-tags) | The tags the storefront loads, and what an editor needs to draw them |
| PUT | [`/api/v1/orgs/{org_slug}/staff/marketing/tags`](#op-put-api-v1-orgs-org-slug-staff-marketing-tags) | Replace the tags the storefront loads with the ones sent |

### Get ads {#op-get-api-v1-orgs-org-slug-staff-marketing-ads}

`GET /api/v1/orgs/{org_slug}/staff/marketing/ads`

Every ad platform with its status, every connected ad account, and the last 30 days of spend.

Needs the Admin or Owner role. ``platforms`` lists Google Ads, Meta Ads, TikTok Ads, Microsoft Ads and Reddit
Ads. A platform's ``status`` is `not_configured` (Coritan has not added its app credentials, so it cannot be
connected), `not_connected`, `pending` (signed in, no account picked yet), `connected`, `failing` (an
account has had errors and no successful read for an hour) or `reauth` (the sign-in ended, so connect
again). ``connections`` has one entry for each ad account the sign-in found, with the same status words
(an account that is found but not picked reads `pending`), when it was last read, the last error, how far
its 13 month history has been read and its spend per currency. ``summary`` is the last 30 days of spend per
currency, with entered spend included. ``warning`` is set when a sign-in ends within 14 days. Nothing here
carries a token.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Disconnect one ad account {#op-delete-api-v1-orgs-org-slug-staff-marketing-ads-connections-connection-id}

`DELETE /api/v1/orgs/{org_slug}/staff/marketing/ads/connections/{connection_id}`

Disconnect one ad account.

Needs the Admin or Owner role. The account's saved sign-in is deleted and nothing is read from it again. The
spend already imported stays in your reports. Connect the platform again to read the account again.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `connection_id` | path | integer | yes |
| `org_slug` | path | string | yes |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Read one ad account now {#op-post-api-v1-orgs-org-slug-staff-marketing-ads-connections-connection-id-sync}

`POST /api/v1/orgs/{org_slug}/staff/marketing/ads/connections/{connection_id}/sync`

Read one ad account now.

Needs the Admin or Owner role. With no body it reads today and the two days before. With ``from`` and ``to``
it reads those days again (at most 400, none before 13 months ago), which is how to refresh a month after the
platform adjusts its numbers. The read runs in the background: check ``last_sync_at`` on the account.
Answers 409 when the account's sign-in has ended or the account is not picked, and 429 after 4 requests an
hour for one account.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `connection_id` | path | integer | yes |
| `org_slug` | path | string | yes |

#### Request body

`application/json`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string (date) or null | no | The first day to read again. Send it with `to`, or send neither. |
| `to` | string (date) or null | no | The last day to read again. |

#### Responses

| Status | Meaning |
| --- | --- |
| `202` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Get ad return {#op-get-api-v1-orgs-org-slug-staff-marketing-ads-return}

`GET /api/v1/orgs/{org_slug}/staff/marketing/ads/return`

What the ad spend returned: orders, new customers, revenue, CAC and ROAS per platform, and per campaign.

Needs the Admin or Owner role. An order counts for the platform its last touch came from, when its invoice is
paid and it is not free: the touch's source and medium name the platform, and when they do not, its click ids
do. Within a platform an order counts for the campaign whose name (or id) equals its `utm_campaign`, ignoring
case. ``new_customers`` counts customers whose first paid order it was. ``roas`` is revenue without tax over
spend, ``cac_minor`` is spend over new customers, and ``platform_roas`` is the value the platform reports over
spend. Each is empty when its divisor is zero. Rows are per currency: spend and revenue meet only when they
are in the same currency, so an ad account billed in one currency and customers paying in another show as
separate rows with no CAC or ROAS. Payback is not computed, because it needs a gross margin that is not kept.
``truncated`` is true when more than 50,000 orders were in the range.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `from` | query | string (date) or null | no | The first day, as YYYY-MM-DD. The default is 30 days before `to`. |
| `to` | query | string (date) or null | no | The last day, as YYYY-MM-DD, included. The default is today in the organization's time zone. |
| `group` | query | string | no | `platform` or `campaign`. Default: `platform`. |
| `currency` | query | string or null | no | Only this currency. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### What was spent, grouped by platform, campaign or day {#op-get-api-v1-orgs-org-slug-staff-marketing-ads-spend}

`GET /api/v1/orgs/{org_slug}/staff/marketing/ads/spend`

What was spent, grouped by platform, campaign or day.

Needs the Admin or Owner role. Every row is in one currency, and a platform that spent in two currencies has
two rows: amounts in different currencies are never added. ``totals`` is the whole range per currency, even
when ``limit`` (rows for each currency, by campaign) cuts the rows. Spend entered by hand is included, spread evenly over its days. The range is at
most 400 days. Days follow each ad account's own time zone (Microsoft Ads and Reddit Ads follow the
organization's), so a day's spend can differ a little from the platform's own report in another zone.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `from` | query | string (date) or null | no | The first day, as YYYY-MM-DD. The default is 30 days before `to`. |
| `to` | query | string (date) or null | no | The last day, as YYYY-MM-DD, included. The default is today in the organization's time zone. |
| `group` | query | string | no | `platform`, `campaign` or `day`. Default: `platform`. |
| `currency` | query | string or null | no | Only this currency. |
| `limit` | query | integer | no | Rows to return for each currency when grouping by campaign. Default: `100`. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Spend entered by hand, newest first {#op-get-api-v1-orgs-org-slug-staff-marketing-ads-spend-manual}

`GET /api/v1/orgs/{org_slug}/staff/marketing/ads/spend/manual`

Spend entered by hand, newest first.

Needs the Admin or Owner role. ``ignored`` is true for an entry whose channel has since been connected, which
reports leave out because the platform's own numbers cover it.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `limit` | query | integer | no | Entries to return, newest first. Default: `100`. |
| `offset` | query | integer | no | Entries to skip. Default: `0`. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Enter spend for a channel that has no connection {#op-post-api-v1-orgs-org-slug-staff-marketing-ads-spend-manual}

`POST /api/v1/orgs/{org_slug}/staff/marketing/ads/spend/manual`

Enter spend for a channel that has no connection.

Needs the Admin or Owner role. ``amount_minor`` is the whole amount over ``starts_on`` to ``ends_on`` (at most
400 days), and reports spread it evenly over those days. A platform with a picked account, or one that was already
read for some of these days, is refused with 409 because its own numbers cover it. Answers 422 for a
channel, currency, amount or dates that do not fit.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `channel` | string | yes | Where the money was spent: a platform key or another network such as x, linkedin, snapchat, pinterest or other. |
| `label` | string | yes | What the spend was for, in your words. |
| `currency` | string | yes | The three letter code of the currency it was paid in. |
| `amount_minor` | integer | yes | The whole amount over the days, in the currency's smallest unit (cents for USD). |
| `starts_on` | string (date) | yes | The first day the spend covers. |
| `ends_on` | string (date) | yes | The last day the spend covers. |

#### Responses

| Status | Meaning |
| --- | --- |
| `201` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Remove an entry of spend entered by hand {#op-delete-api-v1-orgs-org-slug-staff-marketing-ads-spend-manual-entry-id}

`DELETE /api/v1/orgs/{org_slug}/staff/marketing/ads/spend/manual/{entry_id}`

Remove an entry of spend entered by hand. Needs the Admin or Owner role.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `entry_id` | path | integer | yes |
| `org_slug` | path | string | yes |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Pick which of the platform's accounts to read {#op-post-api-v1-orgs-org-slug-staff-marketing-ads-platform-accounts}

`POST /api/v1/orgs/{org_slug}/staff/marketing/ads/{platform}/accounts`

Pick which of the platform's accounts to read.

Needs the Admin or Owner role. ``account_ids`` is the whole list: accounts you name start being read (their
13 month history first), and accounts of this platform you leave out stop being read but keep the spend
already imported. At most 50 accounts for one platform. Answers 422 for an account that is not on the
sign-in. Pick only the accounts that belong to this organization: a sign-in to a manager account can list
accounts that are not yours.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `platform` | path | string | yes | One of google, meta, tiktok, microsoft, reddit. |
| `org_slug` | path | string | yes |  |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `account_ids` | array of string | yes | The accounts to read, by the platform's own ids. Accounts of the platform that are not listed stop being read. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Start signing in to an ad platform {#op-post-api-v1-orgs-org-slug-staff-marketing-ads-platform-connect}

`POST /api/v1/orgs/{org_slug}/staff/marketing/ads/{platform}/connect`

Start signing in to an ad platform. Answers ``authorization_url``, the platform's own page.

Needs the Admin or Owner role. Send the person to ``authorization_url``. The platform sends the browser back
to Coritan, which finds the accounts the sign-in can read and returns the person to ``return_path`` (a page of
the console on the host of this request) with `ads=connected` or `ads=error` in the address. The link is good
for 15 minutes. Answers 409 `not_configured` when the platform's app credentials have not been added to
Coritan, and 422 when the request does not come from one of your console's hosts.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `platform` | path | string | yes | One of google, meta, tiktok, microsoft, reddit. |
| `org_slug` | path | string | yes |  |

#### Request body

`application/json`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `return_path` | string or null | no | The page of the console to come back to when the sign-in is done, such as /staff/marketing/ads. It is a path on the host the request came from. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Read the Discord webhook settings for announcements {#op-get-api-v1-orgs-org-slug-staff-marketing-announcements}

`GET /api/v1/orgs/{org_slug}/staff/marketing/announcements`

Whether a Discord webhook is saved for announcements, and its channel label.

Needs the Admin or Owner role. `configured` says whether a webhook address is saved, `channel_label` is the
name your team gave the channel (null for none) and `webhook_hint` is the last four characters of the
webhook's ID, so you can tell which webhook is saved. The address is a secret and is never answered.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Save or remove the Discord webhook for announcements {#op-put-api-v1-orgs-org-slug-staff-marketing-announcements}

`PUT /api/v1/orgs/{org_slug}/staff/marketing/announcements`

Save the Discord webhook address and the channel label. Send only what changes.

Needs the Admin or Owner role. `webhook_url` must be an `https` address on Discord's own hosts that starts
with `https://discord.com/api/webhooks/`, or 422 answers with `detail` and `field`. It is sealed and never
shown again, and null or an empty string removes it. `channel_label` is at most 80 characters. The answer
is the one `GET` gives. Saving what is already saved changes nothing and writes no audit entry. A change is
recorded as `announcements.updated` with the settings that changed and never the address.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `webhook_url` | string or null | no | The Discord webhook address, which starts with `https://discord.com/api/webhooks/`. It is sealed and never shown again. Null or empty removes the saved one. Left out, the saved one stays. |
| `channel_label` | string or null | no | A name for the channel, such as `#announcements`, shown to your team. At most 80 characters. Null or empty removes it. Left out, the saved one stays. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Read the announcements that were posted {#op-get-api-v1-orgs-org-slug-staff-marketing-announcements-history}

`GET /api/v1/orgs/{org_slug}/staff/marketing/announcements/history`

The announcements posted or refused, newest first.

Needs the Admin or Owner role. Each entry has `status` (`posted` or `failed`), the `title`, `content` and
`link` that were sent, the `channel_label` then saved, `error` (why a failed post failed, never what
Discord said), `actor_type` (`staff`, `user` or `admin` for platform staff), `actor` (`{id, name}`, null
when it is unknown) and `created_at`. The webhook address is never in an entry.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `limit` | query | integer | no | Announcements to return, newest first, 1 to 100. Left out, 20. Default: `20`. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Post an announcement to Discord {#op-post-api-v1-orgs-org-slug-staff-marketing-announcements-post}

`POST /api/v1/orgs/{org_slug}/staff/marketing/announcements/post`

Post one message to the saved Discord webhook.

Needs the Admin or Owner role. `content` is the message, 1 to 1,800 characters. `title` (at most 120
characters) becomes a bold first line, and `link` (an `https` address) goes after the message. The title,
text and link together fit Discord's 2,000 characters, or 422 answers. Nothing in the message can mention
a role or a person. The answer is `{posted: true}`. 409 means no webhook is saved. 502 means Discord did
not take the message, with `detail` saying so in a sentence, and nothing was posted. At most 10 messages
an hour for the organization, and the 11th answers 429. Every attempt is recorded in the audit log as
`announcement.posted` or `announcement.failed` with the words and the channel label, never the address.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `content` | string or null | no | The message, 1 to 1,800 characters. Discord's own formatting works in it. Nothing in it can mention a role or a person. |
| `title` | string or null | no | A bold first line, at most 120 characters. |
| `link` | string or null | no | A link added after the message. It starts with `https://`, at most 500 characters. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### List the campaign links {#op-get-api-v1-orgs-org-slug-staff-marketing-campaigns}

`GET /api/v1/orgs/{org_slug}/staff/marketing/campaigns`

The organization's saved campaign links, newest first.

Needs the Billing, Admin or Owner role. A campaign is a landing page with the UTM words that tag it and an
optional coupon. Every campaign has all its fields, `archived` (true once it was archived) and `url`, the
full link to copy: the storefront address, `base_path`, the UTM words in the order `utm_source`,
`utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, then `coupon`. The words are percent encoded. An
organization with no storefront address gets the path and query alone. `count` is how many campaigns
match before `limit` and `offset` apply, and `max_campaigns` is the most an organization keeps.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `status` | query | string | no | `all` (the default), `active` or `archived`. Default: `all`. |
| `q` | query | string or null | no | Only campaigns whose name or campaign word holds this text, ignoring capitals. |
| `limit` | query | integer | no | Campaigns to return, 1 to 200. Left out, 100. Default: `100`. |
| `offset` | query | integer | no | Campaigns to skip, for the next page. Default: `0`. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Save a campaign link {#op-post-api-v1-orgs-org-slug-staff-marketing-campaigns}

`POST /api/v1/orgs/{org_slug}/staff/marketing/campaigns`

Save a campaign link and answer it, with its `url`.

Needs the Billing, Admin or Owner role. `name`, `utm_source`, `utm_medium` and `utm_campaign` are
required. A bad value answers 422 with `detail`, the sentence to show, and `field`, the input it belongs
to. A name another campaign already has, in any capitals, answers 409 with `field` `name`. An
organization keeps at most 500 campaigns, and the 501st answers 409. A `coupon_code` must be a coupon of
this organization. The change is recorded in the audit log as `campaign.created`.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string or null | no | What you call the campaign, at most 120 characters. Each campaign in an organization has its own name, and two names that differ only in capitals count as one. |
| `base_path` | string or null | no | The page the link opens: a path on your storefront that starts with `/`, at most 400 characters, such as `/pricing`. It holds no host, scheme, fragment, backslash, space or control character, and at most one `?`. A query in it is kept and the UTM words go after it. Left out when you create a campaign, it is `/`. |
| `utm_source` | string or null | no | Where the visit comes from, such as `newsletter`. At most 80 characters, with no `&`, `=`, `#`, `?` or control character. |
| `utm_medium` | string or null | no | How the link is delivered, such as `email`. At most 80 characters, with the same limits as the source. |
| `utm_campaign` | string or null | no | The word the reports group by, at most 120 characters, with the same limits as the source. A visit or order whose campaign is this word belongs to the campaign, whatever its capitals. |
| `utm_term` | string or null | no | The keyword or audience, at most 120 characters. Empty or null for none. |
| `utm_content` | string or null | no | Tells two links of one campaign apart, at most 120 characters. Empty or null for none. |
| `coupon_code` | string or null | no | A coupon of this organization that the link applies, as `?coupon=`. It must exist. Empty or null for none. |
| `notes` | string or null | no | A note for your team, at most 500 characters. Empty or null for none. |

#### Responses

| Status | Meaning |
| --- | --- |
| `201` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Check a campaign link and build its address without saving it {#op-post-api-v1-orgs-org-slug-staff-marketing-campaigns-preview}

`POST /api/v1/orgs/{org_slug}/staff/marketing/campaigns/preview`

Build the link for the fields sent, and list what is wrong with them. Saves nothing.

Needs the Billing, Admin or Owner role. Send the same body as a save; `name` may be left out. The answer
is `{url, problems}`. `problems` is a list of `{field, detail}` with every problem found, and `url` is null
while there is one. The check is the one a save makes, a repeated name and an unknown coupon included.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string or null | no | What you call the campaign, at most 120 characters. Each campaign in an organization has its own name, and two names that differ only in capitals count as one. |
| `base_path` | string or null | no | The page the link opens: a path on your storefront that starts with `/`, at most 400 characters, such as `/pricing`. It holds no host, scheme, fragment, backslash, space or control character, and at most one `?`. A query in it is kept and the UTM words go after it. Left out when you create a campaign, it is `/`. |
| `utm_source` | string or null | no | Where the visit comes from, such as `newsletter`. At most 80 characters, with no `&`, `=`, `#`, `?` or control character. |
| `utm_medium` | string or null | no | How the link is delivered, such as `email`. At most 80 characters, with the same limits as the source. |
| `utm_campaign` | string or null | no | The word the reports group by, at most 120 characters, with the same limits as the source. A visit or order whose campaign is this word belongs to the campaign, whatever its capitals. |
| `utm_term` | string or null | no | The keyword or audience, at most 120 characters. Empty or null for none. |
| `utm_content` | string or null | no | Tells two links of one campaign apart, at most 120 characters. Empty or null for none. |
| `coupon_code` | string or null | no | A coupon of this organization that the link applies, as `?coupon=`. It must exist. Empty or null for none. |
| `notes` | string or null | no | A note for your team, at most 500 characters. Empty or null for none. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Read what each campaign brought in {#op-get-api-v1-orgs-org-slug-staff-marketing-campaigns-results}

`GET /api/v1/orgs/{org_slug}/staff/marketing/campaigns/results`

What every saved campaign brought in over a period, and the campaign words no saved campaign names.

Needs the Billing, Admin or Owner role. The period is `from` to `to` in the brand's time zone, the last 30
days when left out, and at most 366 days. A campaign owns the visits, sign-ups and orders whose
`utm_campaign` is its own, ignoring capitals. The figures are the acquisition report's: a visit is a stored
session with bots left out, a sign-up is a customer whose last touch named the campaign, an order is a paid
order that is not free, and `revenue_minor` is the invoice total without tax in `currency`'s minor units.
Orders and revenue are counted in one currency and never added across currencies: `currencies` names every
currency with an order in the period. `conversion_rate_bp` is orders over visits in basis points, so 123 is
1.23 percent. `visits_complete` is false when the period starts more than 180 days ago, because sessions are
kept for 180 days. `unmatched` lists the campaign words of the period that no saved campaign names, the
biggest first, at most `limit`.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `from` | query | string (date) or null | no | First day of the period, YYYY-MM-DD, in the brand's time zone. Left out, 29 days before the last day, so the period is the last 30 days. A period is at most 366 days. |
| `to` | query | string (date) or null | no | Last day of the period, YYYY-MM-DD, in the brand's time zone. Left out, today. |
| `currency` | query | string or null | no | A three letter currency code. Orders and revenue are counted in this one currency and never added across currencies. Left out, the currency with the most revenue in the period, else the brand's own. |
| `limit` | query | integer | no | Campaign words in `unmatched`, 1 to 200. Left out, 20. The saved campaigns are always all listed. Default: `20`. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Change or archive a campaign link {#op-patch-api-v1-orgs-org-slug-staff-marketing-campaigns-campaign-id}

`PATCH /api/v1/orgs/{org_slug}/staff/marketing/campaigns/{campaign_id}`

Change the fields of a campaign that were sent, or archive it with `archived`, and answer it.

Needs the Billing, Admin or Owner role. Only the fields sent are checked and changed, by the rules of a
save. A null or empty `utm_term`, `utm_content`, `coupon_code` or `notes` clears it. Saving what is
already there changes nothing and writes no audit entry. A change is recorded as `campaign.updated` with
the names of the fields that changed. A campaign of another organization answers 404.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `campaign_id` | path | integer | yes |
| `org_slug` | path | string | yes |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string or null | no | What you call the campaign, at most 120 characters. Each campaign in an organization has its own name, and two names that differ only in capitals count as one. |
| `base_path` | string or null | no | The page the link opens: a path on your storefront that starts with `/`, at most 400 characters, such as `/pricing`. It holds no host, scheme, fragment, backslash, space or control character, and at most one `?`. A query in it is kept and the UTM words go after it. Left out when you create a campaign, it is `/`. |
| `utm_source` | string or null | no | Where the visit comes from, such as `newsletter`. At most 80 characters, with no `&`, `=`, `#`, `?` or control character. |
| `utm_medium` | string or null | no | How the link is delivered, such as `email`. At most 80 characters, with the same limits as the source. |
| `utm_campaign` | string or null | no | The word the reports group by, at most 120 characters, with the same limits as the source. A visit or order whose campaign is this word belongs to the campaign, whatever its capitals. |
| `utm_term` | string or null | no | The keyword or audience, at most 120 characters. Empty or null for none. |
| `utm_content` | string or null | no | Tells two links of one campaign apart, at most 120 characters. Empty or null for none. |
| `coupon_code` | string or null | no | A coupon of this organization that the link applies, as `?coupon=`. It must exist. Empty or null for none. |
| `notes` | string or null | no | A note for your team, at most 500 characters. Empty or null for none. |
| `archived` | boolean or null | no | `true` archives the campaign and `false` brings it back. An archived campaign keeps its link and its results. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Delete a campaign link {#op-delete-api-v1-orgs-org-slug-staff-marketing-campaigns-campaign-id}

`DELETE /api/v1/orgs/{org_slug}/staff/marketing/campaigns/{campaign_id}`

Delete a campaign link. The visits and orders that carried its words stay in the reports.

Needs the Billing, Admin or Owner role. Archive a campaign instead to keep its link and its results in the
list. A campaign of another organization answers 404. Recorded as `campaign.deleted`.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `campaign_id` | path | integer | yes |
| `org_slug` | path | string | yes |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Read what one campaign brought in {#op-get-api-v1-orgs-org-slug-staff-marketing-campaigns-campaign-id-results}

`GET /api/v1/orgs/{org_slug}/staff/marketing/campaigns/{campaign_id}/results`

What one campaign brought in over a period and over the period before it.

Needs the Billing, Admin or Owner role. The figures and their definitions are those of `GET results`.
`totals` holds `visits`, `signups`, `orders`, `new_customers`, `revenue_minor` and `conversion_rate_bp`, and
`totals.previous` holds the same for the comparison period, or null when `compare` is `none`. A campaign of
another organization answers 404.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `campaign_id` | path | integer | yes |  |
| `org_slug` | path | string | yes |  |
| `from` | query | string (date) or null | no | First day of the period, YYYY-MM-DD, in the brand's time zone. Left out, 29 days before the last day, so the period is the last 30 days. A period is at most 366 days. |
| `to` | query | string (date) or null | no | Last day of the period, YYYY-MM-DD, in the brand's time zone. Left out, today. |
| `compare` | query | string or null | no | `previous` (the default) sets every total against the same number of days just before the period, and a calendar month against the month before it. `none` leaves the comparison out. Default: `previous`. |
| `currency` | query | string or null | no | A three letter currency code. Orders and revenue are counted in this one currency and never added across currencies. Left out, the currency with the most revenue in the period, else the brand's own. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### List the ad platforms purchases are sent to, and how each is doing {#op-get-api-v1-orgs-org-slug-staff-marketing-conversions}

`GET /api/v1/orgs/{org_slug}/staff/marketing/conversions`

Every ad platform the organization can send purchases to, with its settings and how it is doing.

Needs the Admin or Owner role. There is one entry for each of ``ga4``, ``google_ads``, ``meta_capi``,
``tiktok_events``, ``microsoft_ads``, ``reddit_ads`` and ``x_ads``, whether or not it was ever set up.
An entry has ``enabled``, ``test_mode``, ``config`` (the settings that are not secret), ``secret_set``
(whether a secret is saved; the secret is never answered), ``fields`` and ``secret`` (what a form for
the kind asks), ``last_delivery`` (the newest event the kind was sent, with its status) and
``coverage``: ``orders`` is how many first purchases were paid in the last 30 days, ``sent`` how many
of them the kind was sent, and ``rate`` is ``sent`` over ``orders`` (null with no orders).
Microsoft Ads has no secret. Its entry has ``feed_url_masked``, the address of its file with the token
hidden, and when Microsoft Advertising last fetched it.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Read what each platform was sent, and why anything was not {#op-get-api-v1-orgs-org-slug-staff-marketing-conversions-deliveries}

`GET /api/v1/orgs/{org_slug}/staff/marketing/conversions/deliveries`

What each platform was sent, newest first, and why anything was not.

Needs the Admin or Owner role. Every row is one event for one platform: ``event`` is ``purchase``
(a first purchase), ``renewal``, ``upgrade``, ``free_server``, ``sign_up``, ``refund`` or
``chargeback``; ``status`` is ``sent``, ``skipped`` (``error`` says why: the buyer did not allow
marketing, the platform has no such event, renewals are off) or ``failed`` (``http_status`` and
``error`` hold the platform's answer, and a timeout or a server error is tried again, three
attempts in all). ``invoice`` names the invoice the row belongs to. Filter by ``kind``, ``status``
and ``invoice_number``; ``count`` is the total that match.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `kind` | query | string or null | no | ga4, google_ads, meta_capi, tiktok_events, microsoft_ads, reddit_ads or x_ads. |
| `status` | query | string or null | no | sent, failed or skipped. |
| `invoice_number` | query | string or null | no | Only the deliveries of this invoice, its refunds and its chargebacks. |
| `limit` | query | integer | no | Default: `50`. |
| `offset` | query | integer | no | Default: `0`. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Turn an ad platform on or off and change its settings {#op-put-api-v1-orgs-org-slug-staff-marketing-conversions-kind}

`PUT /api/v1/orgs/{org_slug}/staff/marketing/conversions/{kind}`

Turn a platform on or off, set its test mode, change its settings, set or clear its secret.

Needs the Admin or Owner role. Send only what changes. ``config`` keys replace the saved ones and a
null or empty string removes one; ``labels`` (Google Ads) and ``events`` (X) replace the whole map.
``secret`` is write only: it is sealed, never answered and never written to the audit log, and an
empty string clears it. X takes an object of ``consumer_key``, ``consumer_secret``, ``access_token``
and ``access_token_secret``. Google Ads and Microsoft Ads take no secret, and a secret for them
answers 422. A value that does not fit, or a platform turned on without what it needs, answers 422
with ``detail``, the sentence to show, and ``field``, the input it belongs to. Every ID is checked
as the analytics and ad tags check theirs.

Turning Microsoft Ads on the first time makes the address of its file, and only that answer carries
the whole address in ``feed_url``. Copy it into a schedule in Microsoft Advertising. Later answers
show it with the token hidden. The answer is ``{integration, changed, feed_url}``. Each change is
recorded in the audit log without any secret.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `kind` | path | string | yes | ga4, google_ads, meta_capi, tiktok_events, microsoft_ads, reddit_ads or x_ads. |
| `org_slug` | path | string | yes |  |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean or null | no | Turn the integration on or off. It needs its settings and secret first (422 names the first one missing). |
| `test_mode` | boolean or null | no | Send every event as a test: Meta and TikTok with the test event code, Google Analytics to its validation endpoint, Reddit with its test flag. Google Ads and X have no test mode. |
| `config` | object or null | no | Settings to change. A key replaces the stored one and null removes it. labels and events replace the whole map. |
| `secret` | string or object or null | no | The platform's secret. Write only: it is sealed and never answered, only secret_set. An empty string clears it. X takes an object of its four OAuth values. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Replace the token in the Microsoft Ads file address {#op-post-api-v1-orgs-org-slug-staff-marketing-conversions-kind-rotate-feed-token}

`POST /api/v1/orgs/{org_slug}/staff/marketing/conversions/{kind}/rotate-feed-token`

Replace the token in the Microsoft Ads file address, so the old address stops working.

Needs the Admin or Owner role. The answer is ``{integration, feed_url}`` and the only one that shows
the new address whole. Put it in the schedule in Microsoft Advertising; from now on the old address
answers 404. Purchases the file already listed stay listed. Answers 409 before Microsoft Ads was
first turned on, since that makes the first address.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `kind` | path | string | yes | microsoft_ads, the one platform that fetches a file from Coritan. |
| `org_slug` | path | string | yes |  |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Send a made-up purchase to an ad platform in test mode {#op-post-api-v1-orgs-org-slug-staff-marketing-conversions-kind-test}

`POST /api/v1/orgs/{org_slug}/staff/marketing/conversions/{kind}/test`

Send one made-up purchase to a platform in test mode and answer what the platform said.

Needs the Admin or Owner role. The event is for one currency unit of a product called "Test
product" and carries nothing about a person. Meta and TikTok send their test event code, Google
Analytics its validation endpoint and Reddit its test flag, so nothing reaches live reporting. The
platform needs its settings and secret saved first (422 names the missing one). Google Ads and X have
no test mode, so a test would count as a real sale and answers 422, and Microsoft Ads fetches its
file and takes no events. At most ten test events an hour for each platform (429). The test is not
logged as a delivery and is not counted in coverage. The answer is ``{kind, event, status, ok,
http_status, error, event_id}``.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `kind` | path | string | yes | ga4, google_ads, meta_capi, tiktok_events, microsoft_ads, reddit_ads or x_ads. |
| `org_slug` | path | string | yes |  |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### List the automatic email flows, with their settings and how many emails each sent {#op-get-api-v1-orgs-org-slug-staff-marketing-flows}

`GET /api/v1/orgs/{org_slug}/staff/marketing/flows`

Every automatic email flow of the organization, whether or not it was ever turned on.

Needs the Admin or Owner role. There is one entry for each of ``unpaid_invoice``, ``stuck_setup``,
``free_upgrade``, ``winback``, ``onboarding`` and ``review_invite``. An entry has its ``label``, ``kind``
(``service`` flows are about the customer's own order and need no marketing consent, ``marketing`` flows
go only to customers who agreed to marketing email) and ``description``; ``enabled`` and ``config``, the
saved settings with the defaults filled in; ``fields``, what a form for the settings asks (``key``,
``label``, ``kind``, ``default``, ``min``, ``max``, ``hint``); ``template_slugs``, one for each step;
``blocked``, the sentence that says why the flow cannot send even when on (the review invitation needs a
saved Trustpilot review link), or null; and ``stats``, how many emails are ``scheduled``, ``sent``,
``skipped``, ``failed`` or ``canceled`` and ``last_sent_at``. Every flow is off until turned on. One customer
gets at most one flow email in 3 days, from any flow.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Read the log of flow emails, newest first {#op-get-api-v1-orgs-org-slug-staff-marketing-flows-messages}

`GET /api/v1/orgs/{org_slug}/staff/marketing/flows/messages`

Every email a flow scheduled, newest first, with what became of it.

Needs the Admin or Owner role. A row has ``flow``, ``step``, ``status``, ``email``, ``scheduled_at``,
``sent_at``, ``customer_id`` and ``error``. ``scheduled`` waits for the next pass (at most ten minutes),
``sent`` went to the mail server, ``skipped`` was left out on purpose (``error`` says why: the address
unsubscribed, the account is not active, another flow email went to the customer in the last 3 days, or
it was due more than a day ago), ``canceled`` lost its reason before it went (the invoice was paid, the
server is running, the customer withdrew consent, the flow was turned off) and ``failed`` was refused by
the mail server. ``count`` is the total that match, before ``limit`` and ``offset``.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `flow` | query | string or null | no | Only this flow: unpaid_invoice, stuck_setup, free_upgrade, winback, onboarding or review_invite. |
| `status` | query | string or null | no | Only this status: scheduled, sent, skipped, failed or canceled. |
| `limit` | query | integer | no | How many rows to answer, 1 to 200. Default: `50`. |
| `offset` | query | integer | no | How many rows to skip. Default: `0`. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Turn an email flow on or off and change its settings {#op-put-api-v1-orgs-org-slug-staff-marketing-flows-key}

`PUT /api/v1/orgs/{org_slug}/staff/marketing/flows/{key}`

Turn a flow on or off and change its settings. Send only what changes.

Needs the Admin or Owner role. ``config`` keys replace the saved ones; GET lists them in ``fields`` with
their ranges. ``coupon_code`` (win-back) takes a coupon of the organization, or null to remove it. A value
that does not fit answers 422 with ``detail``, the sentence to show, and ``field``, the input it belongs
to (``config.after_days``, or ``enabled`` for the review invitation without a saved Trustpilot review
link), and changes nothing. The answer is ``{flow, changed}``: the flow as GET lists it and the names that
differ. Saving what is already saved changes nothing and writes no audit entry. A change is audited as
``flows.updated`` with the settings before and after. A flow starts mailing at its next pass, within ten
minutes, and only customers who qualify from then on.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `key` | path | string | yes | The flow: unpaid_invoice, stuck_setup, free_upgrade, winback, onboarding or review_invite. |
| `org_slug` | path | string | yes |  |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean or null | no | Turn the flow on or off. The review invitation can only be turned on once a Trustpilot review link is saved and on (422 otherwise). |
| `config` | object or null | no | The flow's settings to change, by name, as GET lists them in `fields`. A value outside its range answers 422 and changes nothing. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Send a flow's sample email to yourself {#op-post-api-v1-orgs-org-slug-staff-marketing-flows-key-test}

`POST /api/v1/orgs/{org_slug}/staff/marketing/flows/{key}/test`

Send the flow's email, filled with made-up facts, to the address of the signed-in member.

Needs the Admin or Owner role. The test goes out whether or not the flow is on, as service mail, with the
brand's own links and the flow's coupon code, and writes nothing to the flow log. An address on the
organization's unsubscribe list gets nothing (409). `step` picks the email of a flow with two (422 for a
step the flow lacks). The member needs an address on their account (400). At most ten test emails an hour
for one member (429). The answer is ``{sent, to, flow, step, template}``; 502 when the mail server did not
accept it. Audited as ``flows.test_sent``.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `key` | path | string | yes | The flow: unpaid_invoice, stuck_setup, free_upgrade, winback, onboarding or review_invite. |
| `org_slug` | path | string | yes |  |

#### Request body

`application/json`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `step` | string or null | no | Which email of the flow to send, `1` or `2`. The default is `1`. Only unpaid_invoice and onboarding have a second step. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### List the organization's referrals and what the program has paid {#op-get-api-v1-orgs-org-slug-staff-marketing-referrals}

`GET /api/v1/orgs/{org_slug}/staff/marketing/referrals`

Every referral of the organization, newest first, and the program's totals.

Needs the Admin or Owner role. A referral is one referred customer: their first paid invoice started it.
``status`` is ``qualified`` while it waits out its hold (``hold_until``), ``credited`` once the wallet credit
is paid, and ``void`` when a rule refused it (``void_reason`` says which, or holds the reason staff gave).
``referrer`` and ``referee`` name the two customers, ``code`` the code that was used and ``invoice`` the
invoice that started it. ``reward_amount`` is what the referrer is paid and ``referee_reward_amount`` the
welcome credit, both in wallet dollars (``currency``). ``count`` is how many referrals match the filters.
``totals`` covers the whole program and ignores the filters: the number of referrals in each status,
``credited_amount`` paid out so far and ``owed_amount`` for the open ones.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `status` | query | string or null | no | pending, qualified, credited or void. |
| `q` | query | string or null | no | Part of either customer's email address, without regard to capitals. |
| `limit` | query | integer | no | Referrals to return, newest first. At most 200. Default: `50`. |
| `offset` | query | integer | no | Referrals to skip. Default: `0`. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### List referral codes, customers' and partners' {#op-get-api-v1-orgs-org-slug-staff-marketing-referrals-codes}

`GET /api/v1/orgs/{org_slug}/staff/marketing/referrals/codes`

The organization's referral codes, newest first, each with how many referrals it started.

Needs the Admin or Owner role. A ``customer`` code is made for each customer the first time they open their
referral page. A ``partner`` code is made by your team and pays ``commission_bp`` of the referred customer's
first invoice. ``referrals`` on a code is ``{total, credited, earned}``: the referrals it started, how many
were paid, and what they paid the owner in wallet dollars.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `kind` | query | string or null | no | customer or partner. |
| `active` | query | boolean or null | no | true for codes that work, false for stopped ones. |
| `q` | query | string or null | no | Part of the code, its label or the owner's email address. |
| `limit` | query | integer | no | Codes to return, newest first. At most 200. Default: `50`. |
| `offset` | query | integer | no | Codes to skip. Default: `0`. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Make a partner code for a customer {#op-post-api-v1-orgs-org-slug-staff-marketing-referrals-codes}

`POST /api/v1/orgs/{org_slug}/staff/marketing/referrals/codes`

Make a partner code that pays a share of the first invoice of every customer it brings.

Needs the Admin or Owner role. Name the owner with ``customer_id`` or ``email``: an active customer of this
organization. ``commission_bp`` (1 to 5000, 100 is 1 percent) is the share of the referred customer's first
invoice, without tax, paid into the owner's wallet once the hold has passed. ``code`` is optional: 3 to 40
letters, digits, hyphens or underscores, and without regard to capitals it must not be in use (409). Left out,
we make an 8 character code. At most 30 codes an hour. Answers 422 with ``detail`` and ``field`` for a value
that does not fit. The code is recorded in the audit log as ``referral_code.created``.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `customer_id` | boolean or integer or string or null | no | The customer who owns the code. Give this or email. |
| `email` | string or null | no | The email address of the customer who owns the code. Give this or customer_id. |
| `code` | string or null | no | The code, 3 to 40 letters, digits, hyphens or underscores. Left out, we make an 8 character one. |
| `commission_bp` | boolean or integer or string or null | no | The share of the referred customer's first invoice, without tax, the owner is paid, in basis points: 1 to 5000 (100 is 1 percent). |
| `label` | string or null | no | A note for your team, such as the partner's name. |

#### Responses

| Status | Meaning |
| --- | --- |
| `201` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Stop or restart a code, or change a partner's commission or label {#op-patch-api-v1-orgs-org-slug-staff-marketing-referrals-codes-code-id}

`PATCH /api/v1/orgs/{org_slug}/staff/marketing/referrals/codes/{code_id}`

Change whether a code works, a partner code's commission, or its label. The code itself never changes.

Needs the Admin or Owner role. ``active: false`` stops the code from starting new referrals; referrals it
already started still pay. ``commission_bp`` (1 to 5000) is for a partner code only: a customer's code pays
the program's reward amount. ``label`` is a note for your team, and null or an empty string removes it. Send
only what changes. Answers 404 for a code of another organization and 422 with ``detail`` and ``field`` for a
value that does not fit. A change is recorded as ``referral_code.updated`` with the values before and after.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `code_id` | path | integer | yes | The code's id. |
| `org_slug` | path | string | yes |  |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `active` | boolean or null | no | false stops the code from starting new referrals. Referrals it already has still pay. |
| `commission_bp` | boolean or integer or string or null | no | A partner code's share in basis points: 1 to 5000. |
| `label` | string or null | no | A note for your team. null or an empty string removes it. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Void a referral that has not been paid {#op-post-api-v1-orgs-org-slug-staff-marketing-referrals-referral-id-void}

`POST /api/v1/orgs/{org_slug}/staff/marketing/referrals/{referral_id}/void`

Void a referral so it is never paid. Only a ``pending`` or ``qualified`` referral can be voided.

Needs the Admin or Owner role. ``reason`` is required, 1 to 120 characters, and is kept as the referral's
``void_reason``. A referral that is already ``credited`` or ``void`` answers 409, and money already paid is not
taken back. Answers 404 for a referral of another organization. At most 60 an hour. Recorded in the audit log
as ``referral.voided`` with the reason.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `referral_id` | path | integer | yes | The referral's id. |
| `org_slug` | path | string | yes |  |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `reason` | string or null | no | Why the referral is void, 1 to 120 characters. It is kept with the referral. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Read the Trustpilot review settings and how the invitations are doing {#op-get-api-v1-orgs-org-slug-staff-marketing-reviews}

`GET /api/v1/orgs/{org_slug}/staff/marketing/reviews`

The Trustpilot review settings, and how the review invitation emails are doing.

Needs the Admin or Owner role. `enabled` is whether the storefront asks signed-in customers for a review,
`review_url` the Trustpilot link it opens (null for none), `min_account_age_days` how old an account is
before it asks, and `dismiss_days` how long it stays quiet after a customer closes the prompt. The
invitation is the email flow `review_invite`, which uses the same link. `invitations` holds `enabled`
(the flow's own switch), the number of its messages `scheduled`, `sent`, `skipped` and `failed` (all 0
until the flow has run) and `last_sent_at`.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Change the Trustpilot review settings {#op-put-api-v1-orgs-org-slug-staff-marketing-reviews}

`PUT /api/v1/orgs/{org_slug}/staff/marketing/reviews`

Change the review settings. Send only what changes.

Needs the Admin or Owner role. `review_url` must start with `https://` and be on `trustpilot.com`, at most
500 characters, and the prompt cannot be turned on without one. `min_account_age_days` is 0 to 365 and
`dismiss_days` 1 to 365. A value that does not fit answers 422 with `detail`, the sentence to show, and
`field`, the setting it belongs to. The answer is the one `GET` gives, with `changed` listing the settings
that differ from before. Saving what is already saved changes nothing and writes no audit entry. A change
is recorded as `reviews.updated`, and the storefront applies it to new page loads within a minute.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean or null | no | Turn the review prompt on or off. It needs `review_url`, and an on switch with no link answers 422. |
| `review_url` | string or null | no | Your Trustpilot review link. It starts with `https://` and is on `trustpilot.com`, at most 500 characters. Null or empty removes it. |
| `min_account_age_days` | integer or null | no | How many days old an account is before the prompt asks, 0 to 365. The default is 1. |
| `dismiss_days` | integer or null | no | How many days the prompt stays quiet after a customer closes it, 1 to 365. The default is 30. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### List the newsletter's leads and count who can be mailed {#op-get-api-v1-orgs-org-slug-staff-marketing-subscribers}

`GET /api/v1/orgs/{org_slug}/staff/marketing/subscribers`

The addresses visitors gave the storefront's newsletter form, newest first.

Needs the Admin or Owner role. Every row is one address: ``id``, ``email``, ``source`` (where it was
collected), ``consent`` (whether the visitor agreed to email), ``consent_text`` (the words they agreed
to), ``customer_id`` (the customer with that address, if there is one), ``suppressed`` (true when the
address is on the suppression list and gets no marketing email whatever its consent says) and
``created_at``. Filter by part of the address with ``q``, by ``source`` and by ``consent``; ``total`` is
every lead the filters match. ``counts`` covers the whole organization: ``leads`` (every lead),
``consenting_customers`` (active customers who agreed to marketing email) and ``suppressed`` (addresses
on the suppression list).

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `q` | query | string or null | no | Part of the address. |
| `source` | query | string or null | no | Only leads that came from this source. |
| `consent` | query | boolean or null | no | true for leads who agreed to email, false for the rest. |
| `limit` | query | integer | no | Default: `50`. |
| `offset` | query | integer | no | Default: `0`. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Download the addresses that can be mailed as a CSV file {#op-get-api-v1-orgs-org-slug-staff-marketing-subscribers-export-csv}

`GET /api/v1/orgs/{org_slug}/staff/marketing/subscribers/export.csv`

Every address the organization may send marketing email to, as a CSV file.

Needs the Admin or Owner role. The columns are ``email``, ``source``, ``consent``, ``kind`` (``lead`` or
``customer``) and ``created_at``. The file lists leads who agreed to email and active customers who
agreed to marketing email, once for each address (an address that is both is a lead). It never lists an
address on the suppression list. A cell that starts with ``=``, ``+``, ``-`` or ``@`` begins with an
apostrophe so a spreadsheet does not run it. A file holds at most 100,000 rows and the answer's
``X-Export-Truncated: true`` header says when a list was longer. At most 12 exports an hour (429). Every
export is audited as ``subscribers.exported`` before the file is answered.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### List the addresses that get no marketing email {#op-get-api-v1-orgs-org-slug-staff-marketing-subscribers-suppressions}

`GET /api/v1/orgs/{org_slug}/staff/marketing/subscribers/suppressions`

The organization's suppression list, newest first.

Needs the Admin or Owner role. An address on the list gets no marketing email, whatever its owner's
consent says. Every row has ``id``, ``email``, ``reason`` (``unsubscribed``, ``bounced``,
``complained`` or ``manual``), ``source`` (``link``, ``account``, ``console`` or ``api``) and
``created_at``. Filter by part of the address with ``q``; ``total`` is every row that matches. The list
belongs to this organization alone: an address another brand suppressed is not here.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `org_slug` | path | string | yes |  |
| `q` | query | string or null | no | Part of the address. |
| `limit` | query | integer | no | Default: `50`. |
| `offset` | query | integer | no | Default: `0`. |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Add an address to the suppression list {#op-post-api-v1-orgs-org-slug-staff-marketing-subscribers-suppressions}

`POST /api/v1/orgs/{org_slug}/staff/marketing/subscribers/suppressions`

Stop marketing email to one address.

Needs the Admin or Owner role. ``reason`` must be ``manual`` (the other reasons are set by an
unsubscribe, a bounce or a complaint). The row's source is ``console``. Adding an address cancels the
flow emails still waiting for it, takes marketing consent back from a customer with that address and
marks a lead with it as not consenting. An address already on the list stays as it was and the answer
has ``added: false``. A bad address or reason answers ``422 {"detail", "field"}``. The answer is
``{suppression, added}`` and a new entry is audited as ``suppression.added``.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Request body

`application/json` (required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | yes | The address that gets no marketing email. |
| `reason` | string | no | Why it is on the list. A person adds an address with the reason "manual". |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Take an address off the suppression list {#op-delete-api-v1-orgs-org-slug-staff-marketing-subscribers-suppressions-suppression}

`DELETE /api/v1/orgs/{org_slug}/staff/marketing/subscribers/suppressions/{suppression_id}`

Let marketing email reach an address again.

Needs the Admin or Owner role. This removes the row only: it does not give anyone's consent back, so a
customer or lead who withdrew still has to agree again before they get marketing email. 404 for an id
that is not on this organization's list. Audited as ``suppression.lifted``.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `suppression_id` | path | integer | yes | The id of a row of the suppression list. |
| `org_slug` | path | string | yes |  |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Delete a lead {#op-delete-api-v1-orgs-org-slug-staff-marketing-subscribers-lead-id}

`DELETE /api/v1/orgs/{org_slug}/staff/marketing/subscribers/{lead_id}`

Delete one lead for good.

Needs the Admin or Owner role. Only the lead row goes: the customer with that address, their consent
and the suppression list are not touched, so an address that unsubscribed stays unsubscribed. 404 for an
id that is not this organization's lead. Audited as ``subscriber.deleted`` with the lead's id and
source, never its address.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `lead_id` | path | integer | yes | The id of a lead. |
| `org_slug` | path | string | yes |  |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### The tags the storefront loads, and what an editor needs to draw them {#op-get-api-v1-orgs-org-slug-staff-marketing-tags}

`GET /api/v1/orgs/{org_slug}/staff/marketing/tags`

The tags the storefront loads, and what an editor needs to draw them.

Needs the Admin or Owner role. ``tags`` holds the platforms that are on, each
with the public ids saved for it; a platform with no id is off and absent.
``kinds`` lists every platform with its label, a sentence about it and its
fields (``key``, ``label``, ``hint``, ``pattern``, ``example``), so a form can
be drawn from it. ``consent`` says whether the storefront shows its cookie
banner and which page the banner links to. ``legacy_google_analytics`` is true
while the Google Analytics ID shown still comes from the older single setting,
which the next save replaces.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |

### Replace the tags the storefront loads with the ones sent {#op-put-api-v1-orgs-org-slug-staff-marketing-tags}

`PUT /api/v1/orgs/{org_slug}/staff/marketing/tags`

Replace the tags the storefront loads with the ones sent.

Needs the Admin or Owner role. ``tags`` is the whole object: a platform left
out is turned off. Each id is trimmed and checked against the shape its
platform uses, and a platform with no id is left out. The answer is the one
``GET`` gives, with ``changed`` listing the platforms that differ from before.
A bad id answers 422 with ``detail``, the sentence to show, and ``field``, the
input it belongs to (``meta.pixel_id``). Saving what is already stored changes
nothing. Every change is recorded in the audit log with the ids before and
after. Tags apply to new page loads within a minute.

#### Parameters

| Name | In | Type | Required |
| --- | --- | --- | --- |
| `org_slug` | path | string | yes |

#### Request body

`application/json` (required)

| Field | Type | Required |
| --- | --- | --- |
| `tags` | Tags | yes |

#### Responses

| Status | Meaning |
| --- | --- |
| `200` | Success. |
| `422` | The request is not valid. `detail` lists each problem. |
