# Make campaign links and read their results

> Save a landing link with its UTM words and an optional coupon, copy the full address, and read the visits, sign-ups and orders each campaign brought in.

Source: https://www.coritan.com/docs/organizations/campaigns/

In the dashboard:

- /dashboard/organizations/…/traffic/campaigns: https://www.coritan.com/dashboard/organizations

A *campaign link* is an address on your storefront with UTM words added, so the tracker can tell which campaign a visit came from. You save each link once, copy its full address into an email, a post or an ad, and read what it brought in: visits, sign-ups, orders and revenue.

Your team manages campaign links in the staff console, under Marketing, as [Run campaign links and referrals in the staff console](/docs/organizations/staff-console/marketing-campaigns/) describes. The owner and scripts use the API, which this page describes.

## Before you begin

- You need the Billing, Admin or Owner role in the organization. Support and read-only members cannot read or change campaigns.
- Your storefront must be one that Coritan serves, so the tracker records each visit. [How customers and orders are attributed](/docs/organizations/attribution/) explains what it records.
- A campaign that applies a coupon needs that coupon to exist already. Make it first, as [Staff billing and orders](/docs/organizations/staff-console/billing/#manage-coupons) describes.

## Set it up in your dashboard

Owners, admins and billing members save campaign links in the dashboard. Your team can do the same in the staff console, as [Marketing campaigns and referrals in the staff console](/docs/organizations/staff-console/marketing-campaigns/) describes. A member with another role does not see the page, and its address opens **Overview**.

1. In the [dashboard](https://www.coritan.com/dashboard/organizations), open the organization, then **Growth**, then **Campaigns**.
2. Under **New campaign**, fill in "Name", "Landing page", "Source", "Medium" and "Campaign word". The landing page is a path on your storefront, such as `/pricing`. "Term", "Content", "Coupon" and "Notes" are optional, and a coupon must already exist in your store.
3. Read the **Link** box under the fields. It follows what you type and shows the whole address with every word encoded. A field that is wrong shows its reason once you have typed in it. Select the copy button in the box to copy the link. When we know no address for your storefront, the link is the path alone, and the page says to put your storefront's address in front of it.
4. Select **Create campaign**. A name that another campaign has, in any capitals, and a coupon your store does not have are refused with the API's sentence under the field. The page keeps what you typed so you can fix it.
5. Under **Campaigns**, choose the range: "7 days", "30 days", "90 days" or "12 months". Each campaign shows its **Visits**, **Sign-ups**, **Orders**, **Revenue** and **Conversion** for that range. When orders came in more than one currency, a switch for each currency appears beside the range, because money is never added across currencies. On a narrow screen some columns are left out.

When it works, the page says "Summer launch created." and the campaign appears under **Campaigns** with its figures. **Active**, **Archived** and **All** choose which campaigns the list shows, and the search box finds one by its name or its campaign word.

To change a campaign, open the actions menu of its row and select **Edit**. The builder shows the saved fields, and **Save changes** sends only what you changed. **Archive…** takes it out of the **Active** list and keeps its link and its results, **Restore** brings it back, and **Delete…** removes the saved link only. The visits and orders that carried its words stay in your reports. The copy button beside the menu copies a campaign's link without opening it.

**Campaigns in your reports that are not saved** lists the campaign words that visits and orders carried and no saved campaign names. Select **Save as campaign** on a row to fill the builder with that word, then finish the other fields.

## What a campaign holds

| Field | What it is |
| --- | --- |
| `name` | 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` | The page the link opens, such as `/pricing`. It starts with `/` and is at most 400 characters. It holds no scheme, host, fragment (`#`), backslash, space or control character, and at most one `?`. Left out, it is `/`. |
| `utm_source` | Where the visit comes from, such as `newsletter`. At most 80 characters. |
| `utm_medium` | How the link is delivered, such as `email`. At most 80 characters. |
| `utm_campaign` | The word the reports group by. At most 120 characters. |
| `utm_term` | The keyword or audience. At most 120 characters. Optional. |
| `utm_content` | Tells two links of one campaign apart. At most 120 characters. Optional. |
| `coupon_code` | A coupon of your organization that the link applies. Optional. |
| `notes` | A note for your team, at most 500 characters. Optional. |

`name`, `utm_source`, `utm_medium` and `utm_campaign` are required. A UTM word is trimmed, and it cannot hold `&`, `=`, `#` or `?`, or a control character. An organization keeps at most 500 campaigns.

## The link we build

Every campaign has a `url`, the full link to copy. It is your storefront's address, then `base_path`, then the UTM words in the order `utm_source`, `utm_medium`, `utm_campaign`, `utm_term` and `utm_content`, then `coupon` when the campaign has a coupon code. We percent encode every value, so a word with a space or an `&` in it cannot add a parameter of its own.

A `base_path` may already hold a query, such as `/pricing?plan=pro`. We keep it and add the UTM words after it. We take any `utm_` word, and `coupon` when the campaign has a coupon code, out of that query first, so nothing appears twice and the campaign's own words win. The saved `base_path` is never rewritten.

The address in front is your [custom domain](/docs/organizations/settings/#set-your-custom-domain). With none, it is the public address Coritan uses for links in your emails. When we have neither, the link is the path and query alone, such as `/pricing?utm_source=newsletter`, and you add your storefront's address in front of it.

A storefront that Coritan serves reads `coupon` from any page. It keeps the code for the visit and for seven days in that browser, and fills the coupon field at checkout. A code the buyer typed is never replaced, and the code goes when the buyer removes it or an order uses it.

## Save a campaign

1. Check the link first. `POST /campaigns/preview` takes the same body as a save and answers `{url, problems}`. It saves nothing, and `name` may be left out. `problems` lists every `{field, detail}` found, and `url` is `null` while there is one.
2. Save it with `POST /campaigns`. The answer is the campaign with its `url`.

```bash title="Save a newsletter campaign"
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/campaigns" \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Summer launch", "base_path": "/pricing", "utm_source": "newsletter", "utm_medium": "email", "utm_campaign": "summer-launch", "coupon_code": "SAVE10"}'
```

```json
{
  "id": 14,
  "name": "Summer launch",
  "base_path": "/pricing",
  "utm_source": "newsletter",
  "utm_medium": "email",
  "utm_campaign": "summer-launch",
  "utm_term": null,
  "utm_content": null,
  "coupon_code": "SAVE10",
  "notes": null,
  "archived": false,
  "archived_at": null,
  "url": "https://shop.example.com/pricing?utm_source=newsletter&utm_medium=email&utm_campaign=summer-launch&coupon=SAVE10",
  "created_by_user_id": 7,
  "created_at": "2026-09-21T12:00:00Z",
  "updated_at": "2026-09-21T12:00:00Z"
}
```

A value that does not fit answers `422` with `detail`, the sentence to show, and `field`, the input it belongs to:

```json
{ "detail": "The page cannot start with //, which would name another site.", "field": "base_path" }
```

## Change, archive or delete a campaign

`PATCH /campaigns/{campaign_id}` changes the fields you send, by the same rules as 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.

Send `{"archived": true}` to archive a campaign and `{"archived": false}` to bring it back. An archived campaign keeps its link and its results, and `GET /campaigns?status=archived` lists it. Deleting with `DELETE /campaigns/{campaign_id}` removes the saved link only. The visits and orders that carried its words stay in your reports, and the word then appears under `unmatched` in the results.

`GET /campaigns` lists the campaigns, newest first. It takes `status` (`all`, `active` or `archived`), `q` (text in the name or the campaign word), `limit` (1 to 200, 100 by default) and `offset`. `count` is how many match and `max_campaigns` is 500.

## Read what a campaign brought in

`GET /campaigns/results` reads every saved campaign over a period.

| Parameter | What it sets |
| --- | --- |
| `from` and `to` | The first and last day, `YYYY-MM-DD`, both included, in your brand's time zone. The last 30 days when left out. A period is at most 366 days. |
| `currency` | A three letter code. Orders and revenue count in this one currency. When left out, it is the currency with the most revenue in the period, or your brand's own currency when there were no orders. |
| `limit` | The rows in `unmatched`, 1 to 200 and 20 by default. Every saved campaign is always listed. |

A campaign owns the visits, sign-ups and orders whose `utm_campaign` is its own word, ignoring capitals. The figures are the ones the [acquisition report](/docs/organizations/attribution/#read-the-acquisition-report) uses:

- A *visit* is a stored session of your storefront with bots left out. We keep sessions for 180 days, so `visits_complete` is `false` when the period starts earlier. A stored session keeps its campaign word to 100 characters, so a longer word matches the first 100 characters of a visit's word.
- A *sign-up* is a customer made through the storefront whose last visit named the campaign.
- An *order* is a paid storefront order that is not free. Revenue is the invoice total without tax, in the minor units of `currency`, such as cents. Money is never added across currencies, and `currencies` names every currency with an order in the period.
- `new_customers` counts orders that are the customer's first paid order. `conversion_rate_bp` is orders over visits in basis points, so 123 is 1.23 percent.

```json
{
  "from": "2026-09-01",
  "to": "2026-09-30",
  "zone": "Europe/Berlin",
  "currency": "USD",
  "currencies": ["USD", "EUR"],
  "visits_complete": true,
  "campaigns": [
    {
      "id": 14, "name": "Summer launch", "utm_campaign": "summer-launch", "archived": false,
      "visits": 4, "signups": 3, "orders": 2, "new_customers": 1, "revenue_minor": 3000, "conversion_rate_bp": 5000
    }
  ],
  "unmatched": [
    {
      "campaign": "partner-push",
      "visits": 2, "signups": 1, "orders": 1, "new_customers": 1, "revenue_minor": 3000, "conversion_rate_bp": 5000
    }
  ]
}
```

`unmatched` lists the campaign words of the period that no saved campaign names, the biggest first. It shows traffic you tagged without saving a campaign.

`GET /campaigns/{campaign_id}/results` reads one campaign. It takes `from`, `to` and `currency` too, and `compare` (`previous` by default, or `none`). The answer holds the campaign and `totals`, with the same six figures and `previous`, the figures for the same number of days just before the period (or the whole month before, for a calendar month). `previous` is `null` when `compare` is `none`.

## Result

The campaign appears in the list with its `url`, and its results show the visits and orders that arrived through that link. The change is in the [audit log](/docs/organizations/audit-log/) as `campaign.created`, `campaign.updated` or `campaign.deleted`, with the campaign's name and, for an update, the names of the fields that changed.

## Troubleshooting

`409` with `field` `name`
: Another campaign has this name, in any capitals. Pick another name or change the other campaign.

`409` with no `field`, and the sentence names 500
: The organization already keeps 500 campaigns. Delete or reuse one.

`422` with `field` `coupon_code`
: Your organization has no coupon with that code. A coupon of another organization does not count. Make the coupon first.

`422` with `field` `base_path`
: The page must be a path on your storefront. Write it with a leading `/`, and write a space as `%20`.

A campaign shows no visits
: The campaign word in the visits must equal `utm_campaign`, ignoring capitals. Open the link and check its address. The period must also cover the visits, and sessions older than 180 days are gone.

The `url` has no address in front of it
: We know no address for your storefront, so we give the path and query alone. [Set a custom domain](/docs/organizations/settings/#set-your-custom-domain), or add your storefront's address in front of the link.

A campaign shows orders in the wrong currency
: Orders count only in `currency`. Read the other currency with `currency=EUR`, for example.

## With the API

The staff console's routes under `/api/v1/orgs/{org_slug}/staff/marketing/campaigns` answer the same, for the Billing, Admin and Owner roles, and platform staff have the same routes for any organization in the admin console. All of them need an access token, sent as `Authorization: Bearer $CORITAN_TOKEN`.

| Operation | What it does |
| --- | --- |
| `GET /campaigns` | The campaigns, newest first, with `url` and `archived`. |
| `POST /campaigns` | Save a campaign. |
| `POST /campaigns/preview` | Check a body and build its `url`, saving nothing. |
| `PATCH /campaigns/{campaign_id}` | Change a campaign, or archive it with `archived`. |
| `DELETE /campaigns/{campaign_id}` | Delete a campaign. |
| `GET /campaigns/results` | Every saved campaign's figures, and `unmatched`. |
| `GET /campaigns/{campaign_id}/results` | One campaign's figures beside the period before. |

## Related

- [How customers and orders are attributed](/docs/organizations/attribution/)
- [Read the organization audit log](/docs/organizations/audit-log/)
- [Manage coupons](/docs/organizations/staff-console/billing/#manage-coupons)

## Next steps

- [Read the acquisition report](/docs/organizations/attribution/#read-the-acquisition-report) to see every channel, not only your tagged campaigns.
- [Connect ad accounts and see what ads return](/docs/organizations/ad-spend/) to set each campaign's cost against its orders.
- [Send purchases to your ad platforms](/docs/organizations/conversions/) so the platforms count the sales your links bring.

## API

- `GET /api/v1/orgs/{org_slug}/campaigns`: List the campaign links (https://www.coritan.com/docs/api/reference/organizations/organizations-members/campaigns/#op-get-api-v1-orgs-org-slug-campaigns)
- `POST /api/v1/orgs/{org_slug}/campaigns`: Save a campaign link (https://www.coritan.com/docs/api/reference/organizations/organizations-members/campaigns/#op-post-api-v1-orgs-org-slug-campaigns)
- `POST /api/v1/orgs/{org_slug}/campaigns/preview`: Check a campaign link and build its address without saving it (https://www.coritan.com/docs/api/reference/organizations/organizations-members/campaigns/#op-post-api-v1-orgs-org-slug-campaigns-preview)
- `GET /api/v1/orgs/{org_slug}/campaigns/results`: Read what each campaign brought in (https://www.coritan.com/docs/api/reference/organizations/organizations-members/campaigns/#op-get-api-v1-orgs-org-slug-campaigns-results)
- `GET /api/v1/orgs/{org_slug}/campaigns/{campaign_id}/results`: Read what one campaign brought in (https://www.coritan.com/docs/api/reference/organizations/organizations-members/campaigns/#op-get-api-v1-orgs-org-slug-campaigns-campaign-id-results)
- `PATCH /api/v1/orgs/{org_slug}/campaigns/{campaign_id}`: Change or archive a campaign link (https://www.coritan.com/docs/api/reference/organizations/organizations-members/campaigns/#op-patch-api-v1-orgs-org-slug-campaigns-campaign-id)
- `DELETE /api/v1/orgs/{org_slug}/campaigns/{campaign_id}`: Delete a campaign link (https://www.coritan.com/docs/api/reference/organizations/organizations-members/campaigns/#op-delete-api-v1-orgs-org-slug-campaigns-campaign-id)
- `GET /api/v1/orgs/{org_slug}/staff/marketing/campaigns`: List the campaign links (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-get-api-v1-orgs-org-slug-staff-marketing-campaigns)
- `POST /api/v1/orgs/{org_slug}/staff/marketing/campaigns`: Save a campaign link (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-post-api-v1-orgs-org-slug-staff-marketing-campaigns)
- `POST /api/v1/orgs/{org_slug}/staff/marketing/campaigns/preview`: Check a campaign link and build its address without saving it (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-post-api-v1-orgs-org-slug-staff-marketing-campaigns-preview)
- `GET /api/v1/orgs/{org_slug}/staff/marketing/campaigns/results`: Read what each campaign brought in (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-get-api-v1-orgs-org-slug-staff-marketing-campaigns-results)
- `GET /api/v1/orgs/{org_slug}/staff/marketing/campaigns/{campaign_id}/results`: Read what one campaign brought in (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-get-api-v1-orgs-org-slug-staff-marketing-campaigns-campaign-id-results)
- `PATCH /api/v1/orgs/{org_slug}/staff/marketing/campaigns/{campaign_id}`: Change or archive a campaign link (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-patch-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 (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-delete-api-v1-orgs-org-slug-staff-marketing-campaigns-campaign-id)
