Market Radar API

Endpoints

Every path, and the rules for creating and editing radars, companies, custom sources, groups and newsletters.

Paths are under https://app.trymarketradar.com/api/v1. This page gives the rules; each endpoint's parameters, bodies and responses, field by field, are in the API reference, generated from the OpenAPI document.

Method and pathWhat it doesAccess
GET /radarsThe radarsRead
GET /radars/{id}One radarRead
POST /radarsCreate a radar and start its first sweepRead and write
PATCH /radars/{id}Rename it, move it to another folder, or change its query, sources or ratesRead and write
DELETE /radars/{id}Delete it and its signalsRead and write
GET /radars/{id}/signalsIts signalsRead
GET /radars/{id}/tendersA tender radar's calls for tenderRead
GET /radars/{id}/sweepsIts sweepsRead
GET /radars/{id}/ratesAn exchange rate radar's rates and interest ratesRead
GET /radars/{id}/pricesA price radar's pricesRead
GET /foldersThe foldersRead
POST /foldersCreate a folderRead and write
PATCH /folders/{id}Rename a folderRead and write
DELETE /folders/{id}Delete a folder and its radarsRead and write
GET /groupsThe groups of companies and of custom sourcesRead
POST /groupsCreate a groupRead and write
GET /groups/{id}One groupRead
PATCH /groups/{id}Rename a group or change its membersRead and write
DELETE /groups/{id}Delete a group and its membersRead and write
GET /signalsEvery radar's signalsRead
GET /signals/{id}One signal with its sourcesRead
GET /companiesThe companiesRead
POST /companiesAdd a companyRead and write
PATCH /companies/{id}Edit a companyRead and write
DELETE /companies/{id}Delete a companyRead and write
GET /custom-sourcesThe custom sourcesRead
GET /custom-sources/{id}One custom sourceRead
POST /custom-sourcesAdd a custom sourceRead and write
PATCH /custom-sources/{id}Edit a custom sourceRead and write
DELETE /custom-sources/{id}Delete a custom sourceRead and write
GET /filtersThe catalogue and the account's own filtersRead
GET /newslettersThe newslettersRead
GET /newsletters/{id}One newsletterRead
POST /newslettersCreate a newsletter, not liveRead and write
PATCH /newsletters/{id}Change its settings (Live aside)Read and write
DELETE /newsletters/{id}Delete it and its issuesRead and write
GET /newsletters/{id}/issuesIts issuesRead
GET /newsletters/{id}/issues/{issue_id}One issue, with what each block carriesRead
POST /newsletters/{id}/issues/{issue_id}/approveApprove an issueRead and write
POST /newsletters/{id}/issues/{issue_id}/skipSkip an issueRead and write
DELETE /newsletters/{id}/issues/{issue_id}/signals/{signal_id}Take a signal out of an issueRead and write
DELETE /newsletters/{id}/issues/{issue_id}/tenders/{tender_id}Take a call for tender out of an issueRead and write
GET /newsletters/{id}/recipientsIts recipientsRead
POST /newsletters/{id}/recipientsAdd recipientsRead and write
DELETE /newsletters/{id}/recipients/{recipient_id}Remove a recipientRead and write
GET /newsletters/{id}/templateIts templateRead
POST /newsletters/{id}/template/blocksAdd a blockRead and write
PATCH /newsletters/{id}/template/blocks/{block_id}Change a blockRead and write
POST /newsletters/{id}/template/blocks/{block_id}/moveMove a blockRead and write
DELETE /newsletters/{id}/template/blocks/{block_id}Delete a blockRead and write
PATCH /newsletters/{id}/template/themeChange the themeRead and write

Radars

GET /radars: limit, cursor. A page of radars, news and tender radars alike: read each one's type.

GET /radars/{id}: the radar.

Create a radar

POST /radars: creates a radar, as Create radar in the app does, in a folder, and starts its first sweep over the last seven days. Returns 201 with the radar and the sweep.

Request
{
  "name": "Listed peers",
  "folder_id": 5,
  "query": {
    "lines": [
      {
        "joiner": null,
        "chips": [
          { "filter": "mergers_acquisitions" },
          { "filter": "new_deals" },
          { "text": "A competitor launching an AI inspection service" }
        ]
      },
      {
        "joiner": "AND",
        "chips": [{ "company_id": 91 }, { "company_id": 92 }]
      },
      { "joiner": "NOT", "chips": [{ "filter": "stock_analyses" }] }
    ]
  },
  "custom_source_ids": [12]
}
Response
{ "radar": { "id": 38, "name": "Listed peers", "sweeping": true, … }, "sweep": { "id": 911, "status": "queued", … } }

Where it searches is optional: sources, ["open_web"] or [], custom_source_ids and source_group_ids. Without them a radar searches the open web alone; with custom_source_ids or source_group_ids alone, the open web and those custom sources.

A chip is one of:

ChipMeans
{ "filter": "mergers_acquisitions" }A catalogue filter, by key (GET /filters lists them)
{ "filter": 301 }One of the account's own filters, by id
{ "text": "…" }A new filter in your own words, 10 to 2,000 characters. It is read when the radar is saved: the AI proposes up to three terms and writes its keeps
{ "text": "…", "terms": ["…"] }The same, with your own searches: 1 to 3 terms, taken as given. The AI proposes none and writes keeps from the sentence and the terms together
{ "company_id": 91 }A company of the account
{ "group_id": 3 }One of the account's company groups: the radar follows it

The rules, each failing with 422 and the field:

  • name is required, 1 to 120 characters.
  • A radar lives in a folder: send folder_id, one of the account's folders (GET /folders), or folder_name, 1 to 80 characters, to make the folder with the radar in the same transaction. One of the two, not both (field folder_id). Another account's folder fails on folder_id; a folder_name the account already has, whatever its case, fails with 409 conflict, field folder_name. No plan limits folders. The same holds for every type of radar.
  • The first line's joiner is null; every other line's is AND or NOT.
  • A line holds companies and company groups only, or filters only. There is at most one line of companies, and it is not a NOT line.
  • A query holds at most 50 chips, a group counting as its companies and a company counted once however it is reached (field query).
  • A mute filter goes on a NOT line.
  • A term takes 1 to 60 characters and is searched as written, with any double quotes dropped. The same term twice fails. Each fails on query, naming the rule.
  • An own filter saved with four or five terms, before the limit went to three on 6 October 2026, keeps them: { "filter": id } is never refused for its number of terms.
  • A sentence sent without terms that names more than three subjects (four standards, say) fails on query, asking to split it into two conditions.
  • Something must name what is searched: a line of companies, or a text chip or written filter. Catalogue filters alone (M&A about Finance) name nothing to search.
  • sources is optional: open_web, linkedin, both, or []; ["open_web"] when left out. linkedin reads the LinkedIn company pages of the radar's companies, so it needs a line of companies (field sources). [] reads the radar's custom sources alone.
  • custom_source_ids are the account's custom sources and source_group_ids its website groups; another account's fails on custom_source_ids or source_group_ids. Together they search at most 30 custom sources, a group counting as its custom sources, each once (field sources).
  • A radar keeps at least one source, of the three: it fails on sources with A radar keeps at least one source.

In the app, a subject (a topic) is placed on a line of its own, so that it narrows the radar instead of reading as one more alternative. The API takes the lines as sent.

Create a tender radar

POST /radars with type tenders: a tender radar, with countries and words in place of a query. Its first sweep looks at the calls of the last year and keeps those still open. Returns 201 with the radar and the sweep.

Request
{
  "name": "Solar carports, France",
  "type": "tenders",
  "folder_name": "Public tenders",
  "countries": ["FRA"],
  "words": [
    { "phrase": "ombrières photovoltaïques" },
    { "phrase": "toiture photovoltaïque" },
    { "phrase": "maintenance", "excluded": true }
  ]
}

The rules, each failing with 422 and the field:

  • countries: 1 to 10, each an ISO 3166 alpha-3 code of the EU or the EEA (FRA, BEL, DEU, …).
  • words: 1 to 20, each 2 to 120 characters, none twice whatever its case, and at least one not excluded. excluded is false when absent.

Create an exchange rate radar

POST /radars with type rates: an exchange rate radar, with pairs, interest_rates or both in place of a query. It is never swept. Returns 201 with the radar and sweep null.

Request
{
  "name": "Caillau Mobility, currencies",
  "type": "rates",
  "pairs": ["USD", "CNY", "BRL"],
  "interest_rates": ["estr", "aaa_3m"],
  "folder_id": 7
}

pairs: any of the 29 currencies the ECB publishes a reference rate for, each quoted against the euro (USD for EUR/USD), in the order to show them. A currency the ECB does not publish (the Saudi riyal, the UAE dirham, the Taiwan dollar) fails with 422, field pairs. interest_rates: any of estr, ecb_deposit, aaa_3m, aaa_6m, aaa_1y, borrowing_companies (interest rates); any other code, Euribor included, fails with 422, field interest_rates. Pairs and interest rates together are 1 to 20; sending neither fails with 422, field pairs.

Create a price radar

POST /radars with type commodity_prices: a price radar, with commodities in place of a query, in a folder (folder_id or folder_name) as every radar. It is never swept. Returns 201 with the radar and sweep null.

Request
{
  "name": "Mathevon, raw materials",
  "type": "commodity_prices",
  "folder_id": 7,
  "commodities": ["nickel", "cobalt", "brent"]
}

commodities: 1 to 20 codes, in the order to show them. An unknown code (tungsten, hot-rolled or stainless steel, which only price agencies publish) fails with 422, field commodities.

Edit a radar

PATCH /radars/{id}: any of name, query, sources, custom_source_ids, source_group_ids, folder_id. A query replaces the whole query, and sources, custom_source_ids and source_group_ids each the whole list; one left out stays as it is. folder_id moves the radar into that folder of the account, and nothing else: its query, its sweeps and its signals stay. Returns the radar, as sweep the sweep a changed query or changed sources started, or null, and as removed_signals how many signals a changed query took off the radar (0 otherwise, and on a tender, rate or price radar).

{ "radar": { "id": 38, … }, "sweep": { "id": 912, "started_by": "edited", … }, "removed_signals": 133 }
  • A new name alone is saved, or a folder_id alone; nothing else happens, even on a radar left with nothing to search.
  • A changed query, or changed sources, starts a sweep over the last seven days, as in the app, and the response carries it as sweep. The weekly sweep keeps its day. If a sweep is already running, it is stopped (its status reads cancelled) and the new one replaces it; what it already found stays.
  • Changed sources start a sweep the same way. A query that drops the last company of a radar reading linkedin fails with 422, field sources.
  • A changed query removes the signals it no longer matches, in the same transaction: those that matched no filter still on a line that must hold, and those about a company it takes out, directly or with a group, while it still names a company or a group. removed_signals says how many. An added filter, topic or NOT line removes nothing, nor does taking out a whole line, which widens the query, nor a change to a group's members. A signal a sent issue carries stays; one in an issue on its way leaves the issue. The other signals stay, with the filter and company they matched.
  • To change a written filter's text, replace its chip with a text chip. That makes a new filter for this radar; other radars using the old text keep it. The old filter's signals go with it, and the edit's sweep finds the last seven days again with the new text.

A tender radar takes any of name, countries, words, folder_id, and no query or sources. Each list replaces the old one. A change to either starts a sweep over the last year that keeps the calls still open, stopping one that runs; the calls already on the radar stay.

A body with the other type's fields fails with 422 and the field, and changes nothing: countries or words on a news radar, query or sources on a tender radar. So does POST /radars with countries or words but no type tenders.

An exchange rate radar takes any of name, pairs, interest_rates, folder_id. pairs replaces all its pairs and interest_rates all its interest rates (the one left out stays), and nothing is swept. Any other field fails with 422, and so do pairs or interest_rates on a news or tender radar, or on POST /radars without type rates.

A price radar takes any of name, commodities in the same way: commodities replaces all of them, nothing is swept, any other field fails with 422, and so do commodities on another type of radar, or on POST /radars without type commodity_prices.

Delete a radar

DELETE /radars/{id}: 204. Its sweeps, signals and sources are deleted, or a tender radar's calls. Its companies and filters stay in the account.

A radar's signals and sweeps

GET /radars/{id}/signals: since (a date: signals first reported on or after it), limit, cursor. Newest first by first_reported_at.

GET /radars/{id}/sweeps: limit, cursor. To follow a sweep you started, read the radar's sweeping, or the first sweep of this list, every few seconds. A sweep takes a few minutes.

An exchange rate radar's rates

GET /radars/{id}/rates: since (a date: each rate from it on). An exchange rate radar's pairs, in its order, each with the ECB's latest euro reference rate, its change in per cent over a week and three months, and the year's low and high. The ECB publishes once a working day, around 16:00 Frankfurt time; a rate is how much of the currency one euro buys. Then its interest_rates, each an interest rate: in per cent, its change in basis points, and the day it last moved. A news or tender radar's lists are empty.

{
  "data": [
    {
      "currency": "USD",
      "pair": "EUR/USD",
      "name": "US dollar",
      "latest": { "day": "2026-09-29", "rate": 1.1355 },
      "change": { "week": -0.94, "three_months": -0.45 },
      "year": {
        "low": { "day": "2026-09-29", "rate": 1.1355 },
        "high": { "day": "2026-03-06", "rate": 1.1974 }
      }
    }
  ],
  "source": "ECB euro foreign exchange reference rates",
  "interest_rates": [
    {
      "code": "ecb_deposit",
      "name": "ECB deposit rate",
      "description": "Deposit facility",
      "unit": "%",
      "cadence": "daily",
      "publisher": "ECB",
      "credit": "Source: ECB",
      "latest": { "day": "2026-09-30", "rate": 2.5 },
      "changed_on": "2026-09-16",
      "change_bp": { "week": 0, "three_months": 25, "month": null, "year": null },
      "year": {
        "low": { "day": "2025-10-01", "rate": 2 },
        "high": { "day": "2026-09-16", "rate": 2.5 }
      }
    }
  ]
}

A price radar's prices

GET /radars/{id}/prices: since (a date: each commodity's prices from it on). A price radar's commodities, in its order, each a price: the latest in its unit, its publisher, its change over a week and three months when priced daily or a month and a year when priced monthly, and the year's low and high. Another type of radar's list is empty.

{
  "data": [
    {
      "code": "nickel",
      "name": "Nickel",
      "unit": "USD/t",
      "cadence": "daily",
      "publisher": "market data",
      "latest": { "day": "2026-09-29", "price": 16540 },
      "change": {
        "week": -1.2,
        "three_months": -6.4,
        "month": null,
        "year": null
      },
      "range": {
        "low": { "day": "2026-01-13", "price": 14590 },
        "high": { "day": "2026-05-04", "price": 18920 }
      }
    }
  ]
}

A tender radar's calls

GET /radars/{id}/tenders: status (open or closed; both when absent), limit, cursor. A page of calls for tender, newest found first. A news radar has none.

GET /api/v1/radars/41/tenders?status=open

Folders

GET /folders: the account's folders by name, each with its radars' ids. Not paged: an account holds at most 20.

Response
{
  "data": [
    {
      "id": 5,
      "name": "Competitors",
      "radar_ids": [37, 38],
      "created_at": "2026-09-30T09:12:40Z"
    },
    {
      "id": 6,
      "name": "Public tenders",
      "radar_ids": [41],
      "created_at": "2026-09-30T09:14:02Z"
    }
  ]
}

POST /folders: creates an empty folder, as Create folder in the app does. Returns 201 with the folder.

Request
{ "name": "Caillau Mobility" }
Response
{
  "id": 8,
  "name": "Caillau Mobility",
  "radar_ids": [],
  "created_at": "2026-10-01T09:20:15Z"
}
  • name: required, 1 to 80 characters once trimmed, unique in the account whatever its case. One too short or too long fails with 422, field name; a name taken fails with 409 conflict, field name. No plan limits folders.

Put a radar in it with POST /radars and its id as folder_id, or move one in with PATCH /radars/{id}.

PATCH /folders/{id}: renames a folder, as Rename in the app does. Returns 200 with the folder; its radars stay in it.

Request
{ "name": "Caillau Mobility, 2027" }
  • name: required, with the rules of POST /folders. Another folder's name, whatever its case, fails with 409 conflict, field name; the folder's own name in another case is saved.

DELETE /folders/{id}: 204. As in the app, its radars are deleted with it, each as DELETE /radars/{id} would: their sweeps, signals and sources, or a tender radar's calls, go too, and each radar leaves the newsletters that used it. Companies, custom sources, groups and filters stay. To keep a radar, move it to another folder first (PATCH /radars/{id} with folder_id).

Groups

GET /groups: the account's groups, company groups first, each kind by name, with their members' ids and the radars that follow them. kind, companies or websites, lists one kind only. Not paged.

Response
{
  "data": [
    {
      "id": 3,
      "name": "French banks",
      "kind": "companies",
      "member_ids": [91, 92, 93],
      "radar_ids": [37],
      "created_at": "2026-09-30T10:02:11Z"
    },
    {
      "id": 4,
      "name": "Regulators",
      "kind": "websites",
      "member_ids": [12],
      "radar_ids": [37],
      "created_at": "2026-09-30T10:04:40Z"
    }
  ]
}

POST /groups: returns 201 with the group.

Request
{ "kind": "companies", "name": "French banks", "member_ids": [91, 92, 93] }
  • kind: required, companies or websites.
  • name: required, 1 to 60 characters, unique among the account's groups of its kind whatever its case: a name taken fails with 409 conflict, field name.
  • member_ids: optional, its companies or its custom sources, as kind says. One that is not the account's fails with 422, field member_ids, and no group is made.

No radar follows a new group: name it in a radar's query ({ "group_id": 3 }) or its source_group_ids.

GET /groups/{id}: the group.

PATCH /groups/{id}: name, member_ids, or both. member_ids replaces the whole list ([] empties the group); a company or custom source taken out stays in the account. A group's kind never changes: a body that sends one fails with 422. Every radar following the group searches its members as they are from its next sweep; no sweep starts. A member that would take a following radar past its limit (50 companies and filters in a query, 30 custom sources) fails with 422, field member_ids, naming the radar, and nothing changes.

DELETE /groups/{id}: 204. As in the app, its members are deleted with it, each as DELETE /companies/{id} or DELETE /custom-sources/{id} would, so a member leaves its other groups too, and every radar that followed the group or searched a member stops searching them. The signals already found stay. To delete the group alone, PATCH its member_ids to [] first.

Signals

GET /signals: every radar's signals. since, radar_id, q (words searched in headlines and summaries, each must match, at most eight), limit, cursor.

GET /api/v1/signals?since=2026-09-15&q=acquire&limit=20

GET /signals/{id}: the signal with its sources.

Companies

GET /companies: limit, cursor. A page of companies.

Add a company

POST /companies: returns 201 with the company.

Request
{
  "name": "BNP Paribas",
  "website": "bnpparibas.com"
}
  • name: required, 1 to 120 characters.
  • website: required, a domain or a URL, stored as its domain. Unique in the account: a website the account already holds fails with 409 conflict, field website.
  • linkedin_url: optional, the company's LinkedIn company page, under /company/ or /showcase/, in any form (linkedin.com/company/bnp-paribas, with or without https://, www., a country prefix or a sub-page). Stored as https://www.linkedin.com/company/bnp-paribas. A person's profile or any other address fails with 422; a page another of the account's companies holds fails with 409 conflict, field linkedin_url.

Edit a company

PATCH /companies/{id}: any of name, website, linkedin_url (null removes the page; left out, it stays). Every radar watching the company uses the new details from its next sweep. No sweep starts. A website another of the account's companies holds fails with 409 conflict.

Delete a company

DELETE /companies/{id}: 204. The company leaves every radar's query (its radar_ids say which, before you delete). The signals already found about it stay. A radar left with nothing to search reads searchable: false.

Custom sources

GET /custom-sources: limit, cursor. A page of custom sources.

GET /custom-sources/{id}: the custom source.

Add a custom source

POST /custom-sources: returns 201 with the custom source.

Request
{ "name": "ACPR", "address": "https://acpr.banque-france.fr/en/news" }
  • name: required, 1 to 120 characters.
  • address: required, a domain or a URL, stored as its domain (acpr.banque-france.fr). Unique in the account: an address the account already holds fails with 409 conflict, field address.

To search it, add its id to a radar's custom_source_ids.

Edit a custom source

PATCH /custom-sources/{id}: any of name, address. Every radar searching it uses the new details from its next sweep. No sweep starts.

Delete a custom source

DELETE /custom-sources/{id}: 204. Every radar that searched it stops (its radar_ids say which, before you delete); a radar for which it was the last source searches the open web again. The signals already found on it stay. No sweep starts.

Filters

GET /filters: section (what_happened, subject, leave_out) narrows the catalogue. Not paged.

{
  "catalogue": [
    {
      "section": "what_happened",
      "category": { "key": "deals_finance", "label": "Deals and finance" },
      "filters": [
        {
          "id": 212,
          "key": "mergers_acquisitions",
          "kind": "event",
          "label": "M&A",
          "definition": "…"
        },
        {
          "id": null,
          "key": "funding_events",
          "kind": "event",
          "label": "Funding Events",
          "definition": "…"
        }
      ]
    }
  ],
  "own": [
    {
      "id": 301,
      "key": null,
      "kind": "written",
      "label": "A competitor launching or…",
      "definition": "A competitor launching or buying an AI inspection service",
      "terms": ["AI inspection", "inspection service"],
      "keeps": "Articles on a watched company launching or buying an AI inspection service"
    }
  ]
}

Newsletters

Newsletters are the account's emails of what its radars found (the app's Newsletters). A newsletter has settings, a template and recipients; each week or month it assembles an issue, which a reviewer may approve or skip before it goes out. Live, which sends real email to real recipients, is turned on and off in the app only: the API and the MCP server read it.

GET /newsletters: limit, cursor. A page of newsletters, newest first.

GET /newsletters/{id}: the newsletter.

Create a newsletter

POST /newsletters: returns 201 with the newsletter, not live, with a header and a footer and the account's last saved theme, as Create newsletter in the app.

Request
{ "name": "Weekly Market Radar", "time_zone": "Europe/Paris" }
  • name: required, 1 to 80 characters, unique in the account (409 conflict, field name).
  • time_zone: optional, an IANA zone; Europe/Paris by default. An unknown zone fails with 422 on time_zone.

Change a newsletter's settings

PATCH /newsletters/{id}: any of the fields below; what is not sent stays. All or nothing: a request that fails a rule changes nothing.

Request
{
  "schedule": { "cadence": "weekly", "weekday": 1, "time": "09:00" },
  "subject_pattern": "issue_date",
  "review": true,
  "reviewer": "aiko@acme.com",
  "hold_back": false
}
  • name: 1 to 80 characters, unique in the account.
  • schedule: any of cadence (weekly; monthly is refused for now), weekday (ISO, 1 is Monday), time (HH:MM, on the hour or the half hour) and time_zone.
  • subject_pattern: issue (Weekly Market Radar, issue 15), date (…, 5 October) or issue_date (both).
  • review: each issue goes to the reviewer an hour before it is sent. It needs a reviewer, in the same request or already set; without one it fails with 422 on reviewer.
  • reviewer: a member of the workspace, by email; null for none, which fails while review is on. An address that is not a member's fails with 422.
  • hold_back: an issue not approved in time is held back; otherwise it is sent.
  • live fails with 422 on live: turn it on or off in the app.

Delete a newsletter

DELETE /newsletters/{id}: 204. Its issues, their web versions and its recipients' subscriptions go, and no more issues are sent. Its radars and their signals stay.

Issues

GET /newsletters/{id}/issues: limit, cursor. A page of issues, newest first by send: the one on its way comes first once it is assembled.

GET /newsletters/{id}/issues/{issue_id}: the issue with its blocks: each radar, rates and prices block in order, with its signals, calls for tender, rates or prices, as readers get them.

POST /newsletters/{id}/issues/{issue_id}/approve: the issue awaiting review goes out at its send time. Returns the issue. Approving one already approved, or on its way out, changes nothing and answers the same, so a retry is safe; approving one skipped or held back fails with 409 conflict, field status.

POST /newsletters/{id}/issues/{issue_id}/skip: the issue awaiting review, approved or scheduled is not sent. Skipping one already skipped answers the same; skipping one sent fails with 409 conflict.

DELETE /newsletters/{id}/issues/{issue_id}/signals/{signal_id} and DELETE /newsletters/{id}/issues/{issue_id}/tenders/{tender_id}: the signal, or the call for tender, leaves this issue only, until it starts sending: the radar keeps it, and the block does not refill. Returns the issue. An issue already sent fails with 409 conflict.

An approval or a skip records who made it: the member an assistant signed in acts for, or the member who made the key while they are still a member, or else the key.

Recipients

GET /newsletters/{id}/recipients: limit, cursor, and status (subscribed, unsubscribed or bounced). A page of recipients, newest first.

POST /newsletters/{id}/recipients: returns how many were added, already_there, unsubscribed (not added) and invalid.

Request
{
  "recipients": [
    { "email": "aiko@acme.com", "name": "Aiko Tanaka" },
    { "email": "tomas@acme.com" }
  ]
}

New addresses are subscribed. An address already there is left as it is. One that unsubscribed or bounced is never subscribed again from here: only its owner can, from the link in the email. At most 2,000 recipients a newsletter (403 account_limit).

DELETE /newsletters/{id}/recipients/{recipient_id}: 204, a subscribed recipient. One who unsubscribed or bounced stays, so the address is not added again by mistake: it fails with 409 conflict, field status.

The template

GET /newsletters/{id}/template: the template and its template_version.

A template changes one block at a time. Each change applies from the next issue assembled; an issue already assembled keeps what it was assembled with. Each answers the new template_version. Two changes made at once both land; a member with the editor open is told the template changed when they next save.

POST /newsletters/{id}/template/blocks: returns 201 with the block.

Request
{
  "type": "radar",
  "after": "a1b2c3d4e5",
  "radar_ids": [37],
  "title": "Competitors",
  "max": 5
}
  • type: radar, rates, prices, heading, text or divider. A template has one header and one footer, always there.
  • after: the block to place it after; just above the footer when absent. Nothing goes below the footer.
  • Any of the type's fields; the rest start as the editor's palette drops them. A field the type has not fails with 422, naming it.
  • A radar block's kind follows its radar_ids, and is never sent: news radars or tender radars, never both; a tender block orders by closing date. A rates block takes exchange rate radars only, a prices block commodity price radars only.
  • A text block also takes text: plain text, a blank line between paragraphs.
  • At most 30 blocks.

PATCH /newsletters/{id}/template/blocks/{block_id}: any of the block's fields; what is not sent stays, and show changes key by key. Its id and type do not change. Returns the block.

POST /newsletters/{id}/template/blocks/{block_id}/move: { "after": "d4e5f6" }. The header stays first and the footer last. Returns the template.

DELETE /newsletters/{id}/template/blocks/{block_id}: any block but the header and the footer. Returns the template.

PATCH /newsletters/{id}/template/theme: any of colors (brand, text, background, as #RRGGBB), fonts (headings, text: geist, inter, source-serif, libre-caslon, arial or georgia) and logo: null removes it. A logo is uploaded in the app; an address that is not one of the account's uploads fails with 422 on logo.