Skip to content
Coritan Docs

Connect ad accounts and see what ads return

Read your ad spend from Google, Meta, TikTok, Microsoft and Reddit, add spend by hand, and see new customers, revenue, CAC and ROAS per platform.

View as Markdown

Ad spend shows what you pay each ad platform and what that spending brings back. Coritan reads the cost, clicks and impressions of each campaign, day by day, from your Google Ads, Meta Ads, TikTok Ads, Microsoft Ads and Reddit Ads accounts. It then matches your orders to the platform their last visit came from, and works out how many new customers each platform won, what they paid, what each customer cost (CAC) and what each unit of spend returned (ROAS).

Money is never added across currencies. An ad account that bills in dollars and one that bills in euros give separate rows and separate totals.

  • You need the Owner or Admin role in the organization. No other role can read or change anything here.
  • The platform must be available to connect. Coritan sets up the sign-in app for each platform. A platform that is not set up shows the status not_configured and refuses to connect with a 409 answer.
  • You need an account on the platform with access to the ad account you want to read. For Google Ads and Microsoft Ads that can be a manager account that holds your ad account.
  • Read access is all Coritan asks for. It cannot change a campaign, a budget or an ad.
  • Orders get matched to a platform through the tracker that runs on your storefront. Your ads need utm_source, utm_medium and utm_campaign in their links, or the platform's click ID, so the tracker can name the platform. See How customers and orders are attributed.

Owners and admins connect ad accounts in the dashboard. Your team can do the same in the staff console, as Team and settings describes. A member with another role does not see the page, and its address opens Overview.

  1. In the dashboard, open the organization, then Settings, then Ad spend.
  2. Find the platform's card, "Google Ads", "Meta Ads", "TikTok Ads", "Microsoft Ads" or "Reddit Ads", and select Connect. A card with the badge Not set up says Not set up on this platform yet. and its Connect button stays off. Nothing on your side fixes that: ask support.
  3. Sign in on the platform's own page with the account that can open your ad accounts, and allow read access. The link works for 15 minutes. The platform then sends you back to Ad spend, which says the platform is connected and how many ad accounts it found. When the platform refused, the page says it was not connected and shows the platform's reason. Select Connect again to start over.
  4. Choose the accounts to read. A sign-in that finds one account, on a platform where you have chosen none yet, chooses it for you. Otherwise the card's badge reads Choose accounts. Select Choose accounts…, tick the accounts that belong to your organization and select Save accounts. A sign-in to a manager account can list accounts that are not yours, so tick only your own. You can choose up to 50 accounts for one platform.
  5. Read the card. Accounts read says how many of the accounts found we read, Last read says when, and Spend, 30 days adds up each currency's spend. Each account has a row with its Status, Last read, History, which says how far back we have read, and Spend, 30 days.

When it works, the badge reads Connected, an account's Last read shows a time within the last 15 minutes and Last 30 days at the top of the page shows spend in each currency. A new account's history fills in over several passes: History reads "Reading it now" until the first one lands.

What a card's badge means:

Connected
We read the chosen accounts every 15 minutes.
Choose accounts
The sign-in found accounts and none is chosen, so nothing is read.
Failing
An account has had errors and no successful read for an hour. Its row shows the platform's reason, such as the signed-in person losing access to the ad account. Fix it in the platform. We try again every 15 minutes, and Sync now tries at once.
Sign in again
The platform no longer accepts the saved sign-in. Select Connect again and sign in with the person who owns the ad accounts. Your history and the spend you entered stay.
Not connected
Nothing is signed in yet.
Not set up
Coritan has not added the sign-in app for this platform yet.

A Meta sign-in ends after about 60 days. When it has 14 days or fewer left, the card says The sign-in is about to end. Select Connect again before it does.

To read an account now, open the actions menu of its row and select Sync now. The page says it is reading the account, and the new figures show in a few minutes. You can do this 4 times an hour for one account, and the next try says how long to wait. To stop reading an account, open the same menu and select Disconnect…. The question names the account. Confirm with Disconnect account: we delete its saved sign-in, and the spend we already read stays in your reports.

Spend you enter by hand holds spend for a channel with no connection, such as X, LinkedIn or a print ad, so the return report counts it.

  1. Select Add spend….
  2. Choose the Channel and name the spend in What it was for. Choose the Currency and type the Amount: the whole amount for the days, in whole units and cents, such as 300.00. Enter the First day and the Last day. One entry covers at most 400 days.
  3. Select Add spend. We spread the amount evenly over the days.

A channel that is connected takes no typed spend, because its own numbers cover the days: the form says so under Channel. An entry whose channel was connected later shows Not used under Used in reports, and the reports leave it out. To remove an entry, open the actions menu of its row, select Delete… and confirm with Delete entry.

Last 30 days adds up the spend of every platform and every entry, one row for each currency, and See what it returned opens the Acquisition page, where Return on ad spend works out CAC and ROAS.

Each platform has its own sign-in page. The steps are the same for all five, and the examples below use Google Ads (google). The platform words are google, meta, tiktok, microsoft and reddit.

  1. Ask for the sign-in address with POST /api/v1/orgs/{org_slug}/ads/google/connect. A console sends return_path, the page to come back to, from a page on one of your storefront's hosts. A script sends nothing. The answer holds authorization_url.
  2. Send the person who owns the ad account to authorization_url. They sign in to the platform and allow read access. The link works for 15 minutes.
  3. The platform sends them back to Coritan, which finds every ad account the sign-in can read and returns them to your return_path. The address ends with ads=connected, or with ads=error and a message that says what went wrong. With no return_path, the browser stops on a page that says whether the sign-in finished.
  4. Read GET /api/v1/orgs/{org_slug}/ads. Each account the sign-in found is in connections with the status pending. A sign-in that finds exactly one account, on a platform where you have picked none yet, picks it for you.
  5. Pick the accounts to read with POST /api/v1/orgs/{org_slug}/ads/google/accounts. Send account_ids, the whole list. Accounts you leave out stop being read.

Pick only accounts that belong to your organization. A sign-in to a manager account can list accounts of other advertisers, and Coritan cannot tell which are yours.

Coritan reads each picked account's last 13 months first, then keeps it up to date. Check backfilled_through on the account. It holds a date once the history has been read.

Platform Sign in with What Coritan counts as a conversion Days follow
Google Ads A Google account that can open the ad account or its manager account Conversions and conversion value that Google reports Each ad account's own time zone
Meta Ads Facebook, with the ads_read permission Purchases, taken from the first purchase action Meta lists Each ad account's own time zone
TikTok Ads An advertiser who approves the app Completed payments. The value is the spend times TikTok's payment ROAS Each advertiser's own time zone
Microsoft Ads A Microsoft account that can open the ad account Conversions and revenue that Microsoft reports Your organization's time zone
Reddit Ads A Reddit account that is a member of the business Purchase conversions and their value Your organization's time zone

The numbers the platforms call conversions are shown beside yours as platform_conversions and platform_value_minor. They are the platform's own count, which can include conversions from visits that never reached your orders.

A Meta sign-in lasts about 60 days. Coritan renews it each day in its last 20 days. If the renewal fails and the sign-in has 14 days or fewer left, the account carries a warning with sign_in_ends_soon and the number of days left. The other platforms' sign-ins have no end date.

Coritan reads every picked account every 15 minutes.

  • Each read covers today and the two days before, because platforms revise recent days.
  • Once each night, after 05:00 in your organization's time zone, it reads the last 35 days.
  • A new account reads its whole 13 month history first, in steps. If the read stops, it carries on from where it stopped.

To read an account at once, use POST /api/v1/orgs/{org_slug}/ads/connections/{connection_id}/sync. With no body it reads the recent days. With from and to it reads those days again, up to 400 of them and none before the 13 month history. You can ask four times an hour for one account. The read runs in the background, so check last_sync_at afterwards.

Reading a day again replaces what was saved for it. A day the platform stops listing is removed from that account. An answer with no days at all changes nothing, so a platform that returns nothing never wipes your history.

Some channels have no connection, such as X, LinkedIn, Snapchat, Pinterest or a sponsorship. Add their spend with POST /api/v1/orgs/{org_slug}/ads/spend/manual:

  • channel is one of google, meta, tiktok, microsoft, reddit, x, linkedin, snapchat, pinterest or other.
  • label says what the spend was for.
  • currency is the three letter code and amount_minor is the whole amount in the currency's smallest unit (cents for dollars).
  • starts_on and ends_on are the first and last day it covers, at most 400 days apart. The last day can be up to 31 days after today.

Reports spread the amount evenly over the days. The shares always add up to the whole amount, and any leftover unit goes to the first days. An amount of 1000 cents over three days reads as 334, 333 and 333.

A platform with a picked account, or one that was already read for some of the days you name, refuses an entry with 409. Its own numbers cover those days. If you pick an account of a platform after you added spend for it, the entry stays in the list marked ignored: true and reports leave it out. A day an account read and then disconnected still covers, so an old entry never counts it a second time.

Remove an entry with DELETE /api/v1/orgs/{org_slug}/ads/spend/manual/{entry_id}. Each organization can keep 1000 entries.

GET /api/v1/orgs/{org_slug}/ads/spend answers what was spent. It takes:

  • from and to, both included, as YYYY-MM-DD. The default is the last 30 days. A range is at most 400 days.
  • group: platform (the default), campaign or day.
  • currency: only this currency.
  • limit: for campaign, the rows to return for each currency, up to 500.

Every row names its currency. A platform that spent in two currencies has two rows. totals holds the whole range for each currency, even when limit cuts the rows.

The same call exists for staff under /api/v1/orgs/{org_slug}/staff/marketing/ads/spend, and it gives the same answer.

How CAC and ROAS are worked out

Section titled How CAC and ROAS are worked out

GET /api/v1/orgs/{org_slug}/ads/return answers what the spend brought back. It takes from, to and currency like the spend call, and group as platform (the default) or campaign.

An order counts for a platform when all of these hold:

  1. Its invoice is paid, the order is not free, and the order started in the range. Days are your organization's days.
  2. Its last touch was paid: the visit that came last before the order, not the first one the customer ever made.
  3. The touch names the platform. Its source and medium come first (utm_source=facebook with a paid medium is Meta Ads). When they do not name one, the click IDs on the order do: gclid is Google Ads, msclkid is Microsoft Ads, fbclid is Meta Ads, ttclid is TikTok Ads and rdt_cid is Reddit Ads. An order that is paid media but names no platform counts under "Other paid traffic".

Inside a platform, an order counts for a campaign when its utm_campaign equals the campaign's name (ignoring capitals and spaces around it) or its ID. Campaigns that share a name inside one platform count as one campaign. An order that matches no campaign counts for the platform only, in the row with no campaign name.

Each row has these figures, all in one currency:

Field Meaning
spend_minor What the platform's accounts (and your entered spend) cost.
orders Orders matched to the row.
new_customers Customers whose first paid order is one of those orders.
revenue_minor The orders' invoice totals without tax.
cac_minor spend_minor divided by new_customers. Empty when there are no new customers or no spend.
roas revenue_minor divided by spend_minor. A ROAS of 2.5 means 2.50 back for each 1.00 spent. Empty when there is no spend.
platform_roas The conversion value the platform reports, divided by the spend.
cost_per_click_minor Spend divided by clicks.

For example, if Google Ads cost 150.00 USD in a range and won two new customers whose orders came to 55.00 USD without tax, CAC is 75.00 USD and ROAS is 0.37.

Spend and revenue meet only in the same currency. If your Meta account bills in dollars and your customers pay in euros, the dollar row has spend and no orders, the euro row has orders and no spend, and neither shows CAC or ROAS. To see CAC and ROAS for them, run the ad account in the currency your customers pay in.

Payback is not worked out. It needs the gross margin of a customer's first month, which Coritan does not hold.

One call reads at most 50,000 orders. truncated is true when there were more.

DELETE /api/v1/orgs/{org_slug}/ads/connections/{connection_id} ends one account's connection. Its saved sign-in is deleted and Coritan reads nothing more from it. The spend Coritan already read stays in your reports. Other accounts that came from the same sign-in keep working. To read the account again, connect the platform again.

You can also remove Coritan's access in the platform's own settings. The next read then fails and the account shows reauth.

Privacy and what Coritan keeps

Section titled Privacy and what Coritan keeps
  • The sign-in's tokens are sealed when they are saved, and no call, log or screen shows them again.
  • Coritan asks each platform for read access to ad accounts only.
  • Coritan stores one row for each account, campaign and day: the cost, clicks, impressions, conversions and conversion value. It stores no information about the people who saw or clicked an ad.
  • Only the Owner and Admin roles of your organization, and Coritan staff who manage organizations, can read or change this.
  • Every change is in the audit log: connecting, picking accounts, reading an account on request, disconnecting, and adding or removing spend.
  • Deleting the organization deletes the connections and every spend row with it.
  • 5 platforms, up to 50 accounts for each.
  • 13 months of history for a new account.
  • A read of days, or a spend or return range, covers at most 400 days.
  • 4 requests an hour to read one account now.
  • 1000 spend entries, each over at most 400 days.
not_configured
Coritan has not set up the sign-in app for that platform. Connecting answers 409 with this word. Nothing on your side fixes it.
reauth
The platform no longer accepts the saved sign-in. The person who owns the ad account must connect the platform again. Their accounts, history and entered spend are kept.
failing
The account has had an error and no successful read for an hour. The account's last_error says why, for example that the signed-in person lost access to the ad account. Fix it at the platform. The next 15 minute read tries again.
pending
The sign-in found the account and nobody picked it. Pick it with POST .../accounts.
The spend is lower or higher than the platform's own report
Days follow each ad account's time zone (Microsoft Ads and Reddit Ads follow yours), so a report in another zone cuts the day somewhere else. Recent days also change for a few days while the platform settles them. A read of the last 35 days each night corrects them.
The first read has no days yet
Reading 13 months takes several 15 minute passes when an account has many campaigns. Check backfilled_through.
409 channel_connected or 409 already_imported when adding spend
The platform is connected or its spend was already read for those days. Remove the connection first, or choose days it did not cover.
429 when reading an account
You asked more than four times in an hour for that account. Wait an hour.
A sign-in fails with a message on the way back
The message comes from the platform, such as "did not allow access". Start again with POST .../connect, and allow every permission the platform asks for.
Orders show under "Other paid traffic" or under no campaign
The links of those ads carry no utm_source that names the platform, or a utm_campaign that differs from the campaign's name. Add utm_campaign={campaign name} to the ad's link, or the platform's own {campaignid} value.

Send an access token as Authorization: Bearer $CORITAN_TOKEN. The owner calls are under /api/v1/orgs/{org_slug}/ads. Staff members with the Admin role use the same calls under /api/v1/orgs/{org_slug}/staff/marketing/ads.

Start a Google Ads sign-in
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/ads/google/connect" \
  -H "Authorization: Bearer $CORITAN_TOKEN"

Open authorization_url in a browser. A console that wants the browser back on one of its own pages sends {"return_path": "/marketing/ads"} from a page on one of your storefront's hosts. A request with a return_path from any other host is refused with 422. The answer:

JSON
{
  "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=...",
  "redirect_uri": "https://api.coritan.com/api/v1/ads/google/callback",
  "expires_in": 900
}

After the sign-in, pick the accounts to read:

Read two accounts
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/ads/google/accounts" \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"account_ids": ["7778889999", "7778880000"]}'

Read the status of every platform and account:

Shell
curl "https://api.coritan.com/api/v1/orgs/acme/ads" -H "Authorization: Bearer $CORITAN_TOKEN"

platforms[].status is one of not_configured, not_connected, pending, connected, failing or reauth. Each entry of connections has the same words for an account, plus account_name, currency, last_sync_at, last_error, backfilled_through, warning and spend_30d, the last 30 days for each currency. summary is the last 30 days for the whole organization, for each currency.

Read what the spend returned, for a month, by campaign:

Shell
curl "https://api.coritan.com/api/v1/orgs/acme/ads/return?from=2026-09-01&to=2026-09-30&group=campaign" \
  -H "Authorization: Bearer $CORITAN_TOKEN"
JSON
{
  "from": "2026-09-01",
  "to": "2026-09-30",
  "group": "campaign",
  "currencies": ["USD"],
  "rows": [
    {
      "platform": "google", "label": "Google Ads", "channel": "paid_search", "campaign": "Brand", "currency": "USD",
      "spend_minor": 10000, "clicks": 100, "impressions": 4200, "cost_per_click_minor": 100,
      "orders": 1, "new_customers": 1, "revenue_minor": 2500, "cac_minor": 10000, "roas": 0.25,
      "platform_conversions": 3.5, "platform_value_minor": 21026, "platform_roas": 2.1
    }
  ],
  "totals": [{ "currency": "USD", "spend_minor": 10000, "orders": 1, "new_customers": 1, "revenue_minor": 2500, "cac_minor": 10000, "roas": 0.25 }],
  "truncated": false,
  "basis": { "touch": "last", "revenue": "excluding_tax", "new_customers": "first_paid_order" }
}

Add spend by hand:

Add 30.00 USD of X spend over a week
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/ads/spend/manual" \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"channel": "x", "label": "Launch week", "currency": "USD", "amount_minor": 3000, "starts_on": "2026-10-01", "ends_on": "2026-10-07"}'

A request that breaks a rule answers 409 or 422 with detail.error (a word such as channel_connected) and detail.message (the sentence to show).

API operations on this page

MethodPathWhat it does
GET/api/v1/orgs/{org_slug}/adsGet ads
POST/api/v1/orgs/{org_slug}/ads/{platform}/connectStart signing in to an ad platform
POST/api/v1/orgs/{org_slug}/ads/{platform}/accountsPick which of the platform's accounts to read
POST/api/v1/orgs/{org_slug}/ads/connections/{connection_id}/syncRead one ad account now
DELETE/api/v1/orgs/{org_slug}/ads/connections/{connection_id}Disconnect one ad account
GET/api/v1/orgs/{org_slug}/ads/spend/manualSpend entered by hand, newest first
POST/api/v1/orgs/{org_slug}/ads/spend/manualEnter spend for a channel that has no connection
DELETE/api/v1/orgs/{org_slug}/ads/spend/manual/{entry_id}Remove an entry of spend entered by hand
GET/api/v1/orgs/{org_slug}/ads/spendWhat was spent, grouped by platform, campaign or day
GET/api/v1/orgs/{org_slug}/ads/returnGet ad return
GET/api/v1/orgs/{org_slug}/staff/marketing/adsGet ads
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}/syncRead one ad account now
GET/api/v1/orgs/{org_slug}/staff/marketing/ads/returnGet ad return
GET/api/v1/orgs/{org_slug}/staff/marketing/ads/spendWhat was spent, grouped by platform, campaign or day
GET/api/v1/orgs/{org_slug}/staff/marketing/ads/spend/manualSpend entered by hand, newest first
POST/api/v1/orgs/{org_slug}/staff/marketing/ads/spend/manualEnter spend for a channel that has no connection
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}/accountsPick which of the platform's accounts to read
POST/api/v1/orgs/{org_slug}/staff/marketing/ads/{platform}/connectStart signing in to an ad platform