Ads
This page is the API reference for developers reading or writing ad accounts, audience syncs, conversions, ad spend or feed references.
The Ads app connects a shop to Meta, Google Ads and LinkedIn without being an ad manager. Ads are still created on the platforms. The app holds one ad account per platform account, each backed by an Integration Studio connection. It uploads audiences of the audiences app as SHA-256 hashes of member e-mail addresses, by default only for members with a confirmed consent. It records orders from an ad click as conversions and sends them only when the visitor allowed the consent purpose of the rule. It keeps the daily spend per ad campaign, and the return on ad spend is computed when it is read. The app never calls a platform itself: the connector workflows do that.
Objects
| Object | What it holds |
|---|---|
| Ad account | One account on a platform (meta, google_ads or linkedin): external_account_id, name, connection_ref, currency, and status (connected, error or disconnected). The totals of spend, clicks and conversions are kept on it (see the figures below). |
| Audience sync | One audience of the audiences app uploaded to one ad account: audience_id, subscription_type, active, and the counts of members, matched, uploaded and skipped members. |
| Ad campaign link | The mapping of one campaign on an ad platform (external_campaign_id) to one campaign of the campaigns app (campaign_id). |
| Conversion rule | Which event (order.placed, subscription.confirmed or form.submitted) is sent to which ad account, with which value source and consent purpose. |
| Conversion | One order and one rule: the click ID, the value, the state (ready, sent, skipped_no_consent, failed) and the attempts. |
| Ad spend | The spend of one ad campaign on one day in one account: spend_date, spend (the amount), currency, impressions, clicks and booking_state. |
| Feed reference | Which product feed of a market is handed to which destination (google_merchant_center, meta_catalog, linkedin or other). The feed itself is built elsewhere on the platform; this app only names it by feed_id or feed_url. |
Base path: https://api.revenexx.com/v1/ads.
Scopes
A key calls these routes with the capability key of each route. The App's manifest names one per operation, in the form ads.<group>.<operation>, and the API keys page explains that a key holds capability keys or a namespace such as ads.*. The scope names are listed in the tables below. A call that needs a scope the key does not hold is refused with 403.
Ad accounts
| Operation | Scope | What it does |
|---|---|---|
GET /v1/ads/accounts | ads.accounts.list | Lists the ad accounts. The trash is left out unless trashed asks for it. |
POST /v1/ads/accounts | ads.accounts.create | Adds an ad account. |
GET /v1/ads/accounts/{id} | ads.accounts.get | Reads one ad account. |
PUT /v1/ads/accounts/{id} | ads.accounts.update | Renames an ad account or changes its connection. |
DELETE /v1/ads/accounts/{id} | ads.accounts.delete | Moves an ad account into the trash. A second DELETE on the same account deletes it for good. |
POST /v1/ads/accounts/{id}/restore | ads.accounts.restore | Takes an ad account back out of the trash. |
POST /v1/ads/accounts/trash/empty | ads.accounts.empty-trash | Deletes every ad account in the trash for good. |
POST /v1/ads/accounts/trash/purge | ads.accounts.purge | Deletes the ad accounts that have been in the trash longer than the tenant's trash retention. It is the nightly purge, callable by hand. |
POST /v1/ads/accounts/{id}/status | ads.accounts.status | Reports the state of an ad account's connection. A connector workflow calls it when a login expires or works again. |
GET /v1/ads/accountsfilters:platform,status(for examplestatus=connected),currency,name,connection_ref,external_account_id,clicks_total,conversions_total,trashed(exclude,onlyorinclude), and further plain columns.limittakes 1 to 200 (default 50), withoffsetandorder.POST /v1/ads/accountsbody: requiredplatform,external_account_idandname. Optional:connection_refandcurrency(three capital letters, defaultEUR). The app setsstatusitself:connectedwhen aconnection_refis given, otherwisedisconnected.PUT /v1/ads/accounts/{id}body: the same fields, all optional. The platform and the account ID cannot change:400withaccount_fixed.- Figures on an ad account: read-only. The app recounts them; a value sent for them on a create or update is dropped.
spend_total(number): all spend imported for the account, in its currency (spend rows in another currency are left out). Recounted by the hourly run and after each spend import.clicks_total(integer): all clicks the spend imports reported. Recounted withspend_total.conversions_total(integer): conversions recorded with consent; skipped ones are not counted. Recounted withspend_total.conversion_value_total(number): the value of those conversions in the account's currency. Recounted withspend_total.roas(number or null):conversion_value_totaldivided byspend_total, two decimals; null while nothing was spent.figures_at(date-time or null): when the totals were last recounted.
POST /v1/ads/accounts/{id}/statusbody: requiredstatus(connected,errorordisconnected). Optionalerror.- Trash: an account moves into the trash only when no spend was ever imported for it and it never recorded a conversion. Its audience syncs, conversion rules and campaign links stay as they are. While the account is in the trash, its conversion rules convert nothing and its spend is not booked into a campaign budget.
- Emptying the trash:
POST /v1/ads/accounts/trash/emptyandPOST /v1/ads/accounts/trash/purgeare refused for a storefront contact with403purge_not_allowed; only the back office may run them. - Trash and sync fields: the trash fields
deleted_at,deleted_by,deleted_by_id,trash_batch,purge_atandpurge_blocked_reasonare on the ad account only; campaign links have no trash. Ad accounts and campaign links both carry the sync fieldsexternal_refs,source_dataandsource_synced_at, writable on create and update (an account's own platform ID staysexternal_account_id). Shared rules: Trash; Sync fields. - Errors:
400for an unknown platform, a missingexternal_account_idorname, a bad currency, or an unknown status.404when no account has the ID.409withaccount_existswhen the same platform account is added again, orin_trashwhen it is in the trash (restore it).409withtrash_blocked(detailsnames the blockerhas_history) when the account cannot go into the trash.409within_trashwhen an account in the trash is changed.409withnot_in_trashon a restore of an account that is not in the trash.
Audience syncs
| Operation | Scope | What it does |
|---|---|---|
GET /v1/ads/audience-syncs | ads.syncs.list | Lists the audience syncs. |
POST /v1/ads/audience-syncs | ads.syncs.create | Starts syncing an audience to an ad account. |
GET /v1/ads/audience-syncs/{id} | ads.syncs.get | Reads one audience sync. |
PUT /v1/ads/audience-syncs/{id} | ads.syncs.update | Switches a sync on or off, or changes its subscription type. |
DELETE /v1/ads/audience-syncs/{id} | ads.syncs.delete | Stops syncing the audience. |
POST /v1/ads/audience-syncs/{id}/request | ads.syncs.request | Requests an upload of the audience now. It announces audience_sync.requested for the platform's workflow. |
GET /v1/ads/audience-syncs/{id}/members | ads.syncs.members | Reads one page of the hashed members for the upload. |
POST /v1/ads/audience-syncs/{id}/result | ads.syncs.result | Reports what an upload achieved: the matched, uploaded and skipped counts, and any error. |
GET /v1/ads/audience-syncsfilters:ad_account_id,audience_id,audience_code,subscription_type,state,active,staleand the count columns.limittakes 1 to 200 (default 50).POST /v1/ads/audience-syncsbody: requiredad_account_id,audience_idandsubscription_type. Optional:active(default true),audience_codeandexternal_audience_id.- Members: each member's e-mail address is trimmed, lower-cased and hashed with SHA-256. Only members with a confirmed subscription of the sync's type are served. By default only members with consent are served, not members under the existing-customer exemption (the tenant setting
audience_sync_consent_only, default on). Members without it are counted as skipped and never served.limittakes 1 to 500 (default and maximum 500). - Errors:
400withsubscription_type_requiredwhen no subscription type is sent. A plain400, with no code, whenad_account_idnames no ad account, and whenaudience_idis not a UUID.404when there is no such sync.409withsync_existswhen the audience is already synced to that account.409withaccount_not_connectedwhen an upload is requested for an account whosestatusis notconnected(errorordisconnected).409within_trashwhen a sync is created or updated for an account in the trash (restore it first).409withaccount_in_trashon the request and members routes when the account is in the trash.409withsync_inactiveon the request and members routes when the sync is switched off.502on the members route withconsents_unavailableoraudiences_unavailablewhen the members cannot be read: nothing is served without a consent answer.
Campaign links
| Operation | Scope | What it does |
|---|---|---|
GET /v1/ads/campaign-links | ads.links.list | Lists the links between ad campaigns and campaigns. |
POST /v1/ads/campaign-links | ads.links.create | Creates a link for one ad campaign of an ad account. |
GET /v1/ads/campaign-links/{id} | ads.links.get | Reads one link. |
PUT /v1/ads/campaign-links/{id} | ads.links.update | Maps the ad campaign to a campaign. |
DELETE /v1/ads/campaign-links/{id} | ads.links.delete | Removes a link. Refused while the link is in use. |
GET /v1/ads/campaign-linksfilters:ad_account_id,external_campaign_id,external_name,campaign_id. Filtering oncampaign_idreturns one campaign's links. An ad campaign first seen in a spend import gets a link without acampaign_id;GET /v1/ads/spend?booking_state=unlinkedlists the spend waiting for a mapping.POST /v1/ads/campaign-linksbody: requiredad_account_idandexternal_campaign_id. Optional:campaign_idandexternal_name.- Mapping: mapping an ad campaign to a campaign books the spend imported for it into that campaign's budget, including spend imported before the mapping.
- Errors:
400for a bad campaign ID, or a missing account or ad campaign ID.404when there is no such link.409withlink_existsonPOSTwhen the ad campaign already has a link.409withlink_in_useonDELETEwhen spend was imported for the ad campaign, even while it is still unmapped: a link that ever received spend cannot be deleted.400withad_account_idnaming no ad account.
Ad spend and figures
| Operation | Scope | What it does |
|---|---|---|
GET /v1/ads/spend | ads.spend.list | Lists the imported daily spend. |
POST /v1/ads/spend | ads.spend.import | Imports the daily spend per ad campaign. It is called by the daily spend workflow. |
GET /v1/ads/spend/{id} | ads.spend.get | Reads one day of spend. |
GET /v1/ads/accounts/{id}/figures | ads.spend.figures | Reads the spend, clicks and consented conversions of an ad account per day, and the spend per ad campaign, largest first. It reads only. |
GET /v1/ads/campaigns/{campaign_id}/roas | ads.spend.roas | Reads the return on ad spend of a campaign, computed on each read: the revenue the campaigns app attributes to it, divided by the ad spend imported for the campaign (any booking state), in the campaign's currency, with the spend per platform. |
GET /v1/ads/spendfilters:ad_account_id,external_campaign_id,campaign_id,platform,spend_date,booking_state(for examplebooking_state=unlinked) and further plain columns.limittakes 1 to 200 (default 50).POST /v1/ads/spendbody: requiredrows, an array of 1 to 500 daily rows. Each row needsad_account_id(the ID of an ad account),external_campaign_id,date(YYYY-MM-DD) andspend(the amount, zero or more). Optional:currency(three capital letters, default the account's currency),impressions,clicksandexternal_name. Each row is kept per account, ad campaign and day, and a second import of the same day replaces the earlier row.400withrows_requiredortoo_many_rowswhen the batch is empty or has more than 500 rows.- Booking: spend of a mapped ad campaign is booked into the campaign's budget, summed per campaign, platform and day. Spend of an unmapped ad campaign waits with
booking_state=unlinkeduntil it is mapped; the hourly job does not book it. - Figures: the window runs from the first active day to the last, at most the latest 60 days. Spend is in the account's currency only.
- ROAS: attributed revenue divided by the ad spend imported for the campaign (any booking state), in the campaign's currency.
modelpicks the attribution model,first_touchorlast_touch(the default). An unknown model is refused with400. - Errors: a row that cannot be kept is named in
rejectedwith its index and a reason (invalid_date,invalid_spend,invalid_currency,unknown_account,external_campaign_required,not_an_objectoraccount_in_trash); the rest of the batch is kept.404when there is no such row.400on the ROAS route for a campaign ID that is not valid.
Conversion rules
| Operation | Scope | What it does |
|---|---|---|
GET /v1/ads/conversion-rules | ads.rules.list | Lists the conversion rules. |
POST /v1/ads/conversion-rules | ads.rules.create | Adds a conversion rule. |
GET /v1/ads/conversion-rules/{id} | ads.rules.get | Reads one rule. |
PUT /v1/ads/conversion-rules/{id} | ads.rules.update | Corrects a rule. |
DELETE /v1/ads/conversion-rules/{id} | ads.rules.delete | Deletes a rule that recorded no conversion. |
GET /v1/ads/conversion-rulesfilters:ad_account_id,event,active=true,value_source,consent_purpose,external_conversion_refandname.POST /v1/ads/conversion-rulesbody: requiredad_account_id,nameandevent(order.placed,subscription.confirmedorform.submitted). Optional:active(default true),value_source(order_net,order_gross,fixedornone; defaultorder_net, the grand total less tax and shipping),fixed_value,consent_purpose(default: the tenant settingconversion_consent_purpose, which ismarketing),external_conversion_refandmetadata.- Consent: a conversion is sent only when the visitor allowed the consent purpose that the rule names. Without consent the conversion is kept as
skipped_no_consent, without the click ID and the hashed e-mail, and is never sent. - What is recorded: only
order.placedrecords conversions today. Rules forsubscription.confirmedandform.submittedare accepted but record nothing yet. An order is recorded only when its attribution carries aclick_idwithclick_id_kindgclid,fbclidorli_fat_idthat matches the rule's ad account platform. An order with any other kind, such asmsclkid, is skipped. - Errors:
400withad_account_idornamemissing, an unknown event or value source, or a fixed value without an amount, and whenad_account_idnames no ad account.404when there is no such rule.409withrule_in_usewhen a rule with recorded conversions is deleted.
Conversions
| Operation | Scope | What it does |
|---|---|---|
GET /v1/ads/conversions | ads.conversions.list | Lists the recorded conversions. The workflow reads state=ready. |
GET /v1/ads/conversions/{id} | ads.conversions.get | Reads one conversion. |
POST /v1/ads/conversions/{id}/result | ads.conversions.result | Reports whether the conversion was sent, and the platform's reference or the error. |
POST /v1/ads/conversions/{id}/retry | ads.conversions.retry | Puts a failed conversion back in line for sending. |
GET /v1/ads/conversionsfilters:state,rule_id,ad_account_id,order_id,platform,event,click_id_kindand further plain columns.limittakes 1 to 200 (default 50).POST .../resultbody: optionalok(boolean),external_refanderror.POST .../retrybody: no fields.- Errors:
409withnot_sendablewhen a result is reported for a conversion skipped for lack of consent.409withnot_failedwhen a retry is asked for a conversion that did not fail.404when there is no such conversion.
Feed references
| Operation | Scope | What it does |
|---|---|---|
GET /v1/ads/feed-refs | ads.feeds.list | Lists the feed references. |
POST /v1/ads/feed-refs | ads.feeds.create | Names the product feed of a market for a destination. |
GET /v1/ads/feed-refs/{id} | ads.feeds.get | Reads one feed reference. |
PUT /v1/ads/feed-refs/{id} | ads.feeds.update | Changes a feed reference. |
DELETE /v1/ads/feed-refs/{id} | ads.feeds.delete | Removes a feed reference. |
GET /v1/ads/feed-refsfilters:market,destination,feed_id,feed_url,label,activeandid. There is one reference per market and destination.POST /v1/ads/feed-refsbody: requiredmarketanddestination. Alsofeed_idorfeed_url; the request is refused without one. Optional:active,labelandmetadata.PUT /v1/ads/feed-refs/{id}: the market and the destination cannot change.- Errors:
400withfeed_source_required(detailsnamesfeed_id) when neither a feed ID nor a feed address is sent.400withfeed_url_invalid,destination_invalid,market_requiredordestination_required, withdetails.fieldnaming the field.400withfeed_fixedwhen the market or the destination is changed (details.fieldismarket).409withfeed_existswhen a second reference is added for the same market and destination.404when there is no such reference.
Jobs
| Operation | Scope | What it does |
|---|---|---|
POST /v1/ads/run | ads.jobs.run | Runs the hourly ads job by hand. It records conversions of orders placed in the last three hours, requests the audience uploads that are due, books again the spend the campaigns app did not take on the first try, and recounts the totals of each account. |
- Body: no fields.
- Answer:
accounts_recounted(integer) is the number of accounts whose figures the run recounted (accounts in the trash are not recounted). - Schedule: the same job runs every hour, at minute 17.
Vocabularies
| Operation | Scope | What it does |
|---|---|---|
GET /v1/ads/vocabularies | ads.vocabularies.list | Lists the closed vocabularies this app enforces. |
GET /v1/ads/vocabularies/{name} | ads.vocabularies.get | Reads one vocabulary with its values and labels. 404 when there is no such vocabulary. |