openapi: 3.2.0 info: title: Bird Email Competitive API version: 1.0.0 description: 'The Bird API: one REST API for email, SMS, WhatsApp, verification, and Realtime.' servers: - url: https://{region}.platform.bird.com description: 'Regional API endpoint. Use the host for the region your organization is hosted in. Official Bird SDKs and the CLI select it automatically from your API key, so you rarely need to set it by hand. ' variables: region: default: us1 enum: - us1 - eu1 description: The region your organization's data is hosted in. - url: https://platform.bird.com description: Region-independent endpoint for authentication and account administration. - url: http://localhost:8080 description: Local development. security: - BearerAuth: [] tags: - name: email-competitive description: Watch competitor brands and see how their email compares with yours. Figures about a competitor are estimates from an email panel, which observes a sample of real inboxes; figures about your own sending are counted rather than estimated wherever that is possible. paths: /v1/email/competitive/watchlist: get: x-snippet-key: email.competitive.watchlist.get operationId: getEmailCompetitiveWatchlist summary: Get the competitor watchlist with its latest figures description: 'Returns every competitor brand on the workspace''s watchlist, plus a row for your own sending, each with estimated send volume and how it changed against the previous period, how often the brand sends, inbox placement, estimated read rate, audience overlap with you, and the most recent campaign seen. Figures about a competitor are estimates from an email panel, which observes a sample of real inboxes and scales what it sees up to a whole audience. They are fetched while the request runs, so they are current rather than cached, and two requests minutes apart can differ slightly. Figures about your own sending are counted rather than estimated wherever that is possible. The `provenance` object on each row records which source each figure came from. A figure reads `null` when it is unavailable for that brand, so a `0` always means a real measurement. When a whole row has no figures, `panel_status` says why: the panel may not track the brand''s sending domain, may track it but have seen no mail in the period, or may have been briefly unreachable. `esp` and `list_size` are the exception, and are always `null` here. Read a single brand to get them. API-key calls require Insights preview access for your organization.' tags: - email-competitive security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command parameters: - $ref: '#/components/parameters/EmailCompetitiveRange' responses: '200': description: The watchlist with its figures for the requested period. content: application/json: schema: $ref: '#/components/schemas/EmailCompetitiveWatchlist' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk /v1/email/competitive/watchlist/notable: get: x-snippet-key: email.competitive.watchlist.notable operationId: getEmailCompetitiveNotableCampaigns summary: Get the notable campaigns across watched brands description: 'Returns the campaigns worth a second look across every brand the workspace watches, surfaced for what they did rather than for when they were sent. Each campaign carries the signal that surfaced it: an unusually big send for its brand, a campaign read unusually well for its brand, or heavy spam placement at one mailbox provider. Every signal compares a campaign against its own brand''s history, never against your other brands, so several brands can carry the same signal in one period. Up to 100 findings are returned. Selection takes turns across brands in watchlist order until the response is full, prioritizing spam placement, biggest sends, then read-rate standouts within a brand. Returned findings retain watchlist, signal, domain, and source order. Your own sending is never included. An empty list is an ordinary answer, not a failure: a signal only fires on a campaign that stands out for its own brand, and a watchlist of steady senders produces nothing. Check `panel_status` to tell that apart from the panel being unreachable. This is a separate request from the watchlist on purpose, so a slow or degraded panel read cannot delay the watchlist itself. API-key calls require Insights preview access for your organization.' tags: - email-competitive security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command parameters: - $ref: '#/components/parameters/EmailCompetitiveRange' responses: '200': description: The notable campaigns for the period. content: application/json: schema: $ref: '#/components/schemas/EmailCompetitiveNotableFeed' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk /v1/email/competitive/watchlist/brands: post: x-snippet-key: email.competitive.watchlist.brands.create operationId: createEmailCompetitiveWatchlistBrand summary: Add a competitor brand to the watchlist description: 'Adds a brand to the workspace''s watchlist so its figures appear next to your own. Pass a `brand_id` from a brand search. Adding a brand records the one domain the panel sees the most of its mail from, and every figure reported for the brand describes that domain. A brand that mails from several domains therefore reports less than its full volume. A brand the panel has never seen send cannot be measured at all and is refused. How many brands can be watched is capped per organization, counted across every workspace it owns, so the same competitor watched from two workspaces uses two of the allowance. API-key, OAuth, and service-account calls require Insights preview access for your organization.' tags: - email-competitive security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EmailCompetitiveWatchlistBrandCreate' example: brand_id: '81531' responses: '201': description: The brand was added to the watchlist. headers: Idempotency-Replay: $ref: '#/components/headers/IdempotencyReplay' content: application/json: schema: $ref: '#/components/schemas/EmailCompetitiveWatchlistBrand' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk /v1/email/competitive/watchlist/brands/{watchlist_brand_id}: parameters: - name: watchlist_brand_id in: path required: true description: The watchlist entry to act on. schema: $ref: '#/components/schemas/CompetitiveWatchlistBrandID' get: x-snippet-key: email.competitive.watchlist.brands.get operationId: getEmailCompetitiveBrand summary: Get a watched brand's figures description: 'Returns one watched brand''s figures for the period, together with how each mailbox provider treated its mail and how that compares with your own. The headline figures are the ones the watchlist reports for this brand, derived the same way from the same fields. Estimated volume can differ very slightly between the two views, because each request asks the panel about a different set of domains and the panel scales its estimate per request. The figures the two views share are either rates or built from raw counts, and are identical. The per-provider breakdown, `esp`, and `list_size` are available only here. Every figure is an estimate from an email panel, fetched while the request runs, except your own inbox rate where noted. API-key calls require Insights preview access for your organization.' tags: - email-competitive security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command parameters: - $ref: '#/components/parameters/EmailCompetitiveRange' responses: '200': description: The brand's figures for the period. content: application/json: schema: $ref: '#/components/schemas/EmailCompetitiveBrandProfile' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk delete: x-snippet-key: email.competitive.watchlist.brands.delete operationId: deleteEmailCompetitiveWatchlistBrand summary: Remove a competitor brand from the watchlist description: 'Takes a brand off the workspace''s watchlist and frees its place in the organization''s allowance. Nothing about the brand is retained, so adding it again starts a fresh entry. API-key calls require Insights preview access for your organization.' tags: - email-competitive security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '204': description: The brand was removed from the watchlist. '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk /v1/email/competitive/watchlist/brands/{watchlist_brand_id}/campaigns: parameters: - name: watchlist_brand_id in: path required: true description: The watchlist entry whose campaigns to return. schema: $ref: '#/components/schemas/CompetitiveWatchlistBrandID' get: x-snippet-key: email.competitive.watchlist.brands.campaigns.list operationId: getEmailCompetitiveBrandCampaigns summary: Get the campaigns a watched brand sent description: 'Returns a page of campaigns an email panel observed a watched brand sending over the period, in the requested order. Sampled statistics describe eligible campaigns in the first 300 newest panel rows for each tracked domain, independently of the page. Each campaign is one send the panel saw reach real inboxes, so the subject and timing are what the brand''s subscribers received rather than anything the brand published. Volume and read rate are panel estimates, fetched while the request runs. The panel folds a day''s low-volume sending into a single synthetic entry with no creative and no volume. Those are left out, so the count here is lower than the number of rows the panel holds and describes campaigns a person would recognise as campaigns. API-key calls require Insights preview access for your organization.' tags: - email-competitive security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command parameters: - $ref: '#/components/parameters/EmailCompetitiveRange' - name: sort in: query required: false description: Field to sort campaigns by. schema: $ref: '#/components/schemas/EmailCompetitiveCampaignSort' - $ref: '#/components/parameters/OrderDesc' - $ref: '#/components/parameters/PaginationLimit' - $ref: '#/components/parameters/StartingAfter' - $ref: '#/components/parameters/EndingBefore' responses: '200': description: The brand's campaigns for the period. content: application/json: schema: $ref: '#/components/schemas/EmailCompetitiveCampaignFeed' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk /v1/email/competitive/watchlist/brands/{watchlist_brand_id}/campaigns/{campaign_id}: parameters: - name: watchlist_brand_id in: path required: true description: The watched brand that sent the campaign. schema: $ref: '#/components/schemas/CompetitiveWatchlistBrandID' - name: campaign_id in: path required: true description: The campaign to return, taken from the brand's campaign list. schema: type: string minLength: 1 maxLength: 19 pattern: ^[0-9]+$ example: '3914827265' get: x-snippet-key: email.competitive.watchlist.brands.campaigns.get operationId: getEmailCompetitiveBrandCampaign summary: Get one campaign a watched brand sent description: 'Returns one campaign an email panel observed a watched brand sending. It carries the same figures the brand''s campaign list gives for that campaign, so a page can open one campaign without reading the whole list first. The campaign has to be one the brand in the path sent. An identifier that belongs to another brand''s campaign comes back as not found, whether or not the panel holds it. A workspace can read the campaigns of the brands it watches, and no others. The panel folds a day of low-volume sending into one synthetic entry, and the campaign list leaves those out. They stand for a day of sending rather than for a campaign anyone sent, so they come back as not found here as well. API-key calls require Insights preview access for your organization.' tags: - email-competitive security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command responses: '200': description: The campaign. content: application/json: schema: $ref: '#/components/schemas/EmailCompetitiveCampaign' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk /v1/email/competitive/watchlist/brands/{watchlist_brand_id}/send-time: parameters: - name: watchlist_brand_id in: path required: true description: The watchlist entry whose sending pattern to return. schema: $ref: '#/components/schemas/CompetitiveWatchlistBrandID' get: x-snippet-key: email.competitive.watchlist.brands.sendTime operationId: getEmailCompetitiveBrandSendTime summary: Get when a watched brand sends description: 'Returns how a watched brand''s sending is spread across the week: one figure per weekday and hour of the day, over the last 90 days, with the hour of the day it sends most of its mail in. Each hour counts when the brand **sent**, not when its subscribers opened or received the mail. It answers "when does this brand mail its list", which is what a competing send has to be timed against. It says nothing about how busy a subscriber''s inbox was at that hour. Hours are reported in the timezone you ask for, echoed back in `timezone`, and the week is folded into that zone before it is totalled, so a send at 02:00 UTC on Monday counts as Sunday evening for a reader in New York, which is when it arrived for them. Label an axis from `timezone` rather than from what you asked for: a response the panel could not answer reports UTC regardless. Expect the weekday axis to look flat. For most brands the hour of the day is where the pattern is, and which day of the week it is barely moves the figure; a grid with little variation down its rows is a real finding about how the brand mails rather than a gap in the data. API-key calls require Insights preview access for your organization.' tags: - email-competitive security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command parameters: - $ref: '#/components/parameters/EmailCompetitiveTimezone' responses: '200': description: The brand's sending pattern for the period. content: application/json: schema: $ref: '#/components/schemas/EmailCompetitiveSendTimeGrid' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk /v1/email/competitive/brands/search: get: x-snippet-key: email.competitive.brands.search operationId: searchEmailCompetitiveBrands summary: Search brands to add to the watchlist description: 'Searches for brands by name and returns the ones that can be watched, each with the domain its figures would describe and the identifier to add it with. Paste a domain instead of a name to find the brand that sends from it. Brands the panel has never seen send are left out, since no figure could be reported for them. An empty result for a real brand name therefore means the panel does not track that brand rather than that the search failed. API-key calls require Insights preview access for your organization.' tags: - email-competitive security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command parameters: - name: q in: query required: true description: A brand name, or a sending domain to look up the brand behind it. schema: type: string minLength: 2 maxLength: 128 example: everlane responses: '200': description: Brands matching the search. content: application/json: schema: $ref: '#/components/schemas/EmailCompetitiveBrandSearchResults' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk /v1/email/competitive/volume-series: get: x-snippet-key: email.competitive.volumeSeries operationId: getEmailCompetitiveVolumeSeries summary: Get send volume over time for watched brands description: 'Returns daily send volume for the watched brands you name, plus a line for your own sending, over one shared axis. Intended for a chart comparing a handful of competitors against yourself rather than the whole watchlist: each brand you name and each month of range adds to how long the request takes, so ask for the few you are plotting. Competitor volume is an estimate from an email panel, fetched while the request runs. Your own line counts messages accepted for delivery. The `source` field on each line records which of the two it is, and the two are not measuring the same thing, so a chart putting them on one axis should say so. Every line carries one point per day of the period, oldest first, with a `0` for a day nothing was observed, so the lines need no aligning before plotting. The last point is the last whole UTC day, not the one in progress, so your own line and a competitor''s cover the same days. A line with no points at all has a `panel_status` saying why. API-key calls require Insights preview access for your organization.' tags: - email-competitive security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command parameters: - $ref: '#/components/parameters/EmailCompetitiveRange' - name: brand_ids in: query required: false description: 'Which watched brands to plot, in the order you want the lines. Omit to get only your own line. An id your workspace does not watch is rejected rather than skipped, so a chart cannot quietly lose a line. ' style: form explode: false schema: type: array minItems: 1 maxItems: 20 uniqueItems: true items: $ref: '#/components/schemas/CompetitiveWatchlistBrandID' responses: '200': description: The requested lines for the period. content: application/json: schema: $ref: '#/components/schemas/EmailCompetitiveVolumeSeries' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk components: schemas: EmailCompetitivePeriod: type: object additionalProperties: false description: 'The period every figure in the response covers, echoed back from the request. Figures are fetched when the request is made, so they are current as of `to`. The period always ends at the moment of the request rather than at a cached boundary, which is why two requests a minute apart can differ slightly. ' required: - days - from - to properties: days: type: integer readOnly: true description: Length of the period in days. example: 30 from: type: string format: date-time minLength: 1 readOnly: true description: Start of the period, inclusive. example: '2026-07-13T09:00:00Z' to: type: string format: date-time minLength: 1 readOnly: true description: 'End of the period, exclusive. Daily figures therefore run through the previous whole UTC day and never include the one in progress. ' example: '2026-08-12T09:00:00Z' EmailCompetitiveWatchlistRowProvenance: type: object additionalProperties: false description: 'Where each figure on the row came from, so a comparison can be labelled honestly. Every field on a competitor''s row is a panel estimate. On your own row the source varies by field: what is counted directly is reported as measured, falls back to the panel for what is not, and reports `none` for a field this row never carries at all. Read rate is a panel estimate even on your own row. Comparing a measured rate against a panel estimate of the same rate is not a like for like comparison, because the two count an open differently, so both sides of the comparison come from the panel. ' required: - sends - cadence_per_week - inbox_placement_rate - read_rate - audience_overlap_rate - last_campaign properties: sends: $ref: '#/components/schemas/EmailCompetitiveFieldSource' description: Source of `sends` and of `sends_change_percent`, which is derived from it. cadence_per_week: $ref: '#/components/schemas/EmailCompetitiveFieldSource' description: Source of `cadence_per_week`. inbox_placement_rate: $ref: '#/components/schemas/EmailCompetitiveFieldSource' description: Source of `inbox_placement_rate`. read_rate: $ref: '#/components/schemas/EmailCompetitiveFieldSource' description: Source of `read_rate`. audience_overlap_rate: $ref: '#/components/schemas/EmailCompetitiveFieldSource' description: Source of `audience_overlap_rate`. last_campaign: $ref: '#/components/schemas/EmailCompetitiveFieldSource' description: Source of `last_campaign`. ErrorBody: type: object additionalProperties: false required: - type - code - name - message - doc_url - request_id properties: type: type: string minLength: 1 description: Broad category for coarse client branching. enum: - auth_error - bad_request_error - billing_error - conflict_error - gone_error - internal_error - misdirected_error - not_found_error - not_implemented_error - payload_too_large_error - permission_error - precondition_error - rate_limit_error - service_unavailable_error - too_early_error - validation_error code: type: string minLength: 1 pattern: ^E\d{5}$ description: Opaque, stable, unique error identifier. Never reused. name: type: string minLength: 1 description: Human-readable slug for log readability. Paired with code, never replaces it. message: type: string minLength: 1 description: Human-readable description. Not stable; clients must not parse it. param: type: string minLength: 1 description: Identifies the offending field. Omitted when not applicable. doc_url: type: string minLength: 1 format: uri description: Stable link to the docs page for this error code. request_id: type: string minLength: 1 description: Request correlation ID for support and troubleshooting. Also returned in the `X-Request-Id` response header. vendor_code: type: string minLength: 1 description: 'Verbatim error code from an external system, such as an SMTP response code or a payment decline code. Present only when the code may help you resolve the error. ' details: type: array description: Per-field validation errors. Present only on validation_error responses. items: $ref: '#/components/schemas/ErrorDetail' remediation: type: string minLength: 1 description: A human-readable next step to resolve this error. Present when a recovery is known. next: type: array description: 'The steps that resolve this error. Perform them in order, re-reading after each; a `wait` or `terminal` step is always last. Present for errors with a well-defined recovery, such as unmet preconditions and conflicts. ' items: $ref: '#/components/schemas/NextAction' EmailCompetitiveSendTimeCell: type: object additionalProperties: false description: One weekday and hour of a brand's sending week. required: - weekday - hour - share_percent - intensity - sample_days properties: weekday: $ref: '#/components/schemas/EmailCompetitiveWeekday' readOnly: true description: The day of the week this hour falls on. hour: type: integer readOnly: true minimum: 0 maximum: 23 description: 'The hour this cell covers, in the timezone the response reports. `13` covers 13:00 to 14:00. ' example: 13 share_percent: type: number readOnly: true description: 'Share of everything the brand sent over the period that fell in this hour. It is `0` for an hour the brand demonstrably did not send in, which on a disciplined sender is the most useful thing this grid says. ' example: 3.4 intensity: type: number readOnly: true minimum: 0 maximum: 1 description: 'How strongly the brand sends in this hour, against its own busiest hour at `1`. It is this cell''s sending per `sample_days` divided by the busiest cell''s, so it is derivable from the two numbers beside it and reconciles with them rather than competing: it is published because that correction is easy to get wrong, not because it knows anything they do not. Shade a cell by this rather than by `share_percent`: the period holds one more of some weekdays than others, so a share compares an hour that came round thirteen times against one that came round twelve. ' example: 0.55 sample_days: type: integer readOnly: true description: 'How many days of the period fell on this weekday, whether or not the brand sent on them. It is what separates an hour the brand is quiet in from one there was little chance to observe. ' example: 13 EmailCompetitiveNotableCampaign: type: object additionalProperties: false description: A campaign the panel surfaced, and the reason it did. required: - watchlist_brand_id - brand_name - signal - claim - evidence - mailbox_provider - campaign properties: watchlist_brand_id: $ref: '#/components/schemas/CompetitiveWatchlistBrandID' readOnly: true description: The watchlist entry that sent it. brand_name: type: string minLength: 1 readOnly: true description: The brand's name. example: Allbirds signal: $ref: '#/components/schemas/EmailCompetitiveCampaignSignal' readOnly: true claim: oneOf: - $ref: '#/components/schemas/EmailCompetitiveNotableClaim' - type: 'null' readOnly: true description: 'The panel''s own headline for this campaign, or null where it surfaced the campaign without making one. Null is the common case and is not a fault. ' evidence: $ref: '#/components/schemas/EmailCompetitiveNotableEvidence' readOnly: true description: The figures behind the signal, for ordering or filtering the list. mailbox_provider: oneOf: - $ref: '#/components/schemas/EmailCompetitivePanelMailboxProvider' - type: 'null' readOnly: true description: 'The provider a `landing_in_spam` campaign was heavily filed as spam at: spam placement is measured per provider, and this campaign''s problem is at one of them. Null on every other signal. ' campaign: $ref: '#/components/schemas/EmailCompetitiveCampaign' description: 'The campaign itself. Two of its fields behave differently here than on the brand''s campaign feed, because the panel sends less about a campaign it surfaced this way. `has_creative` reports whether a capture came back with this entry rather than whether the panel ever captured the email, and captures are frequently absent here by design, so expect `false` on campaigns the panel did image. `reach` is null on every entry, because the panel does not estimate an audience for the campaigns it surfaces. ' EmailCompetitiveCampaign: description: One campaign an email panel observed a brand sending. unevaluatedProperties: false allOf: - $ref: '#/components/schemas/EmailCompetitiveCampaignSummary' - type: object required: - reach - read_rate - has_creative - discount_percent - inbox_rate - spam_rate properties: reach: type: - integer - 'null' format: int64 readOnly: true description: 'Estimated recipients this campaign reached, null when the panel observed the campaign but published no estimate for it. ' example: 410000 read_rate: type: - number - 'null' readOnly: true description: 'Estimated share of recipients who read this campaign, null when the panel published no rate for it. Panel read rates count dwell time, so they do not move with the automatic opens that inflate a sender''s own open rate. ' example: 0.228 has_creative: type: boolean readOnly: true description: Whether the panel captured the rendered email for this campaign. example: true discount_percent: type: - number - 'null' readOnly: true description: 'The discount the subject line leads with, null when it names none. Read from the subject text, so it finds a stated offer and not one revealed inside the email. ' example: 40 inbox_rate: type: - number - 'null' readOnly: true description: 'Share of this campaign that reached an inbox, null when the panel observed it without recording where it landed. It describes this send rather than the brand''s domain, so a single bad campaign is visible against a brand whose overall placement still looks healthy. ' example: 0.879 spam_rate: type: - number - 'null' readOnly: true description: Share of this campaign that was filed as spam, null on the same terms. example: 0.121 EmailCompetitiveWeekday: type: string minLength: 1 description: 'A day of the week. Named rather than numbered because the two common numberings disagree about which day the week starts on. ' enum: - monday - tuesday - wednesday - thursday - friday - saturday - sunday example: tuesday EmailCompetitiveNotableFeed: type: object additionalProperties: false description: Campaigns worth a second look across the brands a workspace watches. required: - period - panel_status - data - truncated properties: period: $ref: '#/components/schemas/EmailCompetitivePeriod' description: 'The period the campaigns were observed in, as the panel resolved it. Two things differ from the other competitive reads. It ends at the last instant of the previous whole day rather than at the moment of the request, so a campaign sent this morning is never among these. And the panel holds its selection for a period once it has made it, so two requests a minute apart return the same campaigns rather than differing slightly. ' panel_status: $ref: '#/components/schemas/EmailCompetitivePanelStatus' description: Whether the panel could be read for this feed, and when it could not, why. data: type: array readOnly: true maxItems: 100 description: 'Up to 100 campaigns selected across watched brands. Selection takes turns across brands in watchlist order until the response is full, prioritizing spam placement, biggest sends, then read-rate standouts within each brand. Within one signal, rows compare the matching spam rate, volume ratio, or read rate descending; missing values sort last and ties retain tracked-domain and source order. Selected rows are returned in watchlist order, then biggest-send, read-rate, and spam signal order, followed by tracked-domain and source order. One campaign may appear once per signal because each row carries different evidence. Empty when nothing qualified; check `panel_status` to distinguish that from an unavailable panel. ' items: $ref: '#/components/schemas/EmailCompetitiveNotableCampaign' truncated: type: boolean readOnly: true description: 'Whether Bird omitted eligible panel findings to keep this response to 100 rows. False does not promise that the panel observed every qualifying campaign in the period. ' example: false EmailCompetitiveCampaignSignal: type: string minLength: 1 description: 'Why this campaign was surfaced. The set is open and grows as new signals are added. Every signal describes the campaign against its own brand''s history, never against the other brands you watch, so several brands can carry the same signal in one period and none of them is the top of anything. `biggest_send` is a send far above that brand''s own median: unusual for the brand, not merely large. `read_rate_standout` is a campaign read unusually well for its brand. `landing_in_spam` is one heavily filed as spam at a single mailbox provider, named in `mailbox_provider`, which is worth seeing even when the brand''s overall placement looks healthy. ' x-extensible-enum: - biggest_send - read_rate_standout - landing_in_spam example: read_rate_standout CompetitiveWatchlistBrandID: type: string minLength: 1 pattern: ^cwb_[0-9a-hjkmnp-tv-z]{26}$ example: cwb_01krdgeqcxet5s7t44vh8rt9mg EmailCompetitiveFieldSource: type: string minLength: 1 description: 'Where a figure came from. `measured` means it is counted from your own sending. `panel` means it is an estimate from an email panel, which observes a sample of real inboxes and scales what it sees up to a whole audience. `none` means there is no figure for this field on this row, so there is nothing to attribute a source to. Only your own row carries `measured` figures, and only where the metric is counted rather than estimated. Everything about a competitor is a panel estimate. ' enum: - measured - panel - none example: panel EmailCompetitiveWatchlistBrand: description: 'A brand on the workspace''s watchlist. This is the watchlist entry itself, with no figures on it; read the watchlist to get those. ' unevaluatedProperties: false allOf: - $ref: '#/components/schemas/Timestamps' - type: object required: - id - brand_id - name - industry - sending_domains properties: id: $ref: '#/components/schemas/CompetitiveWatchlistBrandID' readOnly: true description: The watchlist entry. brand_id: readOnly: true allOf: - $ref: '#/components/schemas/EmailCompetitiveBrandID' name: type: string minLength: 1 readOnly: true description: 'The brand''s name when it was added. It is kept as it was so the row still reads correctly if the brand is later renamed or stops being tracked. ' example: Everlane industry: type: - string - 'null' readOnly: true description: The brand's industry when it was added, or null when the brand is not classified. example: DTC Apparel sending_domains: type: array minItems: 1 readOnly: true description: 'The domains this brand''s figures describe. Always one domain today, chosen as the one the panel sees the most of its mail from. ' items: type: string minLength: 1 example: - everlane.com EmailCompetitiveBrandSearchResults: type: object additionalProperties: false description: 'Brands matching the search. Ranked by how well they match, best first, and capped at 8 results because this backs a type-ahead. The panel''s own answer is often shorter than the cap, in which case the cap was never the reason the list is short. ' required: - data properties: data: type: array readOnly: true description: 'Matching brands. Empty when nothing matched, which for an unusual brand name means the panel does not track it rather than that the search failed. ' items: $ref: '#/components/schemas/EmailCompetitiveBrandMatch' EmailCompetitivePanelMailboxProvider: type: string minLength: 1 readOnly: true description: 'A mailbox provider, as the email panel identifies it. A lowercase identifier rather than a display name, so pick your own label for it, and treat the set as open: the panel reports whichever providers it observed, and `gmail`, `hotmail`, `yahoo`, `aol` and `comcast` are the ones it returns most. Apple never appears, because the panel does not measure it, so a surface offering an Apple row has no measurement behind it. The panel''s buckets are not the same as the ones the [mailbox-provider stats breakdown](/docs/api/reference/get-email-stats-by-mailbox-provider) reports: Microsoft''s properties appear here as `hotmail` rather than `microsoft`, and Apple is absent, so the two are not a joinable dimension. ' example: gmail EmailCompetitiveBrandSeries: type: object additionalProperties: false description: 'One line on the volume chart: a watched brand''s sending over time, or your own. ' required: - is_workspace - name - sending_domains - panel_status - source - points properties: watchlist_brand_id: $ref: '#/components/schemas/CompetitiveWatchlistBrandID' readOnly: true description: The watchlist entry this line describes. Absent on your own line, which is not a watchlist entry. is_workspace: type: boolean readOnly: true description: True on the line describing your own workspace's sending. example: false name: type: string minLength: 1 readOnly: true description: 'Label for the line: the brand''s name, or your sending domain on your own line.' example: Everlane sending_domains: type: array minItems: 1 readOnly: true description: The sending domains the line's figures describe. Always one domain today. items: type: string minLength: 1 example: - everlane.com panel_status: $ref: '#/components/schemas/EmailCompetitivePanelStatus' description: 'Why a line has no volume in it. Always `ok` on your own line, which is counted rather than read from the panel. ' source: $ref: '#/components/schemas/EmailCompetitiveFieldSource' description: 'Where the line came from. Your own is an exact count of what was accepted for delivery; a competitor''s is the panel''s estimate of everything they sent. The two share an axis while resting on different measurements, so a chart that compares them should say so. ' points: type: array readOnly: true description: 'One point per day of the period, oldest first, ending with the last whole UTC day rather than the one in progress. A domain the panel tracks but observed nothing for plots as zeros, which is a measured silence rather than a missing measurement. Points are empty only when there was nothing to plot at all, reported by `panel_status` as `not_in_panel` or `unavailable`. ' items: $ref: '#/components/schemas/EmailCompetitiveVolumePoint' EmailCompetitiveBrandProfile: type: object additionalProperties: false description: One watched brand's figures for the period, with its placement broken out by mailbox provider. required: - period - brand - providers properties: period: $ref: '#/components/schemas/EmailCompetitivePeriod' description: The period every figure covers. brand: $ref: '#/components/schemas/EmailCompetitiveWatchlistRow' description: 'The figures the watchlist reports for this brand, derived the same way. Estimated volume can differ very slightly between the two views, because each request asks the panel about a different set of domains and the panel scales its estimate per request. `esp` and `list_size` are populated here; the watchlist reports both as null. ' providers: type: array readOnly: true description: 'Placement per mailbox provider, in the order the panel returned them. Empty when the panel published no breakdown for the brand''s domains. ' items: $ref: '#/components/schemas/EmailCompetitiveProviderPlacement' EmailCompetitiveProviderPlacement: type: object additionalProperties: false description: How one mailbox provider treated a brand's mail, beside your own. required: - mailbox_provider - inbox_rate - spam_rate - workspace_inbox_rate properties: mailbox_provider: $ref: '#/components/schemas/EmailCompetitivePanelMailboxProvider' readOnly: true description: The provider whose treatment of the brand's mail this row describes. inbox_rate: type: number readOnly: true description: 'Share of the brand''s mail this provider put in the inbox. Recomputed from what the panel observed across every domain the brand sends from, so a small subdomain cannot move it as much as the brand''s main one. ' example: 0.862 spam_rate: type: number readOnly: true description: Share of the brand's mail this provider put in spam. example: 0.091 workspace_inbox_rate: type: - number - 'null' readOnly: true description: 'Your own inbox rate at this provider, null when you have not sent or the panel has no breakdown for your sending domain. It is the panel''s view of your sending rather than from our own measurement of it, because a measured rate and a rate the panel estimated are not comparable, and this figure exists to be compared with the brand''s. ' example: 0.921 EmailCompetitiveVolumeSeries: type: object additionalProperties: false description: 'Volume over time for the requested watched brands, plus your own sending, on one shared daily axis. ' required: - period - data properties: period: $ref: '#/components/schemas/EmailCompetitivePeriod' description: The period every line covers. data: type: array readOnly: true description: 'Your own line first, then the requested brands in the order they were asked for. Your line is present once your workspace has sent email. Every line carries the same days in the same order, so they can be plotted against one axis without aligning them first. ' items: $ref: '#/components/schemas/EmailCompetitiveBrandSeries' EmailCompetitiveVolumePoint: type: object additionalProperties: false description: One day of one line on the volume chart. required: - date - sends properties: date: type: string format: date minLength: 1 readOnly: true description: The UTC day this point covers. example: '2026-08-09' sends: type: integer format: int64 readOnly: true description: 'Volume for the day. An estimate for a competitor and an exact count for your own line; `source` on the series records which. A day nothing was observed is `0` rather than a missing point, so every line shares one axis. ' example: 41800 EmailCompetitiveBrandID: type: string minLength: 1 maxLength: 19 pattern: ^[0-9]+$ description: 'Identifier of the brand in the panel''s catalog, used to add it to the watchlist. It is a string for the same reason a campaign id is: the values are wide enough that a client storing every number as a floating point value would round them, and a rounded identifier matches no brand at all. ' example: '81531' _ListEnvelope: type: object required: - next_cursor - prev_cursor - refresh_cursor properties: next_cursor: type: - string - 'null' description: Cursor for the next page. Pass back as `starting_after` to advance forward. `null` when no next page exists. example: eyJ2IjoxLCJzIjoiXCIyMDI2LTA1LTI1VDE0OjAzOjEwWlwiIiwiaSI6IjAxOTJmM2IxLTRjN2UtN2EyYi05ZDYxLThmM2E1YzJlN2I0MCJ9 prev_cursor: type: - string - 'null' description: Cursor for the previous page. Pass back as `ending_before` to step backward. `null` when no previous page exists. example: null refresh_cursor: type: - string - 'null' description: Refresh anchor, the first row of this response. Pass back as `ending_before` to fetch what precedes it in the current sort order. On a newest-first sort those are the items that have appeared since; on any other sort they are the items that sort earlier, so refreshing such a list means re-fetching it instead. Non-`null` whenever `data` is non-empty; `null` only on an empty page. Distinct from `prev_cursor`. example: eyJ2IjoxLCJzIjoiXCIyMDI2LTA1LTI1VDE2OjQyOjAxWlwiIiwiaSI6IjAxOTJmM2IxLTllMDQtN2NkMy1iODE3LTJhNmY0ZDFjOGUwOSJ9 Timezone: type: string minLength: 1 description: IANA timezone identifier, such as `America/New_York`, `Europe/Amsterdam`, or `UTC`. example: America/New_York EmailCompetitiveWatchlistSummary: type: object additionalProperties: false description: 'Where your sending sits against the brands you watch, over the same period as the rows. Every figure here is derived from those rows rather than measured separately, so the two always agree. As on a row, each is present and `null` when the rows cannot support it: the peer medians need at least one watched brand the panel reported on, and the share figures need sending of your own to compare. ' required: - share_of_volume_percent - share_of_volume_change_points - competitor_sends - competitor_sends_change_percent - peer_cadence_median_per_week - peer_inbox_placement_median_rate properties: share_of_volume_percent: type: - number - 'null' readOnly: true description: 'Your share of everything the watched set sent over the period, your own sending included in the total. Your half of the ratio is an exact count of your own sending while the rest is the panel''s estimate, so the two sides are measured differently. ' example: 10.5 share_of_volume_change_points: type: - number - 'null' readOnly: true description: 'How that share moved against the period immediately before, in percentage points. A share that went from 11.7 to 10.5 reports -1.2. ' example: -1.2 competitor_sends: type: - integer - 'null' format: int64 readOnly: true description: 'Estimated volume the watched brands sent between them, excluding your own sending. A panel estimate, so read it as an order of magnitude rather than a count. ' example: 4240000 competitor_sends_change_percent: type: - number - 'null' readOnly: true description: Change in that volume against the period immediately before. example: 12 peer_cadence_median_per_week: type: - number - 'null' readOnly: true description: 'Median campaigns per week across the brands you watch, per sending domain. Your own row is excluded, since it is the figure being held against this one. ' example: 4.4 peer_inbox_placement_median_rate: type: - number - 'null' readOnly: true description: 'Median inbox placement across the brands you watch. Your own row is excluded, as with the cadence median. ' example: 0.892 Timestamps: type: object required: - created_at - updated_at properties: created_at: type: string format: date-time minLength: 1 readOnly: true example: '2026-05-20T09:14:52Z' updated_at: type: string format: date-time minLength: 1 readOnly: true example: '2026-05-25T16:42:01Z' ErrorDetail: type: object additionalProperties: false required: - param - message properties: param: type: string minLength: 1 description: 'Dotted field path, such as `to[0].email`, `subject`, or `.`. When the request was rejected for a query parameter the endpoint does not declare, this carries that parameter''s name instead of a field path. ' message: type: string minLength: 1 description: What is wrong with this field. EmailCompetitiveWatchlistBrandCreate: type: object additionalProperties: false description: 'The brand to add to the watchlist. Obtained from a brand search, which only returns brands that can be watched. ' required: - brand_id properties: brand_id: $ref: '#/components/schemas/EmailCompetitiveBrandID' NextAction: type: object additionalProperties: false required: - kind - description properties: kind: type: string minLength: 1 x-extensible-enum: - operation - external - wait - terminal description: "What you do about this step.\n\n- `operation`: call the operation named in `operation`, then\n read again.\n- `external`: act somewhere this API does not reach, then read\n again.\n- `wait`: nothing is asked of you, so read again later.\n- `terminal`: nothing you do resolves this, so stop retrying.\n\nTolerate a value you do not recognize: show the `description` and\noffer no action.\n" description: type: string minLength: 1 description: A short, human-readable label for the step, suitable for display. operation: type: string minLength: 1 description: 'The operationId to call. Present only when `kind` is `operation`. The operation''s own schema says how to call it; this says only which one, and what to address it with. ' params: type: object additionalProperties: type: string minLength: 1 description: 'The parameters that address the operation, by name: `{"sender_id": "…"}` for an operation on `/v1/sms/senders/{sender_id}/requirements`. A parameter the operation takes in its query string is given the same way, so an operation addressed as `?subject_id=` carries `{"subject_id": "…"}`. Every parameter the call needs is here, whether its value came from the thing you were acting on or is fixed for this step, so you can make the call from this object alone. Present only when `kind` is `operation` and the operation names a subject. A request body, when the operation takes one, is described by the operation''s own schema and never appears here. ' url: type: string format: uri description: 'A URL to open. Present only when `kind` is `external`, and only when the step has one. An external step whose `description` says to go and do something with no URL to open is normal. ' EmailCompetitiveSendTimePeak: type: object additionalProperties: false description: The hour of the day a brand sends most of its mail in. required: - start_hour - end_hour - share_percent properties: start_hour: type: integer readOnly: true minimum: 0 maximum: 23 description: The first hour of the window, in the timezone the response reports. example: 13 end_hour: type: integer readOnly: true minimum: 0 maximum: 23 description: 'The hour the window ends at, exclusive: a window of `13` to `14` covers 13:00 to 14:00. The window is always one hour wide on this endpoint, so this is always the hour after `start_hour`. The pair is kept rather than collapsed because the panel computes the window at whatever width it was asked for, and only this endpoint pins that to an hour. It can therefore be lower than `start_hour` in exactly one case: a peak at 23:00, whose window runs past midnight and ends at `0`. ' example: 14 share_percent: type: number readOnly: true description: 'Share of everything the brand sent over the period that fell in this window. This is the panel''s own figure, while a cell''s `share_percent` is recomputed from the cells in the response. Adding up this hour''s seven cells should therefore land on this number but is not guaranteed to; where they disagree, this one is the panel''s answer about its own peak and the cells are the arithmetic behind the grid. ' example: 13.1 EmailCompetitiveSendTimeGrid: type: object additionalProperties: false description: When a watched brand sends, by weekday and hour of the day. required: - period - timezone - panel_status - cells - peak_send_window properties: period: $ref: '#/components/schemas/EmailCompetitivePeriod' description: 'The period the grid covers. It is always the last 90 days, whatever range the rest of the brand''s figures are shown over: an hour of the week comes round about thirteen times in 90 days and once in a week, and a pattern drawn from one observation per cell is noise. Two things differ from the other competitive reads. It ends at the start of a day rather than at the moment of the request, and the panel answers repeat requests from a cache it holds for a day, so two requests a minute apart return identical figures and this grid can be up to a day behind the figures shown beside it. ' timezone: $ref: '#/components/schemas/Timezone' readOnly: true description: 'The timezone the hours are reported in. Label the grid from this rather than from what was requested: a response the panel could not answer reports UTC whatever was asked for. ' panel_status: $ref: '#/components/schemas/EmailCompetitivePanelStatus' description: Why the grid is empty, when it is. cells: type: array readOnly: true description: 'Every weekday and hour of the week, Monday first and hour ascending: 168 in all, whether or not the brand sent in them, so the grid needs no filling in. Empty when there was nothing to read, which `panel_status` explains. ' items: $ref: '#/components/schemas/EmailCompetitiveSendTimeCell' peak_send_window: oneOf: - $ref: '#/components/schemas/EmailCompetitiveSendTimePeak' - type: 'null' readOnly: true description: 'The hour of the day the brand sends most of its mail in, totalled across the whole week, or null when nothing was observed. It carries no weekday: for most brands the hour of the day is where the pattern is and the day of the week barely moves, so naming a busiest weekday would give a figure more meaning than it has. It is also not always the darkest cell, on the same reasoning: one busy Wednesday can outweigh the hour the brand mails in every single day. ' Error: type: object additionalProperties: false required: - error properties: error: $ref: '#/components/schemas/ErrorBody' EmailCompetitiveBrandMatch: type: object additionalProperties: false description: A brand matching a search, ready to be added to the watchlist. required: - brand_id - name - sending_domains properties: brand_id: readOnly: true allOf: - $ref: '#/components/schemas/EmailCompetitiveBrandID' name: type: string minLength: 1 readOnly: true description: The brand's name. example: Everlane sending_domains: type: array minItems: 1 readOnly: true description: 'The domains this brand''s figures would describe. Always one domain today, chosen as the one the panel sees the most of its mail from. ' items: type: string minLength: 1 example: - everlane.com EmailCompetitiveWatchlist: type: object additionalProperties: false description: 'The workspace''s competitor watchlist with its figures for the requested period. The list is capped by the organization''s competitor limit and is returned whole, so it is not paginated. Your own row is included and is always first. ' required: - period - summary - data properties: period: $ref: '#/components/schemas/EmailCompetitivePeriod' description: The period every figure covers. summary: $ref: '#/components/schemas/EmailCompetitiveWatchlistSummary' description: Where your sending sits against the brands you watch. data: type: array readOnly: true description: 'Your own row first, then each watched brand in the order it was added. Your row is present once your workspace has sent email, since before that there is no sending of yours to compare against. Empty for a workspace that has neither sent nor added a brand. ' items: $ref: '#/components/schemas/EmailCompetitiveWatchlistRow' SortOrder: type: string enum: - asc - desc description: Sort direction, ascending or descending. EmailCompetitiveWatchlistRow: type: object additionalProperties: false description: 'One brand on the watchlist, with its figures for the requested period. Your own workspace appears as a row too, so the table can be read as a single ranking. Every metric is present on every row and is `null` when it is unavailable for that brand, so a `0` is always a real measurement rather than a gap. Check `panel_status` for why a metric is null. `esp` and `list_size` are the exception. They are populated only when you read a single brand, and are always `null` on the watchlist whatever `panel_status` reports. ' required: - is_workspace - name - industry - sending_domains - esp - list_size - panel_status - sends - sends_change_percent - cadence_per_week - inbox_placement_rate - read_rate - audience_overlap_rate - last_campaign - provenance properties: watchlist_brand_id: $ref: '#/components/schemas/CompetitiveWatchlistBrandID' readOnly: true description: The watchlist entry, for removing the brand. Absent on your own row, which is not a watchlist entry. is_workspace: type: boolean readOnly: true description: True on the row describing your own workspace's sending. example: false name: type: string minLength: 1 readOnly: true description: The brand's name as it was when the brand was added to the watchlist. example: Everlane industry: type: - string - 'null' readOnly: true description: The brand's industry as it was when the brand was added, or null when the brand is not classified. example: DTC Apparel sending_domains: type: array minItems: 1 readOnly: true description: 'The domains the brand''s figures describe. Always one domain today: a brand is tracked by the single one the panel sees the most of its mail from, so a brand that splits its mail across several domains reports less than its full volume. ' items: type: string minLength: 1 example: - everlane.com esp: type: - string - 'null' readOnly: true description: 'A sending platform observed on the domain, or null when the panel has none on record. A brand sending through more than one platform reports one of them rather than the list. This is frequently unavailable and updates monthly at best, so treat its absence as normal rather than as pending. Populated only when you read a single brand; on the watchlist it is always null. ' example: Klaviyo list_size: type: - integer - 'null' format: int64 readOnly: true description: 'Estimated number of addresses the brand mails, or null when the panel has no estimate. Populated only when you read a single brand; on the watchlist it is always null. ' example: 1240000 panel_status: $ref: '#/components/schemas/EmailCompetitivePanelStatus' readOnly: true description: Whether panel figures were available for this row, and when they were not, why. sends: type: - integer - 'null' format: int64 readOnly: true description: Messages sent in the period. example: 1240000 sends_change_percent: type: - number - 'null' readOnly: true description: 'Change in send volume against the period immediately before this one, as a percentage. Null when the earlier period has nothing to compare against. ' example: 18 cadence_per_week: type: - number - 'null' readOnly: true description: Average campaigns sent per week over the period. example: 5.2 inbox_placement_rate: type: - number - 'null' readOnly: true description: 'Share of the brand''s observed mail that reached an inbox rather than a spam folder. ' example: 0.889 read_rate: type: - number - 'null' readOnly: true description: Share of delivered mail that was read. example: 0.192 audience_overlap_rate: type: - number - 'null' readOnly: true description: 'Share of your own audience the panel also sees receiving this brand''s mail. Null on your own row, and null for a competitor the panel measured no overlap with, which is an answer rather than a gap. ' example: 0.24 last_campaign: oneOf: - $ref: '#/components/schemas/EmailCompetitiveCampaignSummary' - type: 'null' unevaluatedProperties: false readOnly: true description: 'The most recent campaign observed in the period, or null when none was. Always null on your own row. ' provenance: $ref: '#/components/schemas/EmailCompetitiveWatchlistRowProvenance' readOnly: true description: Where each figure on this row came from. EmailCompetitiveCampaignSummary: type: object description: The most recent campaign observed for a brand in the period. required: - id - subject - sent_at - image_url properties: id: type: string minLength: 1 readOnly: true description: 'The identifier for this campaign. Use it to fetch this one campaign on its own. It is a string, and it needs to stay one. The values are long enough that JavaScript, and any other language that stores every number as a floating point value, will round them, and a rounded identifier matches no campaign at all. Compare it and pass it back as text. ' example: '3914827265' subject: type: string minLength: 1 readOnly: true description: The subject line the panel saw on this campaign. example: 'The Summer Sale: 40% off everything' sent_at: type: string format: date-time minLength: 1 readOnly: true description: When the panel first saw this campaign arrive. example: '2026-08-09T14:02:00Z' image_url: type: - string - 'null' format: uri readOnly: true description: 'Where the panel''s capture of the rendered email can be fetched, null when it captured none. Panels image only some of what they observe, so an absent creative is an ordinary outcome rather than a failed one. The image is served from the panel''s own host rather than from ours, so a page embedding it has to allow that host. ' example: https://images.example.com/creatives/c154c8c4-6356-40e6-92d2-7c6727ec36ca.jpg EmailCompetitiveCampaignSort: type: string minLength: 1 description: Field used to sort campaigns. enum: - sent_at default: sent_at example: sent_at EmailCompetitiveNotableEvidence: type: object additionalProperties: false description: 'The figures behind a campaign''s signal, for ordering or filtering the list yourself. Which field carries a value depends on the signal, and each is null both on the signals it does not describe and on a campaign of its own signal the panel published no figure for. ' required: - ratio_to_median - read_rate_observations - mailbox_provider_spam_rate - mailbox_provider_observations properties: ratio_to_median: type: - number - 'null' readOnly: true description: 'How many times the brand''s own median volume this send was. A value of 29 means the send was twenty-nine times the brand''s typical volume for the period. Null on every signal other than `biggest_send`, and on a `biggest_send` campaign the panel published no ratio for. ' example: 29.61 read_rate_observations: type: - integer - 'null' format: int64 readOnly: true description: 'How many panel observations `campaign.read_rate` was measured over. A rate over thirty observations and one over a hundred and forty are not equally worth showing, and this is what separates them. Null on every signal other than `read_rate_standout`, and on a `read_rate_standout` campaign the panel published no denominator for. ' example: 66 mailbox_provider_spam_rate: type: - number - 'null' readOnly: true description: 'Share of this campaign filed as spam at the one provider named in `mailbox_provider`, as a value between 0 and 1. A different measurement from the campaign''s overall `spam_rate`, and the one this signal is about. Null on every signal other than `landing_in_spam`, and on a `landing_in_spam` campaign whose provider counts the panel did not publish, so a spam entry can arrive without the rate behind it. ' example: 0.79 mailbox_provider_observations: type: - integer - 'null' format: int64 readOnly: true description: 'How many observations at that provider `mailbox_provider_spam_rate` was measured over. Null on the same terms. ' example: 199 EmailCompetitivePanelStatus: type: string minLength: 1 description: 'Whether panel figures are available for a row, and when they are not, why. `ok` means the panel reported figures for the requested period. `not_in_panel` means the panel does not track the sending domain at all, which is common for smaller and newer senders. `no_data` means the panel tracks the domain but observed no mail from it in the period. `unavailable` means the figures could not be retrieved this time and the same request may well succeed on a retry. ' enum: - ok - not_in_panel - no_data - unavailable example: ok EmailCompetitiveCampaignFeed: description: A page of campaigns returned for a watched brand over the period. allOf: - type: object required: - period - panel_status - captured - promo_rate - truncated - data properties: period: $ref: '#/components/schemas/EmailCompetitivePeriod' description: The rolling period used for this request. panel_status: $ref: '#/components/schemas/EmailCompetitivePanelStatus' description: For this campaign feed, no_data means the requested page is empty; it does not mean the whole period has no campaigns. captured: type: integer readOnly: true description: 'Number of eligible campaigns in the first 300 newest panel rows for each tracked domain. This sampled value is independent of the returned page. ' example: 38 promo_rate: type: - number - 'null' readOnly: true description: 'Fraction of captured campaigns whose subject leads with a discount. Null when captured is zero. This sampled value is independent of the returned page. ' example: 0.64 truncated: type: boolean readOnly: true description: 'Whether the sampled statistics or returned page omit part of the requested collection. Use next_cursor to determine whether another page is available. ' example: false data: type: array readOnly: true description: Campaigns in this page, in the requested order. items: $ref: '#/components/schemas/EmailCompetitiveCampaign' - $ref: '#/components/schemas/_ListEnvelope' unevaluatedProperties: false EmailCompetitiveNotableClaim: type: object additionalProperties: false description: 'What the panel itself asserts about a campaign, in its own words. Present only on the campaigns the panel chose to make a claim about, which is a minority of them: a campaign can be surfaced as notable without the panel putting a headline on it, and that is an ordinary outcome rather than missing data. ' required: - text properties: text: type: string minLength: 1 readOnly: true description: 'The panel''s own phrasing, which may name the window the claim was measured over ("Biggest send in 7 days") or not ("Best-read campaign"). Show it as written rather than rebuilding it from the signal, and do not parse a window out of it. ' example: Biggest send in 7 days parameters: PaginationLimit: name: limit in: query required: false description: Maximum number of items to return per page. schema: type: integer minimum: 1 maximum: 100 default: 25 IdempotencyKey: name: Idempotency-Key in: header required: false description: "Client-supplied key. On operations supporting request deduplication, a retained\nresponse is replayed for duplicate requests with the same key within the\nidempotency window (3 hours by default). This protection requires a workspace,\norganization, or staff-account scope. User-only and unauthenticated operations,\nstreams, and operations with a separate replay contract do not use this\nresponse replay.\n\nOn a supported operation, if idempotency protection is unavailable before execution, the API returns\n`503 IdempotencyUnavailable` (E01033) without executing this attempt. Retry with\nbackoff using the same key and request. An operation that takes effect before\nits response is retained can still execute again on retry.\n\nTwo distinct 409 errors signal misuse:\n\n- `request_in_progress` (E01004): The same key is currently being\n processed by a concurrent request. Wait briefly and retry. The lock expires within 30 seconds.\n- `idempotency_key_reuse` (E01005): The same key has already completed\n against a different request body or method. Generate a new key.\n\nRecommended key format is `/` (for example `welcome-user/usr_abc123`).\n" schema: type: string maxLength: 255 EmailCompetitiveRange: name: range in: query required: false description: 'How many days back the response covers, counting from now. One of three fixed trend windows rather than an open date range, matching how a competitive-intelligence chart is read. Defaults to 30. ' schema: type: integer enum: - 7 - 30 - 90 default: 30 example: 30 StartingAfter: name: starting_after in: query required: false description: Cursor from the `next_cursor` field of a previous list response. Returns items immediately after the cursor position in the current sort order. schema: type: string EmailCompetitiveTimezone: name: timezone in: query required: false description: 'IANA timezone identifier to report send times in; defaults to UTC. The grid is folded into this zone before it is summed, so a send lands on the weekday and hour it happened at locally rather than the one it happened at in UTC. A zone this API does not know returns 422 rather than falling back to UTC, so an axis is never labelled with a zone the figures were not folded into. ' schema: $ref: '#/components/schemas/Timezone' EndingBefore: name: ending_before in: query required: false description: Cursor from the `prev_cursor` or `refresh_cursor` field of a previous list response. Returns items immediately before the cursor position in the current sort order. `prev_cursor` returns the preceding page. `refresh_cursor` anchors at the first row of that response, which on a newest-first sort is how to fetch the items that have appeared since. schema: type: string OrderDesc: name: order in: query required: false description: 'Sort direction. Defaults to `desc`, which sorts from newest to oldest or largest to smallest, depending on the selected sort field. ' schema: $ref: '#/components/schemas/SortOrder' default: desc responses: InternalError: description: Internal server error content: application/json: schema: $ref: '#/components/schemas/Error' RateLimited: description: Rate limit exceeded headers: RateLimit: $ref: '#/components/headers/RateLimit' RateLimit-Policy: $ref: '#/components/headers/RateLimit-Policy' Retry-After: $ref: '#/components/headers/RetryAfter' content: application/json: schema: $ref: '#/components/schemas/Error' Forbidden: description: Insufficient permissions content: application/json: schema: $ref: '#/components/schemas/Error' ServiceUnavailable: description: 'The service is temporarily unavailable. If `Retry-After` is present, wait for that delay before retrying; otherwise, retry with exponential backoff. Reuse the same idempotency key and request when retrying a mutation. ' headers: Retry-After: $ref: '#/components/headers/RetryAfter' content: application/json: schema: $ref: '#/components/schemas/Error' Unprocessable: description: 'The request has invalid field values, violates a business rule, or carries a query parameter the endpoint does not declare. Field validation errors use `type: validation_error` and include the affected fields in `details`. Business-rule errors identify the failed rule in `type`. ' content: application/json: schema: $ref: '#/components/schemas/Error' NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/Error' BadRequest: description: Bad request content: application/json: schema: $ref: '#/components/schemas/Error' Unauthorized: description: Authentication required content: application/json: schema: $ref: '#/components/schemas/Error' Conflict: description: Resource conflict content: application/json: schema: $ref: '#/components/schemas/Error' headers: RetryAfter: description: 'Number of seconds to wait before retrying the request. ' schema: type: integer minimum: 0 example: 35 IdempotencyReplay: description: The API includes this header when it replays the response for an earlier request that used the same `Idempotency-Key`. The API does not process the request again. schema: type: string enum: - 'true' RateLimit: description: 'Remaining capacity for the request''s rate-limit policy as an IETF Structured Field. Format: `"";r=;t=`. ' schema: type: string example: '"email_send";r=842;t=35' RateLimit-Policy: description: 'Effective quota for the request''s rate-limit policy as an IETF Structured Field. Format: `"";q=;w=`. ' schema: type: string example: '"email_send";q=1000;w=60' securitySchemes: BearerAuth: type: http scheme: bearer description: 'Pass the API key as a bearer token in the `Authorization` header. Keys use the format `bk_{region}_*`. The prefix identifies the region and selects the API endpoint. Official Bird SDKs and the CLI derive the region from the key. ' CookieAuth: type: apiKey in: cookie name: bird_session description: 'Session cookie set after signing in to the Bird dashboard. The cookie value is an opaque session token; no session data is stored in the cookie itself. ' RealtimeKey: type: apiKey in: header name: X-Realtime-Key description: 'The Realtime app key. Together with `X-Realtime-Secret`, it authenticates a request to the Realtime API in addition to the workspace credential. Both values come from the app''s credentials and must belong to the calling workspace. Official Bird SDKs accept the pair as client configuration. ' RealtimeSecret: type: apiKey in: header name: X-Realtime-Secret description: 'The Realtime app secret paired with `X-Realtime-Key`. The API returns the secret only when the key is created and does not store it. Create a new key and revoke the current key if you lose the secret. Official Bird SDKs accept the pair as client configuration. '