openapi: 3.1.0 info: title: Hyros API version: "1.44" description: | ## Status Codes | Code | Meaning | |------|---------| | `200 OK` | Response to a successful GET, PUT, POST or DELETE | | `400 Bad Request` | Malformed request; form validation errors | | `401 Unauthorized` | Wrong Api-Key or not provided, the key was revoked, or the account it belongs to is not active — blocked, payment pending or pending deletion. An inactive account is refused on every endpoint until its state is restored | | `403 Forbidden` | Api-Key is not authorized for a role the endpoint requires (`Forbidden, missing api key roles`, followed by the missing roles), or the Accessible-Account-Id header targets an account that is not one of the caller's connected client accounts, or one whose state does not grant access | | `429 Too Many Requests` | The per-second or per-minute request limit for that endpoint was exceeded | Roles are checked on every request, not only when the key is created, so a key stops being authorized for a role as soon as the account loses the permission that grants it, even though the key still carries that role. ## Unrecognized parameters and fields On the endpoints listed below, requests must contain only documented query parameters and body fields. An unknown query parameter, an unknown top-level body field, or a query parameter repeated more than once, is rejected with `400 Bad Request` naming the offender instead of being ignored. The error body lists the offenders as `Unknown parameter: `, `Duplicate parameter: ` or `Unknown field: `. For example, `GET /api/v1.0/products?bogus=1` returns `400` with `Unknown parameter: bogus`. Endpoints that enforce this: - `GET /api/v1.0/products`, `PUT /api/v1.0/products/{id}`, `DELETE /api/v1.0/products/{id}` - `GET /api/v1.0/carts` - `GET /api/v1.0/custom-costs`, `PUT /api/v1.0/custom-costs/{id}`, `DELETE /api/v1.0/custom-costs/{id}` - `PUT /api/v1.0/sources/{tag}`, `DELETE /api/v1.0/sources/{tag}` - `GET /api/v1.0/url-rules`, `GET /api/v1.0/url-rules/{id}`, `POST /api/v1.0/url-rules`, `PUT /api/v1.0/url-rules/{id}`, `DELETE /api/v1.0/url-rules/{id}` - `GET /api/v1.0/leads/aggregation` - `GET /api/v1.0/tags/count` - `GET /api/v1.0/attribution/roas`, `GET /api/v1.0/attribution/marginal-cac-curve` - `GET /api/v1.0/conversion-paths` - `POST /api/v1.0/reports/generate`, `GET /api/v1.0/reports/poll/{externalId}` - `GET /api/v1.0/requests/{request_id}` On every other endpoint, unknown query parameters and unknown body fields are ignored, and a repeated query parameter takes its first value. The request succeeds and the unrecognized input has no effect, so a typo silently changes what you get back: `GET /api/v1.0/leads?email=someone@example.com` is not a filter on `emails`, it returns the unfiltered lead list. Keep your own input validation in place for those endpoints. ## Values a field accepts A parameter or body field that takes one of a fixed set of values rejects anything else with `400 Bad Request`, naming the field, echoing the value it refused and listing what it accepts: ``` { "result": "ERROR", "message": ["Invalid frequency 'WEEKLY'. Accepted values: DAILY, ONE_TIME"] } ``` The accepted list is what **that field on that operation** takes, which is not always every value the field can hold elsewhere. `POST /api/v1.0/custom-costs` accepts only `DAILY` and `ONE_TIME` even though a cost created in the Hyros UI can be `MONTHLY`, and neither `POST` nor `PUT /api/v1.0/subscriptions` accepts the `UNKNOWN` status that `GET /api/v1.0/subscriptions` can both return and filter on. A value outside the list is refused. It is never read as absent, so a write never reports success having quietly ignored the field. ## Date Formats Dates are represented as [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) strings (e.g. `2026-08-05T05:45:35Z`) in both requests and responses. ### Always send an explicit time zone offset A date sent without a time zone offset is not interpreted the same way when you write it as when you filter on it: - **Writing** (`POST` and `PUT` bodies): a date without an offset is stored as **UTC**. A date with no time at all becomes `00:00:00` UTC. - **Filtering** (the date parameters of the retrieval endpoints): a date without an offset is read in **your account's configured time zone**. A date with no time becomes `00:00:00` on a `from` bound and `23:59:59` on a `to` bound. A record written without an offset can therefore be missing from the filter for its own day. For an account on `-05:00`, a lead stage sent as `2026-08-27T00:00:00` is stored at `2026-08-27T00:00:00Z`, which is `2026-08-26T19:00` in the account time zone, so `stageFromDate=2026-08-27` does not return it while `stageToDate=2026-08-26` does. Sending the offset removes the ambiguity on both sides: `2026-08-27T00:00:00-05:00` is stored and filtered as the same instant. To see how a date you sent was interpreted, poll `GET /api/v1.0/requests/{request_id}` once the write is `PROCESSED`: its `result` returns the resource in the same shape as its retrieval endpoint, so its dates come back in your account's time zone with an explicit offset, except for the fields that endpoint documents as legacy or offset-less. ### Accepted forms The form accepted by every date field is `yyyy-MM-ddTHH:mm:ss` followed by `Z` or `+HH:mm` / `-HH:mm`, for example `2026-08-27T14:00:00-05:00`. Use it and you never have to think about the rest of this section. Shorter forms are also accepted: a bare date `yyyy-MM-dd`, and a date-time without seconds `yyyy-MM-ddTHH:mm`. `tagsDate` and `leadStage.date` are stricter than the rest: when an offset is present they require seconds precision and nothing more. Both `2026-08-27T14:00:00.123Z` (fractional seconds) and `2026-08-27T14:00-05:00` (no seconds) answer `Invalid date format.` on those two fields. The first of those is what most client libraries produce by default, so send plain seconds-precision dates to those two fields. Fractional seconds are also rejected on the `startDate` and `endDate` of the attribution reports, with `Invalid start date` / `Invalid end date`. Everywhere else, three-digit fractional seconds are accepted and dropped. Offsets must be written with a colon: `-05:00`, not `-0500`. ### Exceptions - `startDate` and `endDate` on `/api/v1.0/custom-costs` keep the wall clock: the offset you send is discarded rather than converted, so `2026-08-27T10:00:00-05:00` and `2026-08-27T10:00:00Z` are both stored as `10:00`. The response returns these two fields the same way, as a local date-time with no offset. The `fromDate`/`toDate` filters of `GET /custom-costs` are not exempt: they are read in your account's time zone, so on an account west of UTC a cost can fall outside the window covering its own day. Widen the window by a day when filtering costs. - On the attribution reports, `startDate` and `endDate` must either both carry an offset or both omit it. Sending an offset on only one of the two bounds is rejected with `There was a problem processing the date in the request.` Some legacy fields use different date encodings for backward compatibility. These exceptions are documented in the corresponding field descriptions. ## Date ranges in a future month Retrieval data is stored in monthly partitions, and the partition for a future month does not exist yet. A request whose date range reaches into such a month is rejected with `400 Bad Request` naming the bound that does, instead of failing while the data is read. The bound is read as the calendar month you wrote, so a range that ends today is always accepted — including an end of day written in a timezone ahead of ours — and so is one that ends on the last day of the current month. The error body carries `'toDate' cannot be in a future month: 2026-11-01`. The rule applies to `fromDate` and `toDate` on: - `GET /api/v1.0/leads`, `GET /api/v1.0/leads/clicks`, `GET /api/v1.0/leads/aggregation` - `GET /api/v1.0/sales`, `GET /api/v1.0/calls`, `GET /api/v1.0/carts`, `GET /api/v1.0/subscriptions` - `GET /api/v1.0/products`, `GET /api/v1.0/sources`, `GET /api/v1.0/ads`, `GET /api/v1.0/ad-accounts` - `GET /api/v1.0/tags/count`, `GET /api/v1.0/stages`, `GET /api/v1.0/keywords` - `GET /api/v1.0/custom-costs`, `GET /api/v1.0/url-rules`, `GET /api/v1.0/conversion-paths` and to `startDate` and `endDate` of the `configuration` sent to `POST /api/v1.0/reports/generate`. The attribution reports — `GET /api/v1.0/attribution`, `/attribution/roas`, `/attribution/ad-account` and `/attribution/marginal-cac-curve` — are stricter: no bound of their window may be in the future at all, read in your account's timezone. They answer `startDate or endDate cannot be in the future.` instead. ## Inverted date ranges A range whose start is later than its end matches nothing. The empty page that came back could not be told apart from a period that genuinely holds no data, so such a range is rejected with `400 Bad Request` naming the pair of parameters that is inverted. The error body carries `fromDate cannot be later than toDate`. The bounds are compared as the instants they resolve to, not as whole days, so a range inverted inside a single day is refused too. A bound written without a time is read as start of day when it is the lower one and end of day when it is the upper one, which is why `fromDate=2026-04-16T10:00&toDate=2026-04-16` is a valid range running to the end of that day rather than an inverted one. The rule applies to `fromDate` and `toDate` on every endpoint that accepts them — the same list as the future-month rule above. A few of those endpoints accept the pair without filtering on it; the range is still refused there rather than accepted and ignored, exactly as the future-month rule already does. The other date ranges answer under their own parameter names, on the endpoints that read them: - `updatedFromDate cannot be later than updatedToDate` — `GET /api/v1.0/leads`, `/leads/aggregation`, `/sales`, `/calls`, `/subscriptions` - `tagFromDate cannot be later than tagToDate` — `GET /api/v1.0/leads`, `/leads/aggregation` - `stageFromDate cannot be later than stageToDate` — `GET /api/v1.0/stages` - `'endDate' cannot be before 'startDate'` — the `configuration` sent to `POST /api/v1.0/reports/generate` ## Rate Limiting Requests are rate limited **per Hyros account, not per API key**: every key of the same account draws from the same budget. The limit always applies to the key owner, including when `Accessible-Account-Id` targets a client account. Each endpoint and HTTP method has **its own budget**, so `GET /leads` and `POST /orders` are counted separately and a client can run at the full rate against several endpoints at the same time. The defaults are **30 requests per second** and **1000 requests per minute**, per endpoint. They can be raised or lowered per account, so treat the response headers rather than these numbers as the source of truth. | Header | Description | Example | |--------|-------------|---------| | `X-RateLimit-Limit` | The policy in effect, as `capacity;w=window_seconds` for each window | `30;w=1, 1000;w=60` | | `X-RateLimit-Remaining` | Requests left in the most restrictive window | `25` | | `X-RateLimit-Reset` | Unix timestamp (seconds) at which the tokens for the rejected request would be available | `1707235200` | Those three accompany authenticated responses, so a `401` carries none of them, and they can also be absent when the limit could not be evaluated — treat a missing header as no information rather than an error. A rejected request answers `429 Too Many Requests`, adds `Retry-After` with the seconds to wait, and returns the standard error envelope with the message `You have reached the limit of api requests, please wait before sending again.` ## Asynchronous Processing Write operations (creates, updates and deletes) are processed **asynchronously**. A `200` response means the request was received and validated; the change is applied afterwards and may not be visible on an immediate read-back. Creates are typically visible within ~10 seconds. Updates and deletes typically take ~5 minutes, and can take longer under heavy load. Read (`GET`) operations are synchronous and return current data. Asynchronous operations are flagged with an **Asynchronous** note. Every write returns a `request_id`. To check whether it has been applied, poll it on the [Retrieve Request Status](#tag/request-status) endpoint, which reports whether the write is `PENDING`, `PROCESSED` or `FAILED` and, once `PROCESSED`, returns the resource it produced. Even after a write is reported as `PROCESSED`, its changes can still take a few seconds to appear on the corresponding `GET` endpoints, so a read-back immediately after a success may briefly not reflect them. A write carrying a blacklisted email address is the exception: it is refused synchronously with a `400` and no `request_id` is issued, because the event would be discarded and there would be nothing to report on. See *Delete Lead*. ## Agency Access to Client Accounts Agencies can act on a connected client account, identified by its external id, on both API surfaces. The rule is the same everywhere: the caller must have an approved connected-account relation with the target account, the request then runs against that account instead of the caller's own, and any other target is rejected with an authorization error (`Not authorized: account is not one of your connected client accounts.`). A connected account whose own state does not grant access is rejected the same way, naming that reason instead (`Not authorized: account is not active.`). When no account id is sent, the request operates on the caller's own account. Rate limits always apply to the caller. * **REST API**: send the optional `Accessible-Account-Id` header on any `/api/v1.0` request. Unauthorized targets are rejected with `403 Forbidden` and the standard error body (`result: ERROR`). * **MCP tools**: pass the optional `accessible_account_id` tool argument. It is advertised in `tools/list` only to agency accounts but validated on every call; unauthorized targets produce a tool error. Tools that are not tied to an account's data (docs search, integration types) and admin tools (which take a `productId` argument instead) do not accept it. security: - ApiKeyAuth: [] tags: - name: Leads - name: Sales - name: Orders - name: Calls - name: Conversion Paths - name: Attribution - name: Ad Accounts - name: Products - name: Tags - name: Sources - name: URL Rules - name: Custom Costs - name: Clicks - name: Carts - name: User Info - name: Keywords - name: Subscriptions - name: Tracking Script - name: Domains - name: Stages - name: Webhook Subscriptions - name: Reports - name: Request Status components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: API-Key schemas: SuccessResponse: type: object properties: request_id: type: string result: type: string example: OK ErrorResponse: type: object properties: result: type: string example: ERROR message: type: array items: type: string RequestStatus: type: object properties: requestId: type: string description: The `request_id` of the polled write request. example: 251b9b0e20a441fba3d3c346579e7105 status: type: string enum: [PENDING, PROCESSED, FAILED] description: Processing status of the asynchronous write. eventType: type: string description: Type of the original operation. example: CREATE_LEAD errorMessage: type: string description: Reason for the failure. Present only when `status` is `FAILED`. createdAt: type: string description: Date the request was received (ISO-8601, UTC). example: '2026-07-21T21:17:03Z' processedAt: type: string description: Date the request finished processing (ISO-8601, UTC). Absent while `status` is `PENDING`. example: '2026-07-21T21:20:41Z' result: oneOf: - $ref: '#/components/schemas/LeadWithStage' - $ref: '#/components/schemas/Cart' - $ref: '#/components/schemas/Source' - $ref: '#/components/schemas/CustomCost' - $ref: '#/components/schemas/Product' - $ref: '#/components/schemas/Call' - $ref: '#/components/schemas/Subscription' - $ref: '#/components/schemas/Click' - type: array title: Sale[] items: $ref: '#/components/schemas/Sale' - type: array title: Call[] items: $ref: '#/components/schemas/Call' - type: array title: Subscription[] items: $ref: '#/components/schemas/Subscription' description: >- The resource produced by the write, embedded once the request is `PROCESSED`, using the same shape its own endpoint returns. It is a single object for lead create/update requests (`eventType` `CREATE_LEAD` or `UPDATE_LEAD`, shaped like an item of `GET /api/v1.0/leads`), cart create/update requests (`CREATE_CART` or `UPDATE_CART`, like `GET /api/v1.0/carts`), source create requests (`CREATE_SOURCE_LINK`, like `GET /api/v1.0/sources`), custom cost create requests (`CREATE_CUSTOM_COST`, like `GET /api/v1.0/custom-costs`), product create requests (`CREATE_PRODUCT`, like `GET /api/v1.0/products`), click create requests (`CREATE_CLICK`, like an item of `GET /api/v1.0/leads/clicks`, with `leadId` and `email` empty until the click is connected to a lead) and call/subscription create requests (`CREATE_CALL` and `CREATE_SUBSCRIPTION`, like `GET /api/v1.0/calls` and `GET /api/v1.0/subscriptions`). It is an array for order requests (`CREATE_ORDER`, `UPDATE_ORDER`, `REFUND_ORDER` or `UPDATE_SALE`) — the order's sales as `GET /api/v1.0/sales` returns them, since an order is stored as one sale per item — and for call and subscription update requests (`UPDATE_CALL`, `UPDATE_SUBSCRIPTION`), which can target several ids at once. Omitted while `PENDING` or `FAILED`, and for delete requests other than an order refund, whose sales survive it and carry a `refundDate` and the refunded amount in `price.refunded`. It is a snapshot taken when the write completed, so enrichment that depends on other resources may be incomplete: a cart's `lead` may be absent when the same request created it. AggregationResult: type: object properties: groups: type: array description: One entry per value of the grouped attribute, largest first. Only as many as `groupsLimit` are returned. items: type: object properties: key: type: string description: The value of the attribute. Always a string, including when the attribute is numeric. example: Opt In count: type: integer description: Records in this group. example: 12 remainingCount: type: integer description: Records left out because their group ranked below the ones returned. Raising `groupsLimit` by this number always brings every remaining group. totalCount: type: integer description: How many records matched the filters, whether or not they hold a value for the attribute. The groups do not add up to it. example: 30 WebhookSubscription: type: object properties: externalId: type: string description: Opaque identifier of the subscription. example: sub-2a475f6baf8f416bac9ff60e1a0fabb5 name: type: string example: My CRM sync targetUrl: type: string description: Public `http`/`https` URL where the event payloads are sent via POST. example: https://example.com/hooks/hyros eventTypes: type: array items: type: string enum: - sale.attributed - sale.refunded - call.attributed - lead.opted.in - lead.opted.in.first.time - lead.origin.assigned - lead.stage.changed - lead.tag.added - lead.tag.removed - subscription.created - subscription.status.changed example: ['sale.attributed', 'lead.opted.in'] state: type: string enum: [ACTIVE, DELETED, AUTOMATIC_PAUSED, MANUALLY_PAUSED] example: ACTIVE secretKey: type: string description: Secret used to validate the HMAC signature of delivered events. Returned once, in the create (`POST`) response; it is not included in the list (`GET`) response. It can also be found in the Hyros app under Settings → Integrations → Hyros Webhook Subscription. example: ssk-244e8d359f67456cb9efac27913283fb creationDate: type: string description: Creation date, in UTC. example: '2026-07-09T14:28:15Z' lastDeliveryDate: type: string nullable: true description: Date of the last event delivery, in UTC. `null` if no event has been delivered yet. example: '2026-07-09T15:13:20Z' AdspendType: type: string enum: [FACEBOOK, GOOGLE, GOOGLE_V2, TIKTOK, SNAPCHAT, LINKEDIN, TWITTER, PINTEREST, BING, REDDIT, APPLOVIN, WHOP_ADS] AdspendSubType: type: string enum: [DISPLAY, VIDEO] SubscriptionStatus: type: string description: >- UNKNOWN is the status Hyros stores when the billing platform reports one it does not model. It is returned and it can be filtered on, but a write does not accept it. enum: [ACTIVE, TRIALING, CANCELED, PAST_DUE, INCOMPLETE, INCOMPLETE_EXPIRED, UNPAID, COMPLETED, PAUSED, UNKNOWN] SubscriptionStatusWrite: type: string description: The statuses a write accepts, which is every status except the UNKNOWN sentinel Hyros stores for a status it does not model. enum: [ACTIVE, TRIALING, CANCELED, PAST_DUE, INCOMPLETE, INCOMPLETE_EXPIRED, UNPAID, COMPLETED, PAUSED] CallState: type: string description: >- On a write, a value outside this list is rejected with `Invalid state ''. Accepted values: QUALIFIED, UNQUALIFIED, CANCELLED, NO_SHOW`. Omitting the field on create leaves the state unset rather than defaulting it. enum: [QUALIFIED, UNQUALIFIED, CANCELLED, NO_SHOW] AdOptimizationConsent: type: string enum: [GRANTED, DENIED, UNSPECIFIED] Item: type: object required: [name, price] properties: name: type: string description: Name of the product. price: type: number description: Product price per unit (costOfGoods included). externalId: type: string description: Unique identifier from the external integration. quantity: type: number description: Number of copies purchased. Defaults to 1. default: 1 sku: type: string description: Unique product reference code. costOfGoods: type: number description: Cost per unit of manufacture. Must be included in the price. Defaults to 0. default: 0 taxes: type: number description: Taxes applied to the item per unit. Defaults to 0. default: 0 itemDiscount: type: number description: Discount applied to this specific line item. packages: type: array items: type: string description: Product packages this item belongs to (used for recurring sales attribution). isRebill: type: boolean description: If true, the sale is marked as recurring even if it's the first one. tag: type: string description: Tag to create for the sale item. categoryName: type: string description: Links the sale to a product category. CartItem: type: object required: [name, price] properties: name: type: string price: type: number externalId: type: string quantity: type: number default: 1 sku: type: string description: Unique product reference code. isRebill: type: boolean description: If true, the sale is marked as recurring even if it's the first one. Provider: type: object properties: id: type: string description: ID of lead in external platform. integration: type: object properties: name: type: string type: type: string id: type: string description: Account ID of the integration. AdSource: type: object properties: adSourceId: type: string adAccountId: type: string description: > Id of the ad account the source belongs to, matching the ids returned by `GET /ad-accounts`. Absent when the source carries no ad platform: sources created by hand belong to no ad account, so there is nothing to report. platform: $ref: '#/components/schemas/AdspendType' TrafficSource: type: object properties: id: type: string name: type: string Goal: type: object properties: id: type: string name: type: string Category: type: object properties: id: type: string name: type: string SourceLinkAd: type: object properties: name: type: string adSourceId: type: string AdHierarchyLevel: type: object description: | One level of the ad hierarchy a source click belongs to, named as Hyros knows it and identified by the id its ad platform issues. Those ids are the ones the reporting endpoints resolve from, so they can be fed straight back in: | Journey field | Endpoint | Value to send | |---------------|----------|---------------| | `ad` | `GET /attribution/roas`, `GET /attribution/marginal-cac-curve` | `id` with `level=ad` | | `adSet` | idem | `id` with `level=source_link` | | `campaign` | idem | `id` with `level=campaign` | | `adAccount` | idem | `id` with `level=account` | | `ad`, `adSet`, `campaign` | `GET /attribution` | `ids`, with the platform-specific `level` (e.g. `facebook_adset`) | | `adAccount` | `GET /ad-accounts`, `GET /attribution/ad-account` | `ids` | | `ad` | `GET /ads` | `adSourceIds` | | `adSet` | `GET /keywords` | `adgroupId` | properties: name: type: string adSourceId: type: string description: The id the ad platform issues for this entity. Attribution: type: object properties: sourceLinkId: type: string name: type: string tag: type: string disregarded: type: boolean organic: type: boolean clickDate: type: string clickId: type: string adSource: $ref: '#/components/schemas/AdSource' sourceLinkAd: $ref: '#/components/schemas/SourceLinkAd' trafficSource: $ref: '#/components/schemas/TrafficSource' goal: $ref: '#/components/schemas/Goal' category: $ref: '#/components/schemas/Category' gclId: type: string description: Only for Google. gbraId: type: string description: Only for Google. wbraId: type: string description: Only for Google. Price: type: object properties: currency: type: string price: type: number discount: type: number hardCost: type: number refunded: type: number Lead: type: object properties: email: type: string id: type: string creationDate: type: string description: ISO 8601 date. When the lead is embedded in `/sales`, `/calls`, or `/subscriptions`, it is returned in the legacy `EEE MMM dd HH:mm:ss zzz yyyy` format for backward compatibility. example: '2023-01-04T04:36:41-05:00' lastUpdatedDate: type: string example: '2023-06-10T09:12:00-05:00' tags: type: array items: type: string ips: type: array items: type: string phoneNumbers: type: array items: type: string firstName: type: string lastName: type: string provider: $ref: '#/components/schemas/Provider' firstSource: $ref: '#/components/schemas/Attribution' description: First attributed source of the lead. Omitted when unknown. lastSource: $ref: '#/components/schemas/Attribution' description: Last attributed source of the lead. Omitted when unknown. originLead: $ref: '#/components/schemas/Lead' description: The origin lead this lead was merged into, when applicable. Omitted otherwise. isOriginLead: type: boolean description: Present on the leads journey (`GET /leads/journey`) and on the nested `originLead`; indicates the lead is an origin lead. LeadStage: type: object description: Current (most recent) stage of the lead. properties: name: type: string description: Name of the stage. date: type: string description: Date the stage was applied (ISO-8601 with timezone offset). Omitted when unavailable. LeadWithConsent: allOf: - $ref: '#/components/schemas/Lead' - type: object properties: adOptimizationConsent: $ref: '#/components/schemas/AdOptimizationConsent' LeadWithStage: allOf: - $ref: '#/components/schemas/LeadWithConsent' - type: object properties: currentStage: $ref: '#/components/schemas/LeadStage' description: Present only when the lead has a stage applied. SaleProduct: type: object description: Product information as it appears attached to a sale or call. properties: id: type: string name: type: string tag: type: string sku: type: string category: $ref: '#/components/schemas/Category' provider: $ref: '#/components/schemas/Provider' Product: type: object description: A product returned by the products listing endpoint. properties: id: type: string description: Product id (its tracking pixel). name: type: string tag: type: string sku: type: string price: type: number customCost: type: number description: Cost of goods of the product, used in profit and ROAS reporting. recurring: type: boolean callProduct: type: boolean category: type: string description: Category name. Click: type: object properties: id: type: string leadId: type: string description: Id of the lead this click is attributed to. Empty until the click is connected to a lead. email: type: string description: Email of that lead. Empty until the click is connected to a lead. date: type: string description: Returned in the legacy `EEE MMM dd HH:mm:ss zzz yyyy` format instead of ISO 8601. example: 'Thu Nov 17 10:51:54 ART 2022' trackedUrl: type: string page: type: string previousUrl: type: string adspendType: $ref: '#/components/schemas/AdspendType' sourceLinkName: type: string ip: type: string agent: type: string cartId: type: string deduplicationParams: type: object additionalProperties: true adSpendId: type: integer parsedParameters: type: object additionalProperties: true Sale: type: object properties: id: type: string orderId: type: string creationDate: type: string description: Returned in the legacy `EEE MMM dd HH:mm:ss zzz yyyy` format instead of ISO 8601. example: 'Thu Jul 02 01:10:33 UTC 2026' refundDate: type: string description: Returned in the legacy `EEE MMM dd HH:mm:ss zzz yyyy` format instead of ISO 8601. Present only when the sale was refunded. example: 'Thu Jul 02 01:10:33 UTC 2026' qualified: type: boolean score: type: number recurring: type: boolean quantity: type: number lead: $ref: '#/components/schemas/Lead' firstSource: $ref: '#/components/schemas/Attribution' lastSource: $ref: '#/components/schemas/Attribution' price: $ref: '#/components/schemas/Price' product: $ref: '#/components/schemas/SaleProduct' provider: $ref: '#/components/schemas/Provider' ConversionPath: type: object description: > One conversion with the ordered source touches it was attributed with. `price`, `usdPrice` and `firstSale` are present on `SALE` and `CALL` rows only; a `LEAD` row does not carry those keys at all. `usdPrice` holds the same amounts as `price`, converted to USD. properties: id: type: string description: > The sale id for `SALE` (`sle-` prefixed), the call id for `CALL` (`cll-` prefixed), and the lead id for `LEAD`. leadId: type: string description: Id of the lead the conversion belongs to. creationDate: type: string description: > ISO 8601 date in the account timezone. It is the sale date for `SALE` and `CALL`, and the join date for `LEAD`. example: '2026-08-14T12:33:30-05:00' price: $ref: '#/components/schemas/Price' usdPrice: $ref: '#/components/schemas/Price' firstSale: type: boolean description: > Whether this is the lead's first sale, or first call. Repeat purchases are returned like any other conversion, so this is what tells them apart. path: type: array description: > The touches the conversion was attributed with, ordered by `clickDate` ascending. Each entry has the same shape as the `firstSource` and `lastSource` of `/sales`, `/calls` and `/leads`. For `SALE` and `CALL` it is the touches the conversion was attributed with up to the sale date; for `LEAD` it is the lead's full touch list. It can be empty when the conversion has no touch on record. items: $ref: '#/components/schemas/Attribution' CustomCost: type: object properties: id: type: string name: type: string cost: type: number frequency: type: string enum: [DAILY, ONE_TIME, MONTHLY] startDate: type: string description: Local ISO 8601 date time with no timezone. example: '2024-12-25T00:00' endDate: type: string nullable: true description: Local ISO 8601 date time with no timezone. example: '2024-12-25T00:00' tags: type: array items: type: string Call: type: object properties: id: type: string tag: type: string qualified: type: boolean name: type: string externalId: type: string score: type: number creationDate: type: string description: Returned in the legacy `EEE MMM dd HH:mm:ss zzz yyyy` format instead of ISO 8601. example: 'Thu Nov 17 10:51:54 ART 2022' state: $ref: '#/components/schemas/CallState' qualification: type: object properties: name: type: string oldName: type: string lead: $ref: '#/components/schemas/Lead' firstSource: $ref: '#/components/schemas/Attribution' lastSource: $ref: '#/components/schemas/Attribution' Source: type: object properties: name: type: string tag: type: string disregarded: type: boolean organic: type: boolean adSource: $ref: '#/components/schemas/AdSource' trafficSource: $ref: '#/components/schemas/TrafficSource' goal: $ref: '#/components/schemas/Goal' category: $ref: '#/components/schemas/Category' creationDate: type: integer description: Returned as epoch milliseconds (a number) instead of ISO 8601. example: 1677151375000 UrlRuleWrite: type: object description: >- The URL rule fields shared by create (`POST`) and update (`PUT`). The two differ in what they require: create needs `name`, `tag`, `wordsToMatch` and `sourceRuleTypes` (see `UrlRuleCreate`), while update takes any subset of these fields and keeps every field it does not mention (see `UrlRuleUpdate`). properties: name: type: string maxLength: 255 description: >- A descriptive label for the rule. At most 255 characters. Cannot be blank: sending `""` is rejected with `Missing required field: name`, on `PUT` as well as on `POST`. tag: type: string description: >- The tag applied when the rule matches. Its prefix sets the rule flavor and must be one of `!` (action tag), `@` (source tag), `$` (sale tag) or `#` (subscription tag). A tag with a missing or unrecognized prefix is rejected with `Invalid tag: it must start with '!' (action tag), '$' (sale tag), '@' (source tag) or '#' (subscription tag). To create a lead-stage rule, send a tag without a prefix and set createLeadStage=true`. The one exception is a lead-stage rule (`createLeadStage: true`): send a tag **without** a prefix — it becomes the lead stage name, and the backend creates the stage and stamps the rule as `LEAD_STAGE`. Sending a prefixed tag alongside `createLeadStage: true` is rejected with `Invalid tag: a lead-stage rule (createLeadStage=true) requires a tag without a prefix; the tag becomes the lead stage name`. On `PUT` the prefix is judged against the rule's flavor, which `createLeadStage` inherits when omitted: a prefix-less tag is accepted on a lead-stage rule — which points it at the stage with that name, creating it if the account has none and leaving the previous stage in place — and rejected on any other, while a prefixed tag is accepted on any other rule and rejected on a lead-stage one, unless the same request sends `createLeadStage` to change the flavor. The tag cannot be blank either: `""` is rejected with `Missing required field: tag`. example: '!webinar-registration' wordsToMatch: type: array minItems: 1 items: type: string description: >- The rule matches when the URL contains any one of these words/substrings. Matching is case-insensitive. Must contain at least one non-empty value, otherwise the request is rejected with `Invalid field: wordsToMatch must contain at least one non-empty value`. A value cannot contain a comma: these are persisted comma-separated, so a comma would split one value into several. On `PUT` it replaces the stored list, which cannot be left empty. wordsNotToMatch: type: array items: type: string description: >- If the URL contains any of these words/substrings, the rule does not match. Matching is case-insensitive. A value cannot contain a comma, for the same reason as `wordsToMatch`. On `PUT` it replaces the stored list; send `[]` to clear it. sourceRuleTypes: type: array minItems: 1 items: type: string enum: [PREVIOUS_URL, REFERRER_URL] description: >- Which URL(s) to inspect. An unrecognized value is rejected with `Invalid sourceRuleTypes ''. Accepted values: PREVIOUS_URL, REFERRER_URL`. Must contain at least one value, otherwise the request is rejected with `Invalid field: sourceRuleTypes must contain at least one value`. On `PUT` it replaces the stored list, which cannot be left empty. applyBaseDomain: type: boolean description: Match against the base domain only instead of the complete URL. Defaults to false on create. disregardSource: type: boolean description: >- Mark matched traffic as low priority so it does not override an existing source ("disregard source"). Defaults to false on create. trafficSourceToMatch: type: array maxItems: 1 items: type: string description: >- URL parameter to read the traffic source from dynamically, e.g. `utm_source` ("Dynamic Source Traffic Parameter"). Maximum of 1 item; more than one is rejected with `Invalid field: trafficSourceToMatch accepts at most one value`. The value cannot contain a comma and is limited to 255 characters. Mutually exclusive with `trafficSourceCategory`: on `PUT`, sending a value here clears a traffic source category the rule already has; send `[]` to clear the parameter, which leaves the category alone. trafficSourceCategory: type: string description: >- Assign a fixed traffic source category by name ("Set manual Traffic Source"). The category is created if it does not exist. Mutually exclusive with `trafficSourceToMatch`; sending both is rejected with `Provide either trafficSourceToMatch (dynamic URL parameter) or trafficSourceCategory (manual category), not both`. On `PUT`, a name clears any `trafficSourceToMatch` the rule has; send `""` to remove the category, which leaves that parameter alone. sourceCategoryToMatch: type: array maxItems: 1 items: type: string description: >- URL parameter to read the source category from dynamically ("Dynamic Source Category Parameter"). Maximum of 1 item; more than one is rejected with `Invalid field: sourceCategoryToMatch accepts at most one value`. The value cannot contain a comma and is limited to 255 characters. Mutually exclusive with `sourceCategory`: on `PUT`, sending a value here clears a manual source category the rule already has; send `[]` to clear the parameter, which leaves the category alone. sourceCategory: type: string description: >- Assign a fixed source category by name ("Set manual Source Category"). The category is created if it does not exist. Mutually exclusive with `sourceCategoryToMatch`; sending both is rejected with `Provide either sourceCategoryToMatch (dynamic URL parameter) or sourceCategory (manual category), not both`. On `PUT`, a name clears any `sourceCategoryToMatch` the rule has; send `""` to remove the category, which leaves that parameter alone. scores: type: array items: $ref: '#/components/schemas/UrlRuleScore' description: >- Keyword groups that score the sale attributed to a matched click; with the default score of 0 they mark it as unqualified ("Keywords to mark sale as unqualified" in the UI). On `PUT` they replace the stored groups; send `[]` to clear them. isEnabled: type: boolean description: >- Whether the rule is active. Defaults to `true` on create. On `PUT`, send `false` to pause the rule and `true` to resume it; omitting it leaves the current state untouched. createLeadStage: type: boolean description: >- Marks a lead-stage rule. When `true`, send a prefix-less `tag`: it becomes the lead stage name, the backend creates the stage, and the rule flavor is stamped as `LEAD_STAGE`. Defaults to false on create. On `PUT` it is not a stored flag but the rule's flavor, so omitting it keeps the rule as it is — a lead-stage rule stays one, prefix-less tag included. Its stage is only touched when the binding is at stake: the rule becomes a lead-stage rule, the `tag` the stage takes its name from changes, or the rule is not bound to a stage yet. Changing the flavor takes this field and a matching `tag`: `true` with a prefix-less tag turns an ordinary rule into a lead-stage one, `false` with a prefixed tag turns a lead-stage rule back into an ordinary one (the rule stops pointing at the stage, which is itself left in place). UrlRuleCreate: description: >- Request body of `POST /api/v1.0/url-rules`: the shared URL rule fields, with the four a rule cannot be created without. allOf: - $ref: '#/components/schemas/UrlRuleWrite' - type: object required: [name, tag, wordsToMatch, sourceRuleTypes] UrlRuleUpdate: description: >- Request body of `PUT /api/v1.0/url-rules/{id}`: the same fields, all optional. Only the fields included in the request are modified; any field left out keeps its stored value. allOf: - $ref: '#/components/schemas/UrlRuleWrite' UrlRuleScore: type: object required: [keywords] description: >- A group of keywords that score the sale attributed to a matched click; with the default score of 0 they mark it as unqualified ("Keywords to mark sale as unqualified" in the UI). properties: keywords: type: array minItems: 1 items: type: string description: >- All of these keywords must be present in the matched URL for the entry to apply. Matching is case-insensitive. Every entry needs at least one non-empty keyword; an entry with none is rejected with `Invalid field: every entry in scores must contain at least one non-empty keyword`. score: type: number description: >- Score assigned to the sale attributed to the matched click. Defaults to 0 (unqualified) when omitted, matching the UI. If several entries match, the highest score wins. UrlRule: type: object description: A URL rule as returned by the retrieval endpoints. Only simple rules are exposed through the API. properties: id: type: string description: Opaque id of the rule (never the raw database id). Use it to retrieve, update or delete the rule. example: 'ur-a3f5c9d2e1b8074f6c2d9a1e5b3f7c8d0e2a4b6c8d0f1a3b5c7d9e1f2a4b6c8d' name: type: string tag: type: string description: >- The tag applied when the rule matches. Its prefix reflects the flavor — `!` action, `@` source, `$` sale, `#` subscription. A lead-stage rule has a prefix-less tag (the lead stage name). example: '!webinar-registration' wordsToMatch: type: array items: type: string description: The rule matches when the URL contains any one of these words/substrings. Matching is case-insensitive. Omitted when empty. wordsNotToMatch: type: array items: type: string description: If the URL contains any of these words/substrings, the rule does not match. Matching is case-insensitive. Omitted when empty. sourceRuleTypes: type: array items: type: string enum: [PREVIOUS_URL, REFERRER_URL] description: Which URL(s) the rule inspects. Omitted when empty. applyBaseDomain: type: boolean description: Whether the rule matches against the base domain only instead of the complete URL. disregardSource: type: boolean description: Whether matched traffic is marked as low priority so it does not override an existing source ("disregard source"). trafficSourceToMatch: type: array items: type: string description: The URL parameter the traffic source is read from, e.g. `utm_source` ("manual traffic source"). Omitted when empty. sourceCategoryToMatch: type: array items: type: string description: The URL parameter the source category is read from dynamically ("Dynamic Source Category Parameter"). Omitted when empty. trafficSourceCategory: type: string description: The name of the fixed traffic source category assigned to the rule ("Set manual Traffic Source"). Omitted when the rule has none. sourceCategory: type: string description: The name of the fixed source category assigned to the rule ("Set manual Source Category"). Omitted when the rule has none. scores: type: array items: $ref: '#/components/schemas/UrlRuleScore' description: >- Keyword groups that score the sale attributed to a matched click; with the default score of 0 they mark it as unqualified. Omitted when empty. urlRuleActionType: type: string enum: [ACTION, SOURCE_LINK, SALE, SUBSCRIPTION, LEAD_STAGE] description: >- The rule flavor — `ACTION` (`!`), `SOURCE_LINK` (`@`), `SALE` (`$`) and `SUBSCRIPTION` (`#`) are derived from the tag prefix; `LEAD_STAGE` results from creating the rule with `createLeadStage: true`. urlRuleType: type: string enum: [SIMPLE] description: Only simple rules are exposed through the API, so this is always `SIMPLE`. isEnabled: type: boolean description: Whether the rule is currently active. createLeadStage: type: boolean description: >- Whether this is a lead-stage rule (`true` exactly when `urlRuleActionType` is `LEAD_STAGE`). Mirrors the write field, but `PUT` no longer needs it: an update that omits it inherits the rule's flavor, so a lead-stage rule keeps its prefix-less `tag`. creationDate: type: string description: Rule creation date, in UTC ISO 8601. example: '2025-10-20T14:00:00Z' Subscription: type: object properties: id: type: string startDate: type: string description: Returned in the legacy `EEE MMM dd HH:mm:ss zzz yyyy` format instead of ISO 8601. example: 'Thu Nov 17 10:51:54 ART 2022' endDate: type: string description: Returned in the legacy `EEE MMM dd HH:mm:ss zzz yyyy` format instead of ISO 8601. example: 'Thu Nov 17 10:51:54 ART 2022' cancelAtDate: type: string description: Returned in the legacy `EEE MMM dd HH:mm:ss zzz yyyy` format instead of ISO 8601. example: 'Thu Nov 17 10:51:54 ART 2022' trialStartDate: type: string description: Returned in the legacy `EEE MMM dd HH:mm:ss zzz yyyy` format instead of ISO 8601. example: 'Thu Nov 17 10:51:54 ART 2022' trialEndDate: type: string description: Returned in the legacy `EEE MMM dd HH:mm:ss zzz yyyy` format instead of ISO 8601. example: 'Thu Nov 17 10:51:54 ART 2022' lastUpdatedDate: type: string description: ISO 8601 date of the last change to the subscription. example: '2021-04-16T20:35:00-05:00' price: type: number status: $ref: '#/components/schemas/SubscriptionStatus' periodicity: type: string planId: type: string tag: type: string name: type: string lead: $ref: '#/components/schemas/Lead' firstSource: $ref: '#/components/schemas/Attribution' lastSource: $ref: '#/components/schemas/Attribution' category: $ref: '#/components/schemas/Category' description: >- Category of the first paid sale item attributed to the subscription. Set once when the first payment is attributed and not updated if the plan changes later. Null when no paid sale has been attributed yet, for example a subscription still in its trial period. provider: $ref: '#/components/schemas/Provider' Cart: type: object properties: id: type: string orderId: type: string description: Only present if cart was purchased. creationDate: type: string description: Cart creation date, in UTC. example: '2022-11-17T13:51:54Z' events: type: integer lead: $ref: '#/components/schemas/LeadWithConsent' provider: $ref: '#/components/schemas/Provider' products: type: array items: $ref: '#/components/schemas/Product' firstSource: $ref: '#/components/schemas/Attribution' lastSource: $ref: '#/components/schemas/Attribution' LeadJourney: type: object description: Every field but `lead` is omitted when the request's `fields` did not select it. A field that was selected but holds no data comes back empty. properties: lead: $ref: '#/components/schemas/LeadWithStage' sales: type: array items: $ref: '#/components/schemas/Sale' calls: type: array items: $ref: '#/components/schemas/Call' carts: type: array items: type: object properties: id: type: string orderId: type: string description: Only present if cart was purchased. creationDate: type: string description: Returned in the legacy `EEE MMM dd HH:mm:ss zzz yyyy` format instead of ISO 8601, unlike the `creationDate` of `/carts`. example: 'Thu Nov 17 10:51:54 ART 2022' events: type: integer provider: $ref: '#/components/schemas/Provider' products: type: array items: $ref: '#/components/schemas/SaleProduct' firstSource: $ref: '#/components/schemas/Attribution' lastSource: $ref: '#/components/schemas/Attribution' subscriptions: type: array items: $ref: '#/components/schemas/Subscription' linkedLeads: type: array items: $ref: '#/components/schemas/LeadWithStage' journey: type: array description: Chronological journey events for the lead. Only present when `includeEvents=true` or when `journey` is requested in `fields`. items: $ref: '#/components/schemas/JourneyEvent' JourneyEvent: type: object description: | A single event in a lead's journey — a source-link click, a stage change, a sale, a call, a cart, a subscription, an action tag, an opt-in or an email. Which fields are present depends on the event `type` (enumerated below). Source click events also report the ad hierarchy they belong to — `trafficSource`, `category`, `adAccount`, `campaign`, `adSet` and `ad`. No other event type carries them, and each level is omitted when it does not exist for that source: - organic sources and email links have no ad hierarchy at all, only their Hyros groupings (`trafficSource`, `category`) - sources tracked at campaign level (Google, LinkedIn) report `campaign` with no `adSet` above it - platforms with no campaign level of their own (TikTok, Snapchat, Pinterest, Bing, X) report `adSet` and no `campaign` - `ad` is present only when the click was attributed to a specific ad properties: type: type: string description: | The kind of event. One of: - `sl` — a click on a source link (the tracked touch-points that build attribution); carries the ad hierarchy it belongs to - `lead-stage` — the lead reached a pipeline stage - `sale` — a purchase - `call` — a booked call - `cart` — a cart event - `subscription-started` — a subscription started - `subscription-ended` — a subscription ended - `action` — the lead achieved a `!` action tag - `opt-in` — the lead opted in - `email` — an email was sent to (or generated for) the lead - `email-open` — the lead opened an email id: type: string description: Identifier of the underlying record. Present only for events backed by an external entity (`sale`, `call`, `subscription-started`, `subscription-ended`, `email`, `email-open`). date: type: string description: ISO 8601 timestamp (in the account timezone) when the event occurred. extra: type: string description: Additional context for the event. tag: type: string name: type: string keyword: type: string integrationProvider: type: string description: Source integration/provider associated with the event, when applicable. currencySymbol: type: string description: Currency symbol for monetary events (e.g. sales). emailSource: type: boolean description: Whether the event originated from an email source. subNames: type: array items: type: string description: Sub-element names, e.g. the individual products of a grouped cart event. trafficSource: $ref: '#/components/schemas/TrafficSource' category: $ref: '#/components/schemas/Category' adAccount: $ref: '#/components/schemas/AdHierarchyLevel' campaign: $ref: '#/components/schemas/AdHierarchyLevel' adSet: $ref: '#/components/schemas/AdHierarchyLevel' ad: $ref: '#/components/schemas/AdHierarchyLevel' paths: /api/v1.0/leads: get: tags: [Leads] summary: Retrieve Leads description: | > [!NOTE] > **Required role** — Get Leads Search and retrieve leads by their join date, last updated date, email, phone number, id, current stage, tags or the date a tag was applied. Every filter is optional. parameters: - name: ids in: query description: Array of lead ids. Maximum of 50 ids. schema: type: string example: '"id1","id2","id3"' - name: emails in: query description: Array of emails or email prefixes. Maximum of 50 emails. schema: type: string example: '"email1","email2"' - name: phones in: query description: Array of phone numbers. Maximum of 50 phones. schema: type: string example: '"+1 202-555-0184","2025550139"' - name: tags in: query description: Array of tag names. Leads matching any of the provided tags will be retrieved. Tags are matched exactly, including any prefix (e.g. `!Tag1`). Maximum of 50 tags. schema: type: string example: '"!Tag1","!Tag2"' - name: tagFromDate in: query description: | ISO 8601 date. Only leads that had a tag applied on or after this date will be retrieved. Combined with `tags` it returns the leads that received one of those tags inside the period, so a lead tagged before it is left out even if it still holds the tag; on its own it matches any tag applied in the period. Leads whose tag assignment has no recorded date are not retrieved. A `tagFromDate` later than `tagToDate` is rejected with `400 Bad Request`. Cannot be later than `tagToDate`. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: tagToDate in: query description: ISO 8601 date. Only leads that had a tag applied on or before this date will be retrieved. Cannot be earlier than `tagFromDate`. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: stage in: query description: Array of stage names. Leads matching any of the provided stages will be retrieved. Stage names are matched case-insensitively. Maximum of 50 stages. schema: type: string example: '"stage1","stage2"' - name: fromDate in: query description: ISO 8601 date. Only leads whose join date is more recent than this will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or later than toDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: toDate in: query description: ISO 8601 date. Only leads whose join date is older than this will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or earlier than fromDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: updatedFromDate in: query description: ISO 8601 date. Only leads updated on or after this date will be retrieved. Use it to fetch just the leads that changed since your last request. Cannot be later than `updatedToDate`. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: updatedToDate in: query description: ISO 8601 date. Only leads updated on or before this date will be retrieved. Cannot be earlier than `updatedFromDate`. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: pageSize in: query description: Maximum number of leads per page. Range 1-250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query description: ID of the next page, taken from the `nextPageId` of a previous response. Omit it to fetch the first page. An invalid or expired pagination cursor is rejected with `400 Bad Request`. schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/LeadWithStage' nextPageId: type: string request_id: type: string example: result: - email: lead1@email.com id: 40b5af5444756c2b5e666fcb658affd2a4b455bce3711c43f88763147381e368 creationDate: '2023-01-04T04:36:41-05:00' lastUpdatedDate: '2023-06-10T09:12:00-05:00' tags: ['$ettst'] adOptimizationConsent: GRANTED currentStage: name: stage1 date: '2023-01-06T09:12:00-05:00' firstSource: sourceLinkId: 7bc217f372eab0efa188bb5b05be87460976e68e03b801d53c39bb4ed54c9750 name: Facebook Adset tag: '@facebook-adset' clickDate: '2023-01-03T22:14:07-05:00' lastSource: sourceLinkId: 12cc9f521ba37e33d624927ea7088d91a6b2e39bf1466c281120cde22a4bee84 name: Facebook Adset 2 tag: '@facebook-adset-2' clickDate: '2023-01-04T01:02:59-05:00' originLead: email: originlead@email.com id: 9c1e5b0a4f7d2c8e6b3a1f0d9e8c7b6a5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a creationDate: '2022-12-30T09:12:00-05:00' isOriginLead: true tags: ['$origin'] - email: lead2@email.com id: b5981e8ce87d08773a4ggre985d2d9949153c6fb55cb5075291941d9f23f4282 creationDate: '2023-01-04T02:37:45-05:00' ips: ['1.2.3.4'] phoneNumbers: ['202-555-0184'] adOptimizationConsent: UNSPECIFIED nextPageId: 568fd86587125f55117505312dc72bb8b71e9647a25e5b142d3f28bccd228360 request_id: e47941e7104gtrgtrgtr24d0884c6df09407e76fd '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: [Leads] summary: Create Lead description: | > [!NOTE] > **Required role** — Create Leads > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~10 seconds. Create or Update leads and their tags. If the applied Tag matches with that of a Product, a sale will be generated for said lead, only if it didn't already have the tag. On an existing lead, `phoneNumbers` replaces the stored numbers, so omitting it deletes them all. Use `PUT /leads` to apply a tag without losing them. requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: Email of the lead. Required if no phone number is provided. If only a phone number is provided, a placeholder email `@hyrosapi.com` is generated and returned as the lead's email. firstName: type: string lastName: type: string tags: type: array items: type: string description: Tags to apply to the lead. Send a JSON array; a single tag may also be sent as a plain string, for clients whose form builder cannot send an array. An empty or blank string, like `null`, applies no tags. Any other shape, such as an object, is rejected with `400 Bad Request`. tagsDate: type: string description: Optional ISO-8601 date (`2023-05-01T10:00:00-03:00`) applied as the assignment date of **every** tag in `tags`, to backdate historical tags during imports, migrations or CRM syncs. Defaults to the current time when omitted. Cannot be in the future. To assign tags with different dates, send one request per date. If a tag generates a sale or a source attribution, those are backdated too. Without a time zone offset the date is stored as UTC; see **Date Formats**. leadIps: type: array items: type: string description: IPs of the lead for ad attribution. phoneNumbers: oneOf: - type: string - type: array items: type: string description: Phone numbers of the lead. Required if no email is provided. On an existing lead this replaces the stored numbers. stage: type: string description: Stage to apply to the lead. On create the stage is a plain string; on update (`PUT /leads`) it is set via the `leadStage` object (`{name, date}`) instead. adOptimizationConsent: $ref: '#/components/schemas/AdOptimizationConsent' description: Optional. Sent blank it reads as UNSPECIFIED. Omitted, a new lead starts as UNSPECIFIED and an existing lead keeps the value it already had. example: email: John@doe.com firstName: John lastName: Doe tags: ['!Tag1'] tagsDate: '2023-05-01T10:00:00-03:00' leadIps: ['172.8.105.28'] phoneNumbers: ['1105385366'] stage: MQL adOptimizationConsent: GRANTED responses: '200': description: Lead created/updated successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. Also returned when the request carries a blacklisted email address, with the message `The event was discarded because an email or phone number it carries is blacklisted for this account. This is set when a lead is permanently deleted.` No `request_id` is issued in that case, so there is nothing to poll. See *Delete Lead*. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' put: tags: [Leads] summary: Update Lead description: | > [!NOTE] > **Required role** — Update Leads > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~5 minutes. Update leads, their tags and their stages. `phoneNumbers` is added to the lead; existing numbers are kept. Lead stages can be applied and removed using `leadStage` and `removeLeadStages`. A stage applied by mistake can therefore be removed without manual cleanup. Every stage name in `removeLeadStages` must exist in the account; otherwise, the whole request is rejected with a `400 Bad Request` and none of the changes are applied. Stages that the lead does not currently have are ignored. The same stage cannot be applied and removed in the same request. Once a stage is removed, the lead is no longer counted for that stage in reports. Tag assignments can be backdated with the optional `tagsDate` field (ISO-8601). It applies as the assignment date of **every** tag in `tags`, so historical tags keep an accurate timeline during imports, migrations or CRM syncs; it defaults to the current time when omitted and cannot be in the future. To assign tags with different dates, send one request per date. Tags the lead already has are ignored (their existing date is not changed). If a tag generates a sale or a source attribution, those are backdated too. parameters: - name: email in: query description: Email used to search for the lead to update. schema: type: string - name: id in: query description: ID used to search for the lead to update. schema: type: string - name: phone in: query description: Phone used to search for the lead to update. schema: type: string requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: New email. firstName: type: string lastName: type: string tags: type: array description: Tags to add to the lead. Send a JSON array; a single tag may also be sent as a plain string, for clients whose form builder cannot send an array. An empty or blank string, like `null`, applies no tags. Any other shape, such as an object, is rejected with `400 Bad Request`. items: type: string tagsDate: type: string description: Optional ISO-8601 date applied as the assignment date of every tag in `tags` (defaults to now, cannot be in the future). Tags the lead already has are ignored, not re-dated. For tags with different dates, send one request per date. Without a time zone offset the date is stored as UTC; see **Date Formats**. removeTags: type: array description: Tags to remove from the lead. Send a JSON array; a single tag may also be sent as a plain string, for clients whose form builder cannot send an array. An empty or blank string, like `null`, removes no tags. Any other shape, such as an object, is rejected with `400 Bad Request`. items: type: string leadIps: type: array items: type: string phoneNumbers: type: array description: Phone numbers to add to the lead. Existing numbers are kept. items: type: string adOptimizationConsent: $ref: '#/components/schemas/AdOptimizationConsent' description: Optional. Blank reads as UNSPECIFIED; omitted leaves the lead's current value unchanged. leadStage: type: object description: Stage to apply to the lead on update. Create (`POST /leads`) uses a plain `stage` string instead. required: [name] properties: name: type: string description: Stage name to apply. Required and cannot be empty or blank; the value is trimmed, and a missing, empty or whitespace-only name is rejected with `400 Bad Request` and message `The lead stage name cannot be empty`. date: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. A malformed date is rejected with `400 Bad Request` and message `Invalid date format.`. removeLeadStages: type: array description: Names of the stages to remove from the lead. Unknown names reject the request; stages the lead does not hold are ignored. items: type: string example: email: John@doe.com firstName: John lastName: Doe tags: ['!Tag1'] tagsDate: '2023-05-01T10:00:00-03:00' removeTags: ['!Tag2'] leadIps: ['172.8.105.28'] phoneNumbers: ['1105385366'] adOptimizationConsent: GRANTED leadStage: name: SQL date: '2025-11-13T14:00:00-03:00' removeLeadStages: ['MQL'] responses: '200': description: Lead updated successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. Also returned when the request carries a blacklisted email address, with the message `The event was discarded because an email or phone number it carries is blacklisted for this account. This is set when a lead is permanently deleted.` No `request_id` is issued in that case, so there is nothing to poll. See *Delete Lead*. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' delete: tags: [Leads] summary: Delete Lead description: | > [!NOTE] > **Required role** — Delete Leads > [!NOTE] > **Asynchronous** — the erasure applies immediately, but the lead typically takes ~5 minutes to disappear from `GET /leads`. Permanently delete a lead and its personal data. Provide one of email or id to identify the lead. Personal data is redacted, and phone numbers, sales and tracking data are deleted while subscriptions are cancelled. The lead's email address and phone numbers are also **blacklisted**, so later events carrying them are discarded instead of re-creating the lead. Any later write carrying that email address — creating the lead again, updating it, or sending an order, cart, call, subscription or click for it — is rejected with a `400` explaining the discard, rather than accepted and silently dropped. This cannot be undone through the API: blacklist entries are only removable from the Hyros app, under **Settings → Tracking Configuration → Blacklist**. parameters: - name: email in: query description: Email used to search for the lead to delete. schema: type: string - name: id in: query description: ID used to search for the lead to delete. schema: type: string responses: '200': description: Lead deleted successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/leads/tags: post: tags: [Leads] summary: Add Tags to Leads description: | > [!NOTE] > **Required role** — Add Tags to Leads > [!NOTE] > **Asynchronous** — the change is typically visible within a few minutes, and can take longer for a large selection. Apply the same tags to every lead selected by `ids` and/or `emails` in one call, instead of sending one `PUT /leads` per lead. At least one of the two selectors is required. The two selectors are combined as a union, so a lead named by both is tagged once, and an email may match several leads, in which case all of them are tagged. Only leads of the account the API-Key belongs to are tagged. Ids and emails that match no lead are skipped; a request whose selectors match no lead at all is rejected with `Lead not found.` The tags are applied through the same call as `PUT /leads`, so the tag semantics are identical: a name prefixed with `$` is a sale tag, with `@` a source tag and with `!` an action tag, while a name with no prefix defaults to an action tag. A `$` tag matching a product's tag generates a sale for each selected lead that did not already carry that tag, so in bulk this multiplies across the selection. The response carries a `request_id`. Once `GET /api/v1.0/requests/{request_id}` reports it as `PROCESSED`, its `result` is the array of the tagged leads, in the shape `GET /leads` returns them. requestBody: required: true content: application/json: schema: type: object required: [tags] properties: ids: type: array items: type: string description: | Ids of the leads to tag, as `GET /leads` returns them. Send a JSON array; a single value may also be sent as a plain string. An empty or blank string is read as no value. Any other shape, such as an object, is rejected with `400 Bad Request`. Maximum of 50 ids. maxItems: 50 emails: type: array items: type: string description: | Emails of the leads to tag, matched exactly. Send a JSON array; a single value may also be sent as a plain string. An empty or blank string is read as no value. Any other shape, such as an object, is rejected with `400 Bad Request`. Maximum of 50 emails. maxItems: 50 tags: type: array items: type: string description: | Tags to apply to every selected lead. A name prefixed with `$` is a sale tag, with `@` a source tag and with `!` an action tag; a name with no prefix defaults to an action tag. Send a JSON array; a single tag may also be sent as a plain string. An empty or blank string is read as no tags and answered with `Missing required field: tags`. Any other shape, such as an object, is rejected with `400 Bad Request`. Maximum of 50 tags. maxItems: 50 tagsDate: type: string description: | Optional ISO-8601 date applied as the assignment date of every tag in this request, to backdate historical tags (defaults to now, cannot be in the future). Unlike `PUT /leads`, action tags (`!`) a lead already carries are re-dated to this date, so an existing assignment date is corrected; sale (`$`) and source (`@`) tags it already carries are left untouched. For tags with different dates, send one request per date. Without a time zone offset the date is stored as UTC; see **Date Formats**. example: ids: ['40b5af5444756c2b5e666fcb658affd2a4b455bce3711c43f88763147381e368'] emails: ['lead1@email.com', 'lead2@email.com'] tags: ['!Tag1'] tagsDate: '2023-05-01T10:00:00-03:00' responses: '200': description: | Request accepted; poll the returned `request_id` on `GET /api/v1.0/requests/{request_id}` for the outcome. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: | Bad Request. Possible messages: `Either 'ids' or 'emails' must be provided.`, `Missing required field: tags`, `The maximum number of ids to be provided for update is 50`, `The maximum number of emails to be provided for update is 50`, `The maximum number of tags to be provided for update is 50`, `ids must be an array of strings`, `emails must be an array of strings`, `tags must be an array of strings`, `Invalid date format.`, `The tags date cannot be in the future.`, `Lead not found.` when the selectors match no lead of the account, and `There was a problem while processing the request` when nothing more specific applies. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/leads/journey: get: tags: [Leads] summary: Retrieve Leads Journey description: | > [!NOTE] > **Required role** — Get Lead Journey Retrieve the details about leads journeys using lead ids and/or emails. At least one of `ids` or `emails` must be provided. Emails are matched exactly (case-insensitive); an email may resolve to more than one lead, in which case one journey is returned per matched lead. Each journey includes the lead's sales, calls, carts, subscriptions and linked leads. Set `includeEvents=true` to additionally return a chronological `journey` array with every event in the lead's timeline: source-link clicks, stage changes, sales, calls, carts, subscriptions, action (`!`) tags, opt-ins and emails (sends and opens). Pass `fields` to return only some of those fields; a field that is not requested is not retrieved either, so a narrower selection answers faster. parameters: - name: ids in: query description: Array of lead ids. Maximum of 50 ids. schema: type: string example: '"id1","id2","id3"' - name: emails in: query description: Array of lead emails, matched exactly (case-insensitive). Maximum of 50 emails. schema: type: string example: '"lead1@email.com","lead2@email.com"' - name: includeEvents in: query description: 'When `true`, each returned journey includes a `journey` array with every event in the lead''s timeline, in chronological order: source-link clicks, stage changes, sales, calls, carts, subscriptions, action (`!`) tags, opt-ins and emails (sends and opens). Source click events also carry the ad hierarchy they belong to. Defaults to `false`. Asking for `journey` in `fields` does the same.' schema: type: boolean default: false - name: fields in: query required: false description: Comma-separated fields to include in each result. If omitted, every field except `journey` is returned. `lead` is always included, since it identifies whose journey the entry is. A field that is requested but holds no data is returned empty, and one that is not requested is not retrieved either, so a narrower selection answers faster. schema: type: string enum: [lead, sales, calls, carts, subscriptions, linkedLeads, journey] example: 'lead,sales' responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/LeadJourney' request_id: type: string '400': description: 'Bad Request. An unknown value in `fields` is rejected with `Invalid field: `.' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/leads/aggregation: get: tags: [Leads] summary: Count Leads By Attribute description: | > [!NOTE] > **Required role** — Get Lead Aggregation Counts the leads from a search by grouping them according to an attribute, which can be a tag, the current stage, or the tag of the first or last source. It is used to determine how many leads there are rather than which leads they are; for example, to understand how leads are distributed across stages, which tags are most frequently used, or where leads from a given period came from. The response contains three numbers that answer different questions: `groups` shows how the leads that have a value for the selected attribute are distributed; `remainingCount` indicates how many leads were left out because their group did not fit within the `groupsLimit` request parameter, so increasing this limit by that number is enough to retrieve all remaining groups; and `totalCount` indicates how many leads matched the search. The groups should not be summed to obtain a total, since a lead without the attribute does not belong to any group, while a lead with multiple values can belong to more than one group. parameters: - name: attribute in: query required: true description: | The attribute to group the leads by. - `TAG` — groups by tag name; a lead is counted once per tag it holds. - `STAGE` — groups by the lead's current stage. The group key is the stage name; a stage deleted after the lead was indexed is reported by its id instead. - `FIRST_SOURCE_TAG` — groups by the tag of the first source link the lead was seen through. - `LAST_SOURCE_TAG` — groups by the tag of the last source link the lead was seen through. schema: type: string enum: [TAG, STAGE, FIRST_SOURCE_TAG, LAST_SOURCE_TAG] example: STAGE - name: groupsLimit in: query description: The maximum number of groups to return, the ones with the most leads first. A group is one value of the attribute. Must be between 1 and 1000. schema: type: integer minimum: 1 maximum: 1000 default: 250 - name: ids in: query description: Array of lead ids to filter by. At most 50 can be provided. schema: type: string example: '"id1","id2","id3"' - name: emails in: query description: Array of emails or email prefixes to filter leads by. At most 50 can be provided. schema: type: string example: '"email1","email2"' - name: phones in: query description: Array of phone numbers to filter leads by. Matching is done on the last digits, so formatting, spaces and country/area codes are tolerated. At most 50 can be provided. schema: type: string example: '"+1 202-555-0184","2025550139"' - name: tags in: query description: Comma-separated tag names. Leads holding ANY of them are counted. Names are matched exactly, including their prefix. This filter narrows which leads are counted, not which groups come back. At most 50 can be provided. schema: type: string example: '"!Tag1","!Tag2"' - name: tagFromDate in: query description: ISO 8601 date. Only leads that had a tag applied on or after this date are counted. Cannot be later than `tagToDate`. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: tagToDate in: query description: ISO 8601 date. Only leads that had a tag applied on or before this date are counted. Cannot be earlier than `tagFromDate`. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: stage in: query description: Comma-separated stage names. Leads whose current stage matches ANY of them are counted. A name that matches no stage of the account is not an error, and the result comes back empty rather than unfiltered. At most 50 can be provided. schema: type: string example: '"Booked Call","Closed Won"' - name: fromDate in: query description: ISO 8601 date. Only leads whose join date is more recent than this are counted. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or later than toDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: toDate in: query description: ISO 8601 date. Only leads whose join date is older than this are counted. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or earlier than fromDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: updatedFromDate in: query description: ISO 8601 date. Only leads updated on or after this date are counted. Cannot be later than `updatedToDate`. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: updatedToDate in: query description: ISO 8601 date. Only leads updated on or before this date are counted. Cannot be earlier than `updatedFromDate`. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: $ref: '#/components/schemas/AggregationResult' request_id: type: string example: result: groups: - key: Opt In count: 12 - key: Booked Call count: 8 - key: Closed Won count: 2 remainingCount: 0 totalCount: 30 request_id: 43573923369e40bbafd46925a5c15ff5 '400': description: Bad Request. `attribute` is missing or is not one of the accepted values, or `groupsLimit` is outside 1-1000. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/leads/clicks: get: tags: [Leads] summary: Retrieve Lead Clicks description: | > [!NOTE] > **Required role** — Get Lead Clicks Retrieve clicks belonging to one or many leads. Exactly one of `leadId`, `leadIds` or `emails` must be provided; sending none, or more than one, returns a `400`. Date filters, page size and pagination apply over the clicks of every selected lead as a single result set, ordered from oldest to newest, and each click carries the `leadId` and `email` of the lead it belongs to. A lead that does not exist is skipped rather than causing the request to fail. For `leadId` and `email` return an empty `result` when the lead cannot be found, and `leadIds` and `emails` skip the ids and emails they cannot resolve, returning an empty `result` when none of them match. A lead with no clicks and a lead that does not exist are therefore indistinguishable in the response. parameters: - name: leadId in: query description: A single lead id. schema: type: string - name: leadIds in: query description: Array of lead ids. Maximum of 50 lead ids. schema: type: string - name: emails in: query description: Array of lead emails, matched exactly (case-insensitive). Maximum of 50 emails. An email may resolve to more than one lead, in which case the clicks of every matched lead are returned. schema: type: string - name: email in: query description: "DEPRECATED: Use emails instead. The lead email, resolving to a single lead." schema: type: string - name: pageSize in: query description: Maximum number of clicks per page. Between 1 and 250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query description: ID of the next page. schema: type: string - name: fromDate in: query description: ISO 8601 date. Only clicks dated after this will be retrieved. When omitted, the search starts 90 days before the earliest join date among the selected leads, the age at which unassigned clicks are deleted, or has no lower bound when a selected lead has no join date. Providing it is recommended when selecting many leads, as it bounds the amount of data searched. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or later than toDate. schema: type: string example: '2025-04-16T20:35:00-05:00' - name: toDate in: query description: ISO 8601 date. Only clicks dated before this will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or earlier than fromDate. schema: type: string example: '2025-04-16T20:35:00-05:00' responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/Click' nextPageId: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/sales: get: tags: [Sales] summary: Retrieve Sales description: | > [!NOTE] > **Required role** — Get Sales Search and retrieve sales by date, email, lead id, product tag, id, recurring status, and refund status. **`fromDate` and `toDate` always bound the date the sale was made, never the date it was refunded.** So `saleRefundedState=REFUNDED` over a range returns the sales *made* in it that are refunded today — not the refunds *issued* in it, which would also cover sales made earlier. Each sale carries its own `refundDate` to count by. Filtering by `orderId` is **not** supported — the supported filters are the query parameters listed below; `orderId` is present only in the response body. Response `creationDate`/`refundDate` are returned in `EEE MMM dd HH:mm:ss zzz yyyy` format, not ISO 8601. parameters: - name: ids in: query description: Array of sales ids. Maximum of 50 ids. schema: type: string - name: emails in: query description: Array of emails or prefixes. Maximum of 50 emails. schema: type: string - name: leadIds in: query description: Array of leadIds. Maximum of 50 lead ids. schema: type: string - name: productTags in: query description: Array of product tags. Maximum of 20 product tags. schema: type: string - name: isRecurringSale in: query description: Filter by recurring status. schema: type: string enum: [RECURRING, NON_RECURRING, ALL] default: ALL - name: saleRefundedState in: query description: Filter by refund status. schema: type: string enum: [REFUNDED, NON_REFUNDED, ALL] default: ALL - name: fromDate in: query description: ISO 8601 date. Only sales after this date will be retrieved. Bounds the date the sale was made, never the date of its refund. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or later than toDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: toDate in: query description: ISO 8601 date. Only sales before this date will be retrieved. Bounds the date the sale was made, never the date of its refund. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or earlier than fromDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: pageSize in: query description: Maximum number of sales per page. Range 1-250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query description: ID of the next page, taken from the `nextPageId` of a previous response. Omit it to fetch the first page. An invalid or expired pagination cursor is rejected with `400 Bad Request`. schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/Sale' nextPageId: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' put: tags: [Sales] summary: Update Sales description: | > [!NOTE] > **Required role** — Update Sales > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~5 minutes. Update sales by their ids. parameters: - name: ids in: query required: true description: Array of sales ids. Maximum of 50 ids. schema: type: string - name: isRecurringSale in: query description: Indicates if the sales will be recurring or not. schema: type: boolean - name: isRefunded in: query description: Indicates if the sales will be refunded or not. schema: type: boolean - name: refundedDate in: query description: ISO 8601 date. Optional even when `isRefunded` is true; if omitted, the refunded date is set to the present date. Without a time zone offset the date is stored as UTC; see **Date Formats**. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: refundedAmount in: query description: Amount to be refunded. schema: type: string responses: '200': description: Sales updated successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/sales/{id}: delete: tags: [Sales] summary: Delete Sale description: | > [!NOTE] > **Required role** — Delete Sales > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~5 minutes. Delete a sale by its id. parameters: - name: id in: path required: true description: Sale id to be deleted. schema: type: string responses: '200': description: Sale deleted successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/orders: post: tags: [Orders] summary: Create Order description: | > [!NOTE] > **Required role** — Create Orders > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~10 seconds. Create an order with all necessary information. Additionally, creates the lead if not already present. requestBody: required: true content: application/json: schema: type: object required: [items] properties: email: type: string description: Email associated with the lead. Required if no phone number is provided. parentEmail: type: string description: Email of the origin lead. If present, the sale will be attributed to the origin lead. firstName: type: string lastName: type: string leadIps: type: array items: type: string description: IPs of the customer that made the purchase for ad attribution. Maximum of 3 ips. maxItems: 3 stage: type: string phoneNumbers: oneOf: - type: string - type: array items: type: string description: Required if no email is provided. orderId: type: string description: Identifier by which sales will be grouped. Only letters, numbers, underscores, hyphens, periods, and colons. No spaces. externalSubscriptionId: type: string cartId: type: string date: type: string description: ISO 8601 date when the transaction was processed. Defaults to current date. Without a time zone offset the date is stored as UTC; see **Date Formats**. example: '2021-04-16T20:35:00' shippingCost: type: number description: Shipping cost distributed across items. Default is 0. taxes: type: number description: Order taxes distributed across items. Default is 0. orderDiscount: type: number description: Discount applied to the complete order, distributed evenly across all line items. hardCost: type: number description: A cost the merchant pays out of the order total, typically payment-processor fees (e.g. Stripe). Distributed evenly across the line items and reported as part of the sale hard cost, lowering net profit. Unlike `taxes` and `shippingCost` it is **not** added to the order revenue, because the buyer did not pay it. Must not be negative. Default is 0. priceFormat: type: string enum: [DECIMAL, INTEGER] default: DECIMAL currency: type: string description: Currency code (e.g. EUR). Defaults to Hyros account setup. items: type: array items: $ref: '#/components/schemas/Item' minItems: 1 example: email: john@doe.com parentEmail: jane@doe.com firstName: John lastName: Doe leadIps: ['172.8.105.28'] stage: Customer phoneNumbers: ['1105385366'] orderId: '56354354588' cartId: d49b708b3df50505869ca54f026e7c97a4959b587605f14f91c7e289de9f80bd date: '2021-04-16T20:35:00' priceFormat: DECIMAL currency: USD taxes: 98.6 shippingCost: 10 orderDiscount: 3.5 hardCost: 3.15 items: - name: T-Shirt - Blue price: 15.50 costOfGoods: 2.25 externalId: '18294892740' quantity: 2 sku: DEPOR-XYZ-BLN-41 taxes: 2.5 itemDiscount: 2.5 packages: ['Package 1', 'Package 2'] isRebill: false tag: $premiun-t-shirt-blue categoryName: premium store responses: '200': description: Order created successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. Also returned when the request carries a blacklisted email address, with the message `The event was discarded because an email or phone number it carries is blacklisted for this account. This is set when a lead is permanently deleted.` No `request_id` is issued in that case, so there is nothing to poll. See *Delete Lead*. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/orders/{id}: put: tags: [Orders] summary: Update Order description: | > [!NOTE] > **Required role** — Update Orders > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~5 minutes. Update an order by replacing its items list and optionally updating order-level fields. The existing sales of the order are soft-deleted and recreated according to the provided items. parameters: - name: id in: path required: true description: | Identifier of the order to update. By default this is the order id used during creation. When `integrationType` is provided in the body, this value is matched against the `externalId` of any item belonging to the order; the whole order is then resolved through that item. schema: type: string requestBody: required: true content: application/json: schema: type: object required: [items] properties: integrationType: type: string description: External integration the order belongs to (e.g. STRIPE, SHOPIFY, KONNEKTIVE, API). When present, the path id is matched against an item externalId for that integration. example: STRIPE stage: type: string description: Stage to apply to the customer's lead. externalSubscriptionId: type: string cartId: type: string shippingCost: type: number description: Shipping cost distributed across items. Default is 0. example: 10.5 taxes: type: number description: Order taxes distributed across items. Default is 0. example: 9.8 orderDiscount: type: number description: Discount applied to the complete order, distributed evenly across all line items. example: 1.2 hardCost: type: number description: A cost the merchant pays out of the order total, typically payment-processor fees (e.g. Stripe). Distributed evenly across the line items and reported as part of the sale hard cost, lowering net profit; it is **not** added to the order revenue. If omitted, the value already stored on the order's sales is kept, so a later call can attach the fee without resending it. Send `0` to clear it. Must not be negative. example: 3.15 priceFormat: type: string enum: [DECIMAL, INTEGER] default: DECIMAL currency: type: string description: Currency code (e.g. EUR). Defaults to Hyros account setup. items: type: array items: $ref: '#/components/schemas/Item' minItems: 1 description: New items list. The existing items of the order will be replaced by this list. example: stage: Repeat Customer cartId: d49b708b3df50505869ca54f026e7c97a4959b587605f14f91c7e289de9f80bd priceFormat: DECIMAL currency: USD taxes: 98.6 shippingCost: 10 orderDiscount: 3.5 hardCost: 3.15 items: - name: T-Shirt - Blue price: 15.50 costOfGoods: 2.25 externalId: '18294892740' quantity: 2 sku: DEPOR-XYZ-BLN-41 taxes: 2.5 itemDiscount: 2.5 isRebill: false tag: $premiun-t-shirt-blue categoryName: premium store responses: '200': description: Order updated successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' delete: tags: [Orders] summary: Refund Order description: | > [!NOTE] > **Required role** — Delete Orders > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~5 minutes. Refund an order by order id and update the income of the lead. A refund records the refunded amount on the order's sales; it does not recalculate their costs. `shippingCost`, `taxes` and `hardCost` are left untouched, which matches how payment processors behave (e.g. Stripe keeps part of its fee on a refund). To adjust them, send a `PUT /api/v1.0/orders/{id}` with the corrected values. parameters: - name: id in: path required: true description: Order id to be refunded. schema: type: string - name: refundedAmount in: query description: Amount to be refunded. schema: type: string responses: '200': description: Order refunded successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/calls: get: tags: [Calls] summary: Retrieve Calls description: | > [!NOTE] > **Required role** — Get Calls Search and retrieve calls by date, email, lead id, product tag or id. parameters: - name: ids in: query description: Array of call ids. Maximum of 50 ids. schema: type: string - name: emails in: query description: Array of emails or prefixes. Maximum of 50 emails. schema: type: string - name: leadIds in: query description: Array of leadIds. Maximum of 50 lead ids. schema: type: string - name: productTags in: query description: Array of product tags. Maximum of 20 product tags. schema: type: string - name: fromDate in: query description: ISO 8601 date. Only calls after this date will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or later than toDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: toDate in: query description: ISO 8601 date. Only calls before this date will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or earlier than fromDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: pageSize in: query description: Maximum number of calls per page. Range 1-250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query description: ID of the next page, taken from the `nextPageId` of a previous response. Omit it to fetch the first page. An invalid or expired pagination cursor is rejected with `400 Bad Request`. schema: type: string - name: qualified in: query description: Filter by qualified status. schema: type: boolean - name: qualificationStages in: query description: Array of qualification stage names. Maximum of 50 qualification stages. schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/Call' nextPageId: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: [Calls] summary: Create Call description: | > [!NOTE] > **Required role** — Create Calls > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~10 seconds. Create a call with all necessary information. Additionally, creates the lead if not already present. requestBody: required: true content: application/json: schema: type: object required: [name, email] properties: name: type: string description: Name of the call. email: type: string description: Email associated with the lead. firstName: type: string lastName: type: string leadIps: type: array items: type: string stage: type: string phoneNumbers: oneOf: - type: string - type: array items: type: string externalId: type: string description: Unique identifier from the external integration. If a call with the same externalId exists, it will be updated. id: type: string description: "DEPRECATED: Use externalId instead." date: type: string description: ISO 8601 date when the call was processed. Without a time zone offset the date is stored as UTC; see **Date Formats**. example: '2021-04-16T20:35:00' qualified: type: boolean description: "DEPRECATED: Use state instead." qualification: type: string description: Custom name of the qualification to apply to the call. state: $ref: '#/components/schemas/CallState' example: name: Call 1 email: john@doe.com firstName: John lastName: Doe leadIps: [] stage: SQL phoneNumbers: [] date: '2021-04-16T20:35:00' qualification: Qualified state: QUALIFIED responses: '200': description: Call created successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. Also returned when the request carries a blacklisted email address, with the message `The event was discarded because an email or phone number it carries is blacklisted for this account. This is set when a lead is permanently deleted.` No `request_id` is issued in that case, so there is nothing to poll. See *Delete Lead*. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' put: tags: [Calls] summary: Update Calls description: | > [!NOTE] > **Required role** — Update Calls > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~5 minutes. Update calls by their ids. parameters: - name: ids in: query description: Array of call ids to update. Provide either `ids` or `externalIds`, not both. Maximum of 50 ids. schema: type: string - name: externalIds in: query description: Array of call externalIds to update. Provide either `ids` or `externalIds`, not both. Maximum of 50 external ids. schema: type: string - name: name in: query required: true description: Call name to assign. schema: type: string - name: qualification in: query description: Call qualification to assign. schema: type: string - name: state in: query description: Call state to assign. schema: $ref: '#/components/schemas/CallState' - name: qualified in: query description: "DEPRECATED: Use state instead." schema: type: boolean responses: '200': description: Calls updated successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/calls/{id}: delete: tags: [Calls] summary: Delete Call description: | > [!NOTE] > **Required role** — Delete Calls > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~5 minutes. Delete a call by its id. parameters: - name: id in: path required: true description: Call id to be deleted. schema: type: string responses: '200': description: Call deleted successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/conversion-paths: get: tags: [Conversion Paths] summary: Retrieve Conversion Paths description: | > [!NOTE] > **Required role** — Get Conversion Paths Retrieves the conversions of one type in a date range, each one with the ordered source touches it was attributed with. Use it to run your own attribution model over Hyros data, with your own weights, decay curve or exclusions. A page carries the paths of up to 250 conversions, so a month of sales takes a handful of calls instead of one `GET /api/v1.0/leads/journey` call per lead. `conversionType` decides both what a row is and which date the range filters on. For `SALE` and `CALL` the range applies to the sale date, and `path` is the touches the conversion was attributed with, up to that sale date. For `LEAD` the range applies to the join date, and `path` is the lead's full touch list. Repeat purchases are included: `firstSale` is what tells them apart from the lead's first sale (or first call). Every entry of `path` has the same shape as the `firstSource` and `lastSource` objects of `/sales`, `/calls` and `/leads`, and the entries are ordered by `clickDate` ascending. `path` can come back empty when the conversion has no touch on record. `SALE` and `CALL` rows carry `price`, `usdPrice` and `firstSale`; `LEAD` rows do not have those keys at all. The endpoint always pages over every conversion in the range, so there is no per-conversion selector: sending the shared `ids` parameter is rejected with a `400`. parameters: - name: conversionType in: query required: true description: > The kind of conversion to return. The date range applies to the sale date for `SALE` and `CALL`, and to the join date for `LEAD`. schema: type: string enum: [SALE, CALL, LEAD] - name: fromDate in: query description: > ISO 8601 date, either `yyyy-MM-dd` or a full timestamp. Only conversions more recent than this will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or later than toDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: toDate in: query description: > ISO 8601 date, either `yyyy-MM-dd` or a full timestamp. Only conversions older than this will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or earlier than fromDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: windowAttributionDaysRange in: query description: > Attribution window in days (0-365, default 0 = no window). A touch that happened more than this many days before the conversion is left out of `path`. The stored path already reflects the account's attribution timeframe at tracking time, so the window can only narrow it. schema: type: integer minimum: 0 maximum: 365 default: 0 - name: pageSize in: query description: Maximum number of conversions per page. Range 1-250. schema: type: integer minimum: 1 maximum: 250 default: 50 - name: pageId in: query description: ID of the next page, taken from the `nextPageId` of a previous response. Omit it to fetch the first page. An invalid or expired pagination cursor is rejected with `400 Bad Request`. schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/ConversionPath' nextPageId: type: string request_id: type: string examples: sales: summary: Two sales of the same lead, the second one a repeat purchase value: result: - id: sle-db882bfed1b5280735eb03ac5b9be0c6edf04d7a52204d41f0c6cf6c28ad5223 leadId: 6e7f85e73a49189f2cb30cc255304adb4d3b20a0bc26776a29bf2a861602078a creationDate: '2026-08-14T12:33:30-05:00' price: price: 49.99 discount: 0 hardCost: 0 refunded: 0 currency: USD usdPrice: price: 49.99 discount: 0 hardCost: 0 refunded: 0 currency: USD firstSale: true path: - sourceLinkId: slk-8e5c10ea09ed899f9e7a386501b8003e7fe8a0429633b0c1bf647ce92f51433a name: testing tag: '@testing' disregarded: false organic: false clickDate: '2026-08-02T09:14:07-05:00' trafficSource: id: cat-cc4342fa4567fc3611afeb888e31eba2 name: facebook goal: id: cat-909ba403c207fbdfb6344b96f3294342 name: all category: id: cat-b83d72b83a742f9bd8d93a2403ba3809 name: hyros test campaign 2 adSource: platform: FACEBOOK adSourceId: '23843648148170123' adAccountId: '887874684917514' sourceLinkAd: name: summer promo video adSourceId: '23843648148170123' - sourceLinkId: slk-12cc9f521ba37e33d624927ea7088d91a6b2e39bf1466c281120cde22a4bee84 name: newsletter tag: '@newsletter' disregarded: false organic: true clickDate: '2026-08-13T18:41:52-05:00' trafficSource: id: cat-5f0a2f5e7c1d4b8a9e3f6c2d1b0a9e8d name: email goal: id: cat-909ba403c207fbdfb6344b96f3294342 name: all category: id: cat-7d3c1b9a5e4f2d8c6b0a3f1e9d7c5b3a name: august newsletter - id: sle-4f7d2c8e6b3a1f0d9e8c7b6a5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9e8d7c6b leadId: 6e7f85e73a49189f2cb30cc255304adb4d3b20a0bc26776a29bf2a861602078a creationDate: '2026-08-27T08:02:11-05:00' price: price: 149 discount: 0 hardCost: 0 refunded: 0 currency: USD usdPrice: price: 149 discount: 0 hardCost: 0 refunded: 0 currency: USD firstSale: false path: - sourceLinkId: slk-12cc9f521ba37e33d624927ea7088d91a6b2e39bf1466c281120cde22a4bee84 name: newsletter tag: '@newsletter' disregarded: false organic: true clickDate: '2026-08-13T18:41:52-05:00' trafficSource: id: cat-5f0a2f5e7c1d4b8a9e3f6c2d1b0a9e8d name: email goal: id: cat-909ba403c207fbdfb6344b96f3294342 name: all category: id: cat-7d3c1b9a5e4f2d8c6b0a3f1e9d7c5b3a name: august newsletter nextPageId: ae910fcb3d22e2b89bd69dbc92ed2ad6836f6a611cf0a5e53082222fe1055eb6 request_id: cee7451ac5aa4dea84a10435d30ad111 leads: summary: A lead, with no price and no firstSale value: result: - id: 40b5af5444756c2b5e666fcb658affd2a4b455bce3711c43f88763147381e368 leadId: 40b5af5444756c2b5e666fcb658affd2a4b455bce3711c43f88763147381e368 creationDate: '2026-08-03T22:14:07-05:00' path: - sourceLinkId: slk-7bc217f372eab0efa188bb5b05be87460976e68e03b801d53c39bb4ed54c9750 name: Facebook Adset tag: '@facebook-adset' disregarded: false organic: false clickDate: '2026-08-03T22:14:07-05:00' trafficSource: id: cat-cc4342fa4567fc3611afeb888e31eba2 name: facebook goal: id: cat-909ba403c207fbdfb6344b96f3294342 name: all category: id: cat-b83d72b83a742f9bd8d93a2403ba3809 name: hyros test campaign 2 adSource: platform: FACEBOOK adSourceId: '23843648148170123' adAccountId: '887874684917514' nextPageId: 568fd86587125f55117505312dc72bb8b71e9647a25e5b142d3f28bccd228360 request_id: 5696f9a3524e42318f3cdf40176e70e3 '400': description: > Bad Request. A missing or invalid `conversionType`, an `ids` parameter, a `windowAttributionDaysRange` outside 0-365, an invalid date format, or an invalid `pageSize`/`pageId`. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: missingConversionType: summary: conversionType was not sent value: result: ERROR message: ['Missing required parameter: conversionType'] invalidConversionType: summary: conversionType is not one of the allowed values value: result: ERROR message: ['Invalid value for parameter conversionType. Allowed values: SALE, CALL, LEAD'] idsNotSupported: summary: The shared ids parameter was sent value: result: ERROR message: ['The ids parameter is not supported by this endpoint'] invalidWindow: summary: windowAttributionDaysRange is outside 0-365 value: result: ERROR message: ['Invalid days range for window of attribution: 400'] invalidPageId: summary: The pagination cursor is invalid or expired value: result: ERROR message: ['Invalid pageId'] '403': description: > The Api-Key does not carry the `Get Conversion Paths` role. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: result: ERROR message: ['Forbidden, missing api key roles', 'Get Conversion Paths'] /api/v1.0/attribution: get: tags: [Attribution] summary: Get Ads Attribution Report description: | > [!NOTE] > **Required role** — Get Attribution Retrieves Facebook AdSet or Google Campaign attribution information. **Notes:** - When `isAdAccountId` is `true` and `timeGroupingOption` is `day`, `week`, `month`, or `year`, the request will fail. - When `status` is `active` or `paused` and `timeGroupingOption` is not `source_link`, the request will fail. - When `timeGroupingOption` is not one of `source_link`, `day`, `week`, `month` or `year`, the request will fail with `Invalid timeGroupingOption: . Accepted values: source_link, day, week, month, year`. - `keywordsIds` is required at the `google_keyword` and `google_v2_keyword` levels, both of which report at the keyword level. Either of them without it fails the request. - `isAdAccountId` is not supported at the keyword level, and sending it there fails the request: the ids reported on are derived from `keywordsIds`, so the ad account id is discarded. Pass `keywordsIds` without `isAdAccountId`. - When `isAdAccountId` is `true`, the response rows follow the order the account's sources were paginated in, which `newestFirst` controls. - Ids that do not exist at the requested `level` are skipped rather than failing the request: the ids that do resolve are still reported on, and the ones that do not are named in `message`. An id that exists but has no data for the range is not named there, so an empty `result` with no such `message` means every id exists and none of them reported anything for the range. parameters: - name: attributionModel in: query required: true description: Attribution model. schema: type: string enum: [last_click, scientific, first_click] - name: startDate in: query required: true description: An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date. If the date does not include a timezone, the configured account timezone will be assumed. Both bounds must either carry an offset or both omit it. schema: type: string example: '2020-05-12T10:00:00' - name: endDate in: query required: true description: An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date. If the date does not include a timezone, the configured account timezone will be assumed. Both bounds must either carry an offset or both omit it. schema: type: string example: '2021-04-13T10:00:00' - name: level in: query required: true description: | Attribution level for the report. `google_adgroup`, `google_v2_adgroup`, `google_keyword` and `google_v2_keyword` are only available on accounts with the Google v2 integration. schema: type: string enum: - google_campaign - google_v2_campaign - google_adgroup - google_v2_adgroup - google_ad - google_v2_ad - google_keyword - google_v2_keyword - facebook_adset - facebook_campaign - tiktok_adgroup - snapchat_adsquad - pinterest_adgroup - twitter_adgroup - bing_adgroup - linkedin_campaign - facebook_ad - tiktok_ad - snapchat_ad - pinterest_ad - twitter_ad - bing_ad - linkedin_ad - name: fields in: query required: true description: | Comma-separated fields to include in the report. Available values: `sales`, `revenue`, `calls`, `total_revenue`, `recurring_revenue`, `refund`, `unique_sales`, `leads`, `new_leads`, `cost`, `profit`, `roi`, `roas`, `refund_count`, `refund_sales_percentage`, `refund_revenue_percentage`, `cost_per_call`, `cost_per_lead`, `cost_per_sale`, `cost_per_new_lead`, `cost_per_unique_sale`, `unique_customers`, `unique_customers_revenue`, `cost_per_unique_customer`, `net_profit`, `hard_costs`, `qualified_calls`, `unqualified_calls`, `cost_per_qualified_call`, `time_of_sale_attribution`, `time_of_call_attribution`, `clicks`, `new_visits`, `cost_per_new_visit`, `cost_per_click`, `reported`, `reported_result`, `shop_reported_result`, `reported_vs_revenue`, `new_customers_percentage`, `recurring_customers`, `total_customers`, `customers`, `ctr`, `cpm`, `cvr`, `impressions`, `gross_margins`, `partial_video_views`, `unique_calls`, `canceled_calls`, `cost_per_unique_call`, `net_profit_percentage`, `taxes`, `cost_of_goods`, `shipping_value`, `30_days_ltv`, `60_days_ltv`, `90_days_ltv`, `6_months_ltv`, `1_year_ltv`, `30_days_ltv_forecast`, `60_days_ltv_forecast`, `90_days_ltv_forecast`, `6_months_ltv_forecast`, `1_year_ltv_forecast`, `churn_rate`, `one_time_sales`, `subscription_30_days_forecast`, `subscription_60_days_forecast`, `subscription_90_days_forecast`, `subscription_6_months_forecast`, `subscription_1_year_forecast`, `cac`, `aov`, `new_subscriptions`, `canceled_subscriptions`, `direct_subscriptions`, `new_mrr`, `new_trials`, `converted_trials`, `canceled_trials`, `cost_per_new_subscriptions`, `cost_per_new_trials`, `carts`, `atc_events`, `purchased_carts`, `atc_cvr`, `atc_rate`, `cost_per_atc`, `name`, `parent_name`, `contribution_profit`, `contribution_margin`, `leads_optins`, `no_show_calls`, `new_customers_roas`, `new_customers_aov`, `new_customers_orders`, `recurring_customers_percentage`, `returning_customers`, `returning_customers_rate`, `returning_customers_revenue`, `returning_customers_revenue_rate`, `mrr`, `arr`, `cost_per_unique_sales`, `refunded_sales_percentage`, `refunded_revenue_percentage` `unique_customers` counts the distinct leads whose purchase in the row is their first ever, the metric the account's report screens call New Customers. A repeat buyer, a subscription rebill and a lead imported as an existing customer all bring revenue without raising it, while `revenue` carries no first-purchase condition, so a row can report revenue with `unique_customers` at `0`. `revenue` covers one-time sales only: rebills land in `recurring_revenue`, and `total_revenue` and `roas` are the ones that count both. `unique_customers_revenue` is the part of the revenue those first purchases brought, and `returning_customers` is `customers` minus `unique_customers` rather than a direct count of prior buyers. `cac` is null when the row has spend but no `unique_customers`. A `0` means the customers came without ad spend. `roas`, `new_customers_roas` and `roi` are `null` when the row has no ad spend, so a `0` on any of them always reflects real spend. schema: type: string example: 'sales,revenue,calls,cost,roi,roas' - name: ids in: query required: true description: Comma-separated ids based on the `level`. They must exist at the given `level`; the ones that do not are skipped. Can be an ad account id when `isAdAccountId` is true (only one allowed). Maximum of 50 ids. Not required at the keyword level, where the ids reported on are derived from `keywordsIds`. schema: type: string example: '205044496234,205044496235' - name: keywordsIds in: query description: Map of ad group ids and keywords. Required for the `google_keyword` and `google_v2_keyword` levels, and only used by them. At those levels the ids reported on are derived from it. schema: type: string example: '66457534290:[391764277422,10000010]' - name: currency in: query schema: type: string enum: [usd, user_currency] default: user_currency - name: dayOfAttribution in: query description: If true, filters by click date instead of sale date. schema: type: boolean default: false - name: scientificDaysRange in: query description: Day range (1-30) for first ad attribution. Used with scientific model. schema: type: integer minimum: 1 maximum: 30 default: 30 - name: sourceConfiguration in: query description: > How organic and paid sources are handled. `prioritize_paid`, the default, discards organic clicks before attributing, so a sale closed by an organic click is credited to its previous paid click. `all_sources` credits the source that closed the sale, organic or paid. schema: type: string enum: [all_sources, only_organic, only_paid, prioritize_organic, prioritize_paid] default: prioritize_paid - name: excludeHardCosts in: query description: > When `true`, hard costs (taxes, shipping and cost of goods) are subtracted from the reported revenue, the way the report screens in the app do when they exclude hard costs. When `false`, revenue is reported before hard costs. schema: type: boolean default: false - name: ignoreRecurringSales in: query schema: type: boolean default: false - name: isAdAccountId in: query description: If true, the id in `ids` is an ad account ID. All sources of that account will be paginated and reported on at the selected `level`. Not supported at the `google_keyword` and `google_v2_keyword` levels. schema: type: boolean default: false - name: forecastingOption in: query schema: type: string enum: [first_sale, total_sales] default: first_sale - name: windowAttributionDaysRange in: query description: Days range for discard attribution (0-365). schema: type: integer minimum: 0 maximum: 365 default: 0 - name: newCustomerConfiguration in: query description: | Filters the report's sales by whether the sale is the customer's first purchase. `only_unique_customers` keeps only those first purchases, `only_returning_customers` keeps everything else, and the default `all_customers` counts both. Sales, revenue and ROAS are narrowed by it, ad spend and leads are not, so under `only_unique_customers` ROAS divides filtered revenue by full spend. It keys on the first-purchase flag alone, so unlike the `unique_customers` metric it does not exclude leads marked as existing customers. schema: type: string enum: [all_customers, only_returning_customers, only_unique_customers] - name: status in: query description: Filter ad spend by status. Only supported when `timeGroupingOption` is `source_link`. schema: type: string enum: [active, paused] - name: timeGroupingOption in: query description: > Defines how the response will be grouped. With `source_link` every row is one of the requested sources. With `day`, `week`, `month` or `year` the report is segmented by date instead: each row is a date bucket covering every requested source, identified by `id` (the bucket's date, `YYYY-MM-DD`), `start_date` and `end_date`, and labelled by `name`. schema: type: string enum: [source_link, day, week, month, year] default: source_link - name: lead_stage in: query description: Filters the report to the leads and sales whose lead's current stage is any of the given account lead stages, by stage name (case-insensitive), comma-separated (e.g. `mql,sql,customer`), so a lead that has since moved on no longer counts. Leads, sales, revenue and ROAS are all narrowed by it, ad spend is not, so cost per lead and cost per acquisition rise against the filtered counts. An unknown stage name returns a 400 listing the account's stage names, which `GET /stages` also lists. schema: type: string - name: newestFirst in: query description: Reads the ad account's sources newest first, ordering them by creation, so the first page holds the most recently created sources instead of the oldest ones. Only applies when `isAdAccountId` is `true`. schema: type: boolean default: false - name: pageSize in: query description: Maximum number of sources per page. Range 1-250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: request_id: type: string result: type: array items: type: object additionalProperties: true example: request_id: d3d80491c48140b89a23071d19c0f88f result: - id: '205044496234' revenue: 997.00 sales: 10 calls: 5 campaign_id: '23843648123456789' reported_result: null '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: invalidAttributionModel: summary: attributionModel is not one of the models this report serves value: result: ERROR message: ['Invalid attributionModel: depreciation. Accepted values: last_click, first_click, scientific'] invalidTimeGroupingOption: summary: timeGroupingOption is not one of the accepted values value: result: ERROR message: ['Invalid timeGroupingOption: ad_account. Accepted values: source_link, day, week, month, year'] /api/v1.0/attribution/roas: get: tags: [Attribution] summary: Get ROAS description: | > [!NOTE] > **Required role** — Get ROAS Retrieves the cash actually collected by the entity a single `id` identifies at the given `level`, against its ad spend. A narrow preset over `GET /api/v1.0/attribution`: the reporting level is resolved from the id and the requested level, so the platform never has to be supplied and the caller does not need to know which reporting level each platform's ads, ad sets or campaigns report at. The metric set, attribution model and row grouping are fixed. Rebills are already counted inside `total_revenue`, and sales are credited to the click that drove them however long they took to land. `roas` is revenue over spend, so break-even is `1.0` rather than `0`. `roas` and `new_customers_roas` are `null` when the row has no ad spend, so a `0` always reflects real spend. Every metric of the fixed set is always present in `result`, `null` where the row holds no value. **Attribution model.** This endpoint always measures under `last_click`, and the model is not selectable. That matters for reading the number: under `last_click` a sale is credited to the entity whose click came *last* before the purchase, so the same entity over the same range would report a different `roas` under a first-click model. Use `GET /api/v1.0/attribution` instead to choose the attribution model, to pick reporting levels or metrics, to group results by date, or to report on several entities at once. **Notes:** - `id` and `level` are both required, and the id must belong to the level given. - Campaign-level reporting is available on Meta, Google and LinkedIn. The remaining platforms have no campaign level, so report those at the `ad` or `source_link` level. - An id that does not belong to the account returns a `400` rather than a report of zeros. parameters: - name: id in: query required: true description: Id of the entity to report on, as the ad platform issues it. It must belong to the given `level`. schema: type: string example: '23851234567890123' - name: level in: query required: true description: | Granularity the id refers to. - `ad`: a single ad. - `source_link`: an ad set on Meta, an ad group elsewhere. The level most tracked data sits at. - `campaign`: a campaign, whose contained ads are resolved and aggregated. - `account`: a whole ad account. schema: type: string enum: [ad, source_link, campaign, account] example: source_link - name: startDate in: query required: true description: An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date. If the date does not include a timezone, the configured account timezone will be assumed. Both bounds must either carry an offset or both omit it. schema: type: string example: '2020-05-12' - name: endDate in: query required: true description: An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date. If the date does not include a timezone, the configured account timezone will be assumed. Both bounds must either carry an offset or both omit it. schema: type: string example: '2020-05-19' - name: basis in: query description: | Which date the range filters on. - `click_date`: credits the clicks made inside the range, however long their sales took to land afterwards. Since the endpoint measures under `last_click`, the click it dates a sale by is the *last* one before the purchase. - `sale_date`: counts only the revenue collected inside the range. schema: type: string enum: [click_date, sale_date] default: click_date responses: '200': description: Successful response. content: application/json: schema: type: object properties: request_id: type: string result: type: object additionalProperties: true description: The single entity reported on. Absent when it has no row for the range. example: request_id: cb0e1b0e5a7f4c0b9f0e1b0e5a7f4c0b result: id: '23851234567890123' roas: 3.42 new_customers_roas: 2.81 revenue: 8200.00 recurring_revenue: 1350.00 total_revenue: 9550.00 cost: 2792.40 unique_sales: 74 unique_customers: 61 reported_result: 88 reported_vs_revenue: 1120.00 '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/attribution/marginal-cac-curve: get: tags: [Attribution] summary: Get Marginal CAC Curve description: | Computes, from the account's own daily spend history, what happens to the cost of acquiring the next customer at every observed daily spend level for the entity a single `id` identifies at the given `level` — and past which spend level the next dollar is wasted. Days in the range are grouped into equal-count buckets by ascending daily spend; each bucket carries its average CAC and the marginal CAC versus the previous cheaper bucket (the incremental spend per incremental customer). The saturation point is the first spend level from which every remaining level is wasteful — its marginal CAC exceeds the ceiling, or the extra spend produced no extra customers. Degraded data returns a partial curve explained through `notes` rather than an error. **Attribution model.** This endpoint always measures on the click date and with rebills included, and defaults to `first_click`: a customer is credited to the first ad that touched them, so each daily point relates a spend level to the acquisitions it caused and recurring revenue rolls up to the acquiring ad — a cost-of-acquisition curve. Passing `attributionModel=last_click` credits the customer to the click that immediately preceded the purchase instead, turning the result into a cost-of-*closing* curve: what it costs the entity to close the next customer. Use it for retargeting and bottom-of-funnel entities, whose real contribution first-click hands over to whichever ad acquired the customer earlier. Read a prospecting entity that way and the curve distorts, since most of the customers it acquired were closed elsewhere. **The ceiling.** Defaults to the entity's realized LTV for the requested window — break-even, with no margin — and `ceilingBasis` states which rule produced it. Note the realized LTV groups customers by what they bought, not by how they retain, so two traffic sources whose customers buy the same products report the same LTV even if their rebill behavior differs. At the `account` level no LTV is available, so the ceiling is the caller's `cacCeiling` or none. **Notes:** - `id` and `level` are both required, and the id must belong to the level given. - Campaign-level reporting is available on Meta, Google and LinkedIn. The remaining platforms have no campaign level, so report those at the `ad` or `source_link` level. - An id that does not belong to the account returns a `400` rather than a curve of zeros. - Customers are attributed to their click date, so the most recent days may undercount customers whose conversions have not happened yet. parameters: - name: id in: query required: true description: Id of the entity to report on, as the ad platform issues it. It must belong to the given `level`. schema: type: string example: '23851234567890123' - name: level in: query required: true description: | Granularity the id refers to. - `ad`: a single ad. - `source_link`: an ad set on Meta, an ad group elsewhere. The level most tracked data sits at. - `campaign`: a campaign, whose contained ads are resolved and aggregated. - `account`: a whole ad account. Day-grouped ranges are capped at 90 days on this level. schema: type: string enum: [ad, source_link, campaign, account] example: source_link - name: startDate in: query description: ISO 8601 starting date of the history. Defaults to 90 days before `endDate`. If the date does not include a timezone, the configured account timezone will be assumed. Both bounds must either carry an offset or both omit it. A defaulted bound never carries an offset, so send both bounds when you send one. schema: type: string example: '2020-05-12' - name: endDate in: query description: ISO 8601 ending date of the history. Defaults to today. If the date does not include a timezone, the configured account timezone will be assumed. Both bounds must either carry an offset or both omit it. A defaulted bound never carries an offset, so send both bounds when you send one. schema: type: string example: '2020-08-10' - name: ltvWindow in: query description: | Realized LTV window used as the break-even ceiling when `cacCeiling` is absent. Rejected at the `account` level, which carries no LTV — provide `cacCeiling` there instead. schema: type: string enum: ['30_days', '60_days', '90_days', '6_months', '1_year'] default: '90_days' - name: cacCeiling in: query description: | Maximum acceptable cost to acquire one customer, chosen by the caller (e.g. a fraction of LTV that preserves margin). Overrides the LTV break-even ceiling. Must be zero or positive. schema: type: number example: 52 - name: attributionModel in: query description: | Which model credits a customer to the entity. - `first_click`: the customer belongs to the first ad that touched them — a cost-of-acquisition curve, and the way to read prospecting entities. - `last_click`: the customer belongs to the click that immediately preceded the purchase — a cost-of-closing curve, and the way to read retargeting and bottom-of-funnel entities. schema: type: string enum: [first_click, last_click] default: first_click responses: '200': description: | Successful response. `curve` is ordered by ascending daily spend level; `saturationPoint` is null when saturation was not reached in the observed range. `notes` explains a partial curve: `NO_SPEND_DATA`, `NO_CUSTOMERS`, `INSUFFICIENT_DATA`, `LTV_CEILING_UNAVAILABLE`. `ceilingBasis` is `CALLER_PROVIDED` or `LTV_BREAKEVEN`, and `ltvWindow` is null unless the ceiling came from the LTV break-even rule. `name` is null at the `account` level, whose rows are date ranges rather than a named entity. content: application/json: schema: type: object properties: request_id: type: string result: type: object additionalProperties: true description: The curve computed for the entity reported on. example: request_id: cb0e1b0e5a7f4c0b9f0e1b0e5a7f4c0b result: id: '23851234567890123' level: SOURCE_LINK name: YT-LongForm-Founder startDate: '2020-05-12' endDate: '2020-08-10' attributionModel: FIRST_CLICK daysSampled: 84 cacCeiling: 52.00 ceilingBasis: CALLER_PROVIDED ltvWindow: null curve: - spendPerDay: 812.40 days: 28 newCustomers: 594 avgCac: 38.29 marginalCac: null - spendPerDay: 1490.10 days: 28 newCustomers: 941 avgCac: 44.33 marginalCac: 54.68 - spendPerDay: 2274.75 days: 28 newCustomers: 1088 avgCac: 58.54 marginalCac: 149.46 saturationPoint: efficientSpendPerDay: 1490.10 saturatedSpendPerDay: 2274.75 reason: MARGINAL_CAC_ABOVE_CEILING notes: [] '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/attribution/ad-account: get: tags: [Attribution] summary: Get Ad Accounts Attribution Report description: | > [!NOTE] > **Required role** — Get Ad Account Attribution Retrieves Ad account attribution information. **Notes:** - When `adLevelDateGroupingOption` is not one of `ad_account`, `day`, `week`, `month` or `year`, the request will fail with `Invalid adLevelDateGroupingOption: . Accepted values: ad_account, day, week, month, year`. parameters: - name: attributionModel in: query required: true schema: type: string enum: [last_click, scientific, first_click] - name: startDate in: query required: true description: An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date. If the date does not include a timezone, the configured account timezone will be assumed. Both bounds must either carry an offset or both omit it. schema: type: string example: '2020-05-12T10:00:00' - name: endDate in: query required: true description: An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date. If the date does not include a timezone, the configured account timezone will be assumed. Both bounds must either carry an offset or both omit it. schema: type: string example: '2021-04-13T10:00:00' - name: fields in: query required: true description: > Comma-separated fields to include in the report (same set as Ad Attribution endpoint). `unique_customers` counts the distinct leads whose purchase in the row is their first ever, the metric the account's report screens call New Customers. A repeat buyer, a subscription rebill and a lead imported as an existing customer all bring revenue without raising it, while `revenue` carries no first-purchase condition, so a row can report revenue with `unique_customers` at `0`. `revenue` covers one-time sales only: rebills land in `recurring_revenue`, and `total_revenue` and `roas` are the ones that count both. `unique_customers_revenue` is the part of the revenue those first purchases brought, and `returning_customers` is `customers` minus `unique_customers` rather than a direct count of prior buyers. `cac` is null when the row has spend but no `unique_customers`. A `0` means the customers came without ad spend. `roas`, `new_customers_roas` and `roi` are `null` when the row has no ad spend, so a `0` on any of them always reflects real spend. schema: type: string - name: ids in: query required: true description: The Ad Account id. Maximum of 1 id. schema: type: string example: '205044496234' - name: currency in: query schema: type: string enum: [usd, user_currency] default: user_currency - name: dayOfAttribution in: query schema: type: boolean default: false - name: scientificDaysRange in: query schema: type: integer minimum: 1 maximum: 30 default: 30 - name: sourceConfiguration in: query description: > How organic and paid sources are handled. `prioritize_paid`, the default, discards organic clicks before attributing, so a sale closed by an organic click is credited to its previous paid click. `all_sources` credits the source that closed the sale, organic or paid. schema: type: string enum: [all_sources, only_organic, only_paid, prioritize_organic, prioritize_paid] default: prioritize_paid - name: adLevelDateGroupingOption in: query description: > Defines how the response will be grouped. With `ad_account`, the default, the response is a single row covering the whole date range. With `day`, `week`, `month` or `year` the report is segmented by date instead: each row is a date bucket identified by `id` (the bucket's date, `YYYY-MM-DD`), `start_date` and `end_date`, all in your account's timezone, and labelled by `name`. The first and last buckets are bounded by `startDate` and `endDate`, so additive metrics such as `sales` and `revenue` add up to the `ad_account` row while ratios such as `roas` do not. schema: type: string enum: [ad_account, day, week, month, year] default: ad_account - name: excludeHardCosts in: query description: > When `true`, hard costs (taxes, shipping and cost of goods) are subtracted from the reported revenue, the way the report screens in the app do when they exclude hard costs. When `false`, revenue is reported before hard costs. schema: type: boolean default: false - name: reportSourceVisibility in: query description: > When `true`, the account's source links are resolved with the visibility rules the report screens in the app use: deleted source links are excluded and, under the `scientific` model, link activity is evaluated from `scientificDaysRange` days before `startDate`. When `false`, deleted links count and activity is evaluated over the raw date range. schema: type: boolean default: false - name: ignoreRecurringSales in: query schema: type: boolean default: false - name: forecastingOption in: query schema: type: string enum: [first_sale, total_sales] default: first_sale - name: windowAttributionDaysRange in: query schema: type: integer minimum: 0 maximum: 365 default: 0 - name: newCustomerConfiguration in: query description: | Filters the report's sales by whether the sale is the customer's first purchase. `only_unique_customers` keeps only those first purchases, `only_returning_customers` keeps everything else, and the default `all_customers` counts both. Sales, revenue and ROAS are narrowed by it, ad spend and leads are not, so under `only_unique_customers` ROAS divides filtered revenue by full spend. It keys on the first-purchase flag alone, so unlike the `unique_customers` metric it does not exclude leads marked as existing customers. schema: type: string enum: [all_customers, only_returning_customers, only_unique_customers] responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: type: object additionalProperties: true request_id: type: string example: result: - id: '205044496234' sales: 64 leads: 0 total_revenue: 9283.41 cost: 9172.25 cost_per_sale: 143.32 start_date: '2020-05-12T10:00:00' end_date: '2021-04-13T10:00:00' reported_result: null request_id: d9031a3780c14ecdb8ca3af5cd26bc91 '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: invalidAttributionModel: summary: attributionModel is not one of the models this report serves value: result: ERROR message: ['Invalid attributionModel: depreciation. Accepted values: last_click, first_click, scientific'] invalidAdLevelDateGroupingOption: summary: adLevelDateGroupingOption is not one of the accepted values value: result: ERROR message: ['Invalid adLevelDateGroupingOption: source_link. Accepted values: ad_account, day, week, month, year'] /api/v1.0/ad-accounts: get: tags: [Ad Accounts] summary: List Ad Accounts description: | > [!NOTE] > **Required role** — Get Ad Accounts List the ad accounts connected to your account. Use the returned `id` values as the ad account id for the Ad Attribution endpoint (with `isAdAccountId=true`) and the Ad Account Attribution endpoint. Results are paginated: each response includes a `nextPageId`; pass it back as `pageId` to retrieve the following page. The last page has a `null` `nextPageId`. parameters: - name: ids in: query required: false description: Comma-separated ad account ids to filter by. If omitted, all connected ad accounts are returned. Maximum of 50 ids. schema: type: string example: '205044496234,118822334455' - name: fields in: query required: false description: Comma-separated fields to include in each result. If omitted, all fields are returned. schema: type: string enum: [name, type] example: 'name,type' - name: pageSize in: query required: false description: The maximum number of ad accounts to return in a single page. Defaults to 50. schema: type: integer minimum: 1 maximum: 250 default: 50 - name: pageId in: query required: false description: The id of the next page to retrieve, taken from the `nextPageId` of a previous response. Any change to the other parameters resets pagination. schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: request_id: type: string nextPageId: type: string nullable: true description: Cursor for the next page, or null on the last page. result: type: array items: type: object properties: id: type: string name: type: string type: type: string example: request_id: 43573923369e40bbafd46925a5c15ff5 nextPageId: b8f1c2d3e4a5 result: - id: '205044496234' name: Acme - Meta Ads type: FACEBOOK - id: '118822334455' name: Acme - Google Ads type: GOOGLE '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/products: post: tags: [Products] summary: Create Product description: | > [!NOTE] > **Required role** — Create Products > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~10 seconds. Create a product with name, price and category. The tag is created from the product's name. requestBody: required: true content: application/json: schema: type: object required: [name, price] properties: name: type: string description: Name of the product. Must have at least 3 characters. price: type: number description: 'Plain number only: up to 8 digits before the decimal point and up to 2 after (max 99999999.99). Currency symbols and separators are not accepted.' category: type: string packages: type: array items: type: string description: Product packages (used for recurring sales attribution). example: name: Product 1 price: 5.66 category: Category 1 packages: ['Package 1', 'Package 2'] responses: '200': description: Product created successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' get: tags: [Products] summary: Retrieve Products description: | > [!NOTE] > **Required role** — Get Products Search and retrieve products by name, tag, category and recurrence. Results are paginated. parameters: - name: name in: query description: Filter products whose name equals this value (exact match). schema: type: string - name: tag in: query description: Filter products whose tag equals this value (exact match). The sale prefix "$" is added automatically when omitted. schema: type: string - name: category in: query description: Filter products by the name of their category. schema: type: string - name: isRecurringSale in: query description: Filter by recurring status. schema: type: string enum: [RECURRING, NON_RECURRING, ALL] default: ALL - name: pageSize in: query description: Maximum number of products per page. Range 1-250. schema: type: integer minimum: 1 maximum: 250 default: 50 - name: pageId in: query description: ID of the next page. Returned in `nextPageId` of each response. schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/Product' nextPageId: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/products/{id}: put: tags: [Products] summary: Update Product description: | > [!NOTE] > **Required role** — Update Products Update a product's fields. Only the fields included in the request are modified; any field left out is kept unchanged. parameters: - name: id in: path required: true description: Product id (its tracking pixel, as returned by the products listing). schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Name of the product. Must have at least 3 characters. price: type: number description: 'Plain number only: up to 8 digits before the decimal point and up to 2 after (max 99999999.99). Currency symbols and separators are not accepted.' customCost: type: number description: Cost of goods of the product, used in profit and ROAS reporting. tag: type: string sku: type: string category: type: string isRecurringSale: type: boolean description: Whether the product is a recurring sale. A product cannot be both a recurring sale and a call product. callProduct: type: boolean description: Whether the product is a call product. A product cannot be both a recurring sale and a call product. packages: type: array items: type: string description: Product packages (used for recurring sales attribution). Omit to keep the current packages unchanged; send an empty array to remove the product from all packages. updateHistoricalSales: type: boolean description: When true, a cost change is propagated to the product's existing sales, updating their profit and ROAS. default: false example: price: 7.99 customCost: 3.00 updateHistoricalSales: true responses: '200': description: Product updated successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' delete: tags: [Products] summary: Delete Product description: | > [!NOTE] > **Required role** — Delete Products Delete a product by its id. parameters: - name: id in: path required: true description: Product id (its tracking pixel, as returned by the products listing) to be deleted. schema: type: string responses: '200': description: Product deleted successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/tags: get: tags: [Tags] summary: List all Tags deprecated: true description: | > [!NOTE] > **Required role** — Get Tags Deprecated: to be replaced by GET /api/v1.0/tags/count. List all of your created tags. responses: '200': description: Successful response. content: application/json: schema: type: object properties: request_id: type: string result: type: array items: type: string example: request_id: 43573923369e40bbafd46925a5c15ff5 result: ['!tag1', '!tag2', '$sale1'] '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/tags/count: get: tags: [Tags] summary: List tags with lead counts description: | > [!NOTE] > **Required role** — Get Tags List your created tags along with the count of leads that have each tag. A lead that has several of your tags is counted once for each of those tags. parameters: - name: name in: query description: Exact tag name to filter by. schema: type: string - name: pageSize in: query description: Range 1-250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: type: object properties: name: type: string amount: type: integer nextPageId: type: string request_id: type: string example: result: - name: '!tag1' amount: 12 - name: '!tag2' amount: 0 - name: '$sale1' amount: 3 nextPageId: 1073e129b360b78db3508bea584d1f295c7851c3d9b290308ac57528b6e38a21 request_id: 43573923369e40bbafd46925a5c15ff5 '401': description: Unauthorized. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/sources: get: tags: [Sources] summary: List all Sources description: | > [!NOTE] > **Required role** — Get Sources Search and retrieve sources by name or tag, by organic/disregarded status and by adSpendType/adSpendId. parameters: - name: adSourceIds in: query description: Array of ad source ids to retrieve. Maximum of 50 ad source ids. schema: type: string - name: includeOrganic in: query schema: type: boolean - name: includeDisregarded in: query schema: type: boolean - name: integrationType in: query schema: $ref: '#/components/schemas/AdspendType' - name: name in: query description: Source name, matched in full and ignoring case. Partial names do not match. Combined with `tag`, both have to match. schema: type: string - name: tag in: query description: Source tag, matched in full and ignoring case. A tag is unique within the account, so at most one source is returned. Combined with `name`, both have to match. schema: type: string - name: pageSize in: query description: Between 1 and 250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/Source' nextPageId: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: [Sources] summary: Create Source description: | > [!NOTE] > **Required role** — Create Sources > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~10 seconds. Create a source. Can be organic, non-organic, or from ad platforms (Google/Facebook/etc). requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string category: type: string goal: type: string trafficSource: type: string isDisregard: type: boolean isOrganic: type: boolean integrationType: type: string enum: [GOOGLE, FACEBOOK, TIKTOK, SNAPCHAT, LINKEDIN] description: Subset of `AdspendType` accepted when creating a source. adSourceId: type: string description: | Id of the ad. Required if `integrationType` is present. - Facebook: adset id - Google: campaign id - TikTok: ad group id - Snapchat: ad squad id - LinkedIn: campaign id accountId: type: string description: Id of the ad account. Required if `integrationType` is present. adspendSubType: $ref: '#/components/schemas/AdspendSubType' description: Required when `integrationType` is GOOGLE. campaignId: type: string description: Required when `integrationType` is FACEBOOK. examples: organic: summary: Organic ad value: name: Organic ad 1 category: Instagram posts goal: opt ins trafficSource: organic isOrganic: true facebook: summary: Facebook ad value: name: Facebook adset 1 integrationType: FACEBOOK adSourceId: '238xxxxxxxx300546' accountId: '32xxxxxxxx93135' campaignId: '238xxxxxxxx300546' responses: '200': description: Source created successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/sources/{tag}: put: tags: [Sources] summary: Update Source description: | > [!NOTE] > **Required role** — Update Sources > [!NOTE] > **Asynchronous** — effect is not immediate (see note below). Edit an existing source, identified by its `tag`. Every field is optional: only the fields present in the request are changed, the rest are left untouched. Editing a source re-attributes its associated sales in the background. Note: the update is applied asynchronously. A `200` response means the request was accepted; the changes (and the re-attribution of associated sales) may take a few minutes to be reflected in `GET /api/v1.0/sources` and in reports. parameters: - name: tag in: path required: true description: The `tag` of the source to update, as returned by `GET /api/v1.0/sources`. schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: New name of the source. tag: type: string description: 'New tag to be assigned to the source (a new tag, or an existing one not used by another source). Must be a source tag: include the `@` prefix, e.g. `@my-source` (a value with no prefix is automatically prefixed with `@`). A tag with a non-source prefix (`$`, `!`, `#`) is rejected with `Invalid tag`.' category: type: string description: Name of the source category. Created if it does not exist. goal: type: string description: Name of the goal. Created if it does not exist. trafficSource: type: string description: Name of the traffic source. Created if it does not exist. isDisregard: type: boolean description: Whether the source is disregarded when attributing a sale. isOrganic: type: boolean description: Whether the source is marked as an organic source. example: name: Organic ad 1 (renamed) tag: '@organic-ad-1' category: Instagram posts isDisregard: true responses: '200': description: Source updated successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' delete: tags: [Sources] summary: Delete Source description: | > [!NOTE] > **Required role** — Delete Sources Delete an existing source, identified by its `tag`. The source is soft deleted and no longer appears in attribution. parameters: - name: tag in: path required: true description: The `tag` of the source to delete, as returned by `GET /api/v1.0/sources`. schema: type: string responses: '200': description: Source deleted successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/ads: get: tags: [Sources] summary: List all Ads description: | > [!NOTE] > **Required role** — Get Ads Search and retrieve ads by adSpendType/adSpendId. parameters: - name: integrationType in: query schema: $ref: '#/components/schemas/AdspendType' - name: adSourceIds in: query description: Array of ad source ids to retrieve. Maximum of 50 ad source ids. schema: type: string - name: pageSize in: query description: Between 1 and 250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: type: object properties: name: type: string adSource: $ref: '#/components/schemas/AdSource' source: $ref: '#/components/schemas/Source' creationDate: type: integer description: Returned as epoch milliseconds (a number) instead of ISO 8601. example: 1677151375000 nextPageId: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/url-rules: get: tags: [URL Rules] summary: List URL Rules description: | > [!NOTE] > **Required role** — Get URL Rules Retrieve the account's URL rules. Only simple rules are returned; built-in default rules are excluded. Use the returned opaque `id` (e.g. `ur-`) to retrieve, update or delete a rule. parameters: - name: ids in: query description: 'Array of opaque URL rule ids (as returned in the `id` field, e.g. `ur-`) to filter by. Maximum of 50 ids. An id not matching that format is rejected with `Invalid field format: ids`.' schema: type: string - name: tag in: query description: Return only the rule carrying this exact tag, including its prefix. schema: type: string - name: urlRuleActionType in: query description: >- Return only rules of this flavor. An unrecognized value is rejected with `Invalid urlRuleActionType ''. Accepted values: ACTION, SOURCE_LINK, SALE, SUBSCRIPTION, LEAD_STAGE, CALL_QUALIFICATION`. schema: type: string enum: [ACTION, SOURCE_LINK, SALE, SUBSCRIPTION, LEAD_STAGE, CALL_QUALIFICATION] - name: fromDate in: query description: ISO 8601 date. Only rules created on or after this date are retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or later than toDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: toDate in: query description: ISO 8601 date. Only rules created on or before this date are retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or earlier than fromDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: pageSize in: query description: Maximum number of rules per page. Range 1-250. Defaults to 50. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query description: 'ID of the next page, returned as `nextPageId` in each response. Changing any other parameter resets pagination. A cursor that is unknown or has expired is rejected with `The provided pageId is invalid or has expired` rather than silently returning the first page.' schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/UrlRule' nextPageId: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: [URL Rules] summary: Create URL Rule description: | > [!NOTE] > **Required role** — Create URL Rules Create a URL rule. Each rule matches a tracked URL against `wordsToMatch` and, on a match, applies its `tag`. The tag prefix sets the rule flavor — `!` action, `@` source, `$` sale, `#` subscription — and an invalid prefix is rejected (see the `tag` field). Lead-stage rules are the exception: they take a prefix-less tag together with `createLeadStage: true`. A rule whose tag or matching words duplicate an existing rule is rejected with `A URL rule with this tag or matching words already exists`. Only simple rules can be created through the API. The operation is synchronous: on success the new rule's `id` is returned in `result`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UrlRuleCreate' examples: action: summary: Action-tag rule on webinar registration URLs value: name: Webinar Registration tag: '!webinar-registration' wordsToMatch: ['webinar/register'] sourceRuleTypes: ['PREVIOUS_URL'] sourceWithManualCategories: summary: Source-tag rule assigning fixed traffic source and source categories value: name: Paid Facebook Traffic tag: '@facebook-ads' wordsToMatch: ['fbclid'] sourceRuleTypes: ['REFERRER_URL'] trafficSourceCategory: Facebook sourceCategory: Paid leadStage: summary: Lead-stage rule — prefix-less tag becomes the stage name value: name: Booked Call tag: 'booked-call' wordsToMatch: ['calendar/booked'] sourceRuleTypes: ['PREVIOUS_URL'] createLeadStage: true responses: '200': description: URL rule created successfully. `result` carries the new rule's id. content: application/json: schema: type: object properties: request_id: type: string result: type: string description: Opaque id of the newly created rule (e.g. `ur-`), for use with the retrieve/update/delete endpoints. example: 'ur-a3f5c9d2e1b8074f6c2d9a1e5b3f7c8d0e2a4b6c8d0f1a3b5c7d9e1f2a4b6c8d' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/url-rules/{id}: get: tags: [URL Rules] summary: Retrieve URL Rule description: | > [!NOTE] > **Required role** — Get URL Rules Retrieve a single URL rule by its id. Returns the standard list envelope with at most one element — an empty `result` array when no rule has that id. parameters: - name: id in: path required: true description: Opaque id of the URL rule to retrieve (e.g. `ur-`), as returned by `GET /api/v1.0/url-rules`. schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/UrlRule' request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' put: tags: [URL Rules] summary: Update URL Rule description: | > [!NOTE] > **Required role** — Update URL Rules Update a URL rule by its id. Every field is optional: only the fields included in the request are modified; any field left out is kept unchanged. Pausing a rule is therefore a one-field request — `{"isEnabled": false}` — with no need to read the rule and send it back whole. A field that *is* sent replaces its stored value, so an empty array clears a list (`"wordsNotToMatch": []`) and an empty string clears a manual category (`"trafficSourceCategory": ""`). `name`, `tag`, `wordsToMatch` and `sourceRuleTypes` can be replaced but not emptied: a rule without them can never fire, so sending one of them empty is rejected. A dynamic parameter and its manual category stay mutually exclusive — sending a non-empty value for one clears the other side of the stored rule, while clearing one (`[]` / `""`) leaves the other untouched. The same tag-prefix validation as on create applies, judged against the rule's flavor: a prefix-less `tag` is accepted on a lead-stage rule and rejected on any other, and a prefixed `tag` the other way round — unless the request sends `createLeadStage` to change the flavor. A new prefix-less tag points the rule at the stage with that name, creating it if the account has none; the stage it pointed at before keeps its name and its leads. An id that does not exist, or that belongs to a rule the API does not manage, is rejected with `The externalId does not exist for this user`. The operation is synchronous. parameters: - name: id in: path required: true description: Opaque id of the URL rule to update (e.g. `ur-`), as returned by `GET /api/v1.0/url-rules`. schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UrlRuleUpdate' examples: pause: summary: Pause a rule — one field, nothing else touched value: isEnabled: false matchingWords: summary: Replace the matching words and the URLs inspected value: wordsToMatch: ['webinar/register', 'webinar/signup'] sourceRuleTypes: ['PREVIOUS_URL', 'REFERRER_URL'] clearExclusions: summary: Clear a list — an empty array replaces the stored one value: wordsNotToMatch: [] responses: '200': description: URL rule updated successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' delete: tags: [URL Rules] summary: Delete URL Rule description: | > [!NOTE] > **Required role** — Delete URL Rules Delete a URL rule by its id, so it no longer tags matching traffic. The operation is synchronous. parameters: - name: id in: path required: true description: Opaque id of the URL rule to delete (e.g. `ur-`), as returned by `GET /api/v1.0/url-rules`. schema: type: string responses: '200': description: URL rule deleted successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/custom-costs: get: tags: [Custom Costs] summary: Retrieve Custom Costs description: | > [!NOTE] > **Required role** — Get Custom Costs Retrieve custom costs. A cost matches the date window when its active range (from startDate to endDate, or open-ended when no endDate is set) overlaps the fromDate/toDate window. Use the returned id to update or delete a cost. parameters: - name: ids in: query description: Array of custom cost ids. Maximum of 50 ids. schema: type: string - name: fromDate in: query description: ISO 8601 date. Only costs active on or after this date will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or later than toDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: toDate in: query description: ISO 8601 date. Only costs active on or before this date will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or earlier than fromDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: pageSize in: query description: Maximum number of custom costs per page. Range 1-250. Defaults to 50. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query description: ID of the next page. schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/CustomCost' nextPageId: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: [Custom Costs] summary: Create Custom Cost description: | > [!NOTE] > **Required role** — Create Custom Costs > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~10 seconds. Create a custom cost with all necessary information. requestBody: required: true content: application/json: schema: type: object required: [startDate, frequency, cost, tags] properties: name: type: string description: Descriptive label for the cost. startDate: type: string description: Start date in ISO 8601 format. If no time is provided, the start of the day is used. The time zone offset is discarded rather than converted, so the date is stored exactly as written; see **Date Formats**. endDate: type: string description: End date in ISO 8601 format. For ONE_TIME costs it is set to the start date. The time zone offset is discarded rather than converted, so the date is stored exactly as written; see **Date Formats**. frequency: type: string enum: [DAILY, ONE_TIME] cost: type: number description: Must be greater than zero. Currency matches Hyros account settings. tags: type: array items: type: string description: Source tags to assign the costs to. Maximum of 10 tags. maxItems: 10 example: name: Monthly Agency Fee startDate: '2024-12-25T00:00:00.000Z' endDate: '2024-12-20T00:00:00.000Z' frequency: DAILY tags: ['@instagram', '@facebook'] cost: 999.50 responses: '200': description: Custom cost created successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/custom-costs/{id}: put: tags: [Custom Costs] summary: Update Custom Cost description: | > [!NOTE] > **Required role** — Update Custom Costs Update a custom cost by its id. This is a full replacement: every field must be provided, and any omitted optional field is cleared. Costs created from the Hyros UI with a MONTHLY frequency are returned by the retrieve endpoint, but the API only accepts DAILY or ONE_TIME, so a MONTHLY cost cannot be edited through this endpoint without changing its frequency. parameters: - name: id in: path required: true description: Id of the custom cost to update. schema: type: string requestBody: required: true content: application/json: schema: type: object required: [startDate, frequency, cost, tags] properties: name: type: string description: Descriptive label for the cost. startDate: type: string description: Start date in ISO 8601 format. If no time is provided, the start of the day is used. The time zone offset is discarded rather than converted, so the date is stored exactly as written; see **Date Formats**. endDate: type: string description: End date in ISO 8601 format. For ONE_TIME costs it is set to the start date. The time zone offset is discarded rather than converted, so the date is stored exactly as written; see **Date Formats**. frequency: type: string enum: [DAILY, ONE_TIME] cost: type: number description: Must be greater than zero. Currency matches Hyros account settings. tags: type: array items: type: string description: Source tags to assign the costs to. Maximum of 10 tags. maxItems: 10 example: name: Monthly Agency Fee startDate: '2024-12-25T00:00:00.000Z' endDate: '2025-01-25T00:00:00.000Z' frequency: DAILY tags: ['@instagram', '@facebook'] cost: 1299.50 responses: '200': description: Custom cost updated successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' delete: tags: [Custom Costs] summary: Delete Custom Cost description: | > [!NOTE] > **Required role** — Delete Custom Costs Delete a custom cost by its id. parameters: - name: id in: path required: true description: Custom cost id to be deleted. schema: type: string responses: '200': description: Custom cost deleted successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/clicks: post: tags: [Clicks] summary: Create Click description: | > [!NOTE] > **Required role** — Create Clicks > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~10 seconds. Creates a click with the URL the user clicked on and a session id representing the potential lead session. Optionally includes an email to create a lead, and ad data. requestBody: required: true content: application/json: schema: type: object required: [referrerUrl] properties: referrerUrl: type: string description: The URL for the click. sessionId: type: string description: Unique string representing the lead session. previousUrl: type: string userAgent: type: string ip: type: string sourceLinkTag: type: string description: A `@tag` representing the ad. Must start with `@`. isOrganic: type: boolean integrationType: type: string enum: [GOOGLE, GOOGLE_V2, FACEBOOK, TIKTOK, SNAPCHAT, LINKEDIN, TWITTER, PINTEREST, BING] description: Subset of `AdspendType` accepted when creating a click. adSourceId: type: string description: | Id of the group of ads. Required if `integrationType` is present. - Facebook: adset id · Google: campaign id · TikTok: ad group id · Snapchat: ad squad id · LinkedIn: campaign id adspendAdId: type: string description: Id of the ad. Only for Facebook and Google. adSourceClickId: type: string description: Click id in the ad platform. Used for offline conversions (Facebook, Google, TikTok, Snapchat). email: type: string phones: oneOf: - type: string - type: array items: type: string tag: type: string description: Tag to apply to the lead. date: type: string description: | Date when the click was made. Allowed formats: `yyyy-MM-ddTHH:mm:ssZ`, `yyyy-MM-ddTHH:mmZ`, `yyyy-MM-ddTHH:mm:ss+HH:mm`, `yyyy-MM-ddTHH:mm+HH:mm`, `yyyy-MM-ddTHH:mm:ss`, `yyyy-MM-ddTHH:mm`, `yyyy-MM-dd` Without a time zone offset the date is stored as UTC; see **Date Formats**. examples: organic: summary: Click for an organic ad value: sessionId: RWp7VmL3nlAi6zdG0KKQ referrerUrl: landing.page.com previousUrl: previous.url userAgent: Mozilla/5.0 (X11; Linux x86_64) ip: 0.0.0.0 sourceLinkTag: '@facebook-post' isOrganic: true platform: summary: Click for a platform ad value: sessionId: RWp7VmL3nlAi6zdG0KKQ referrerUrl: landing.page.com ip: 0.0.0.0 integrationType: GOOGLE adSourceId: '6500000028' adspendAdId: '4800000004652' adSourceClickId: Cj0KxxxxxxKQBhCNARIsACUEW_bnG48 leadClick: summary: Click for a platform ad linked to a lead value: sessionId: RWp7VmL3nlAi6zdG0KKQ referrerUrl: landing.page.com ip: 0.0.0.0 integrationType: GOOGLE adSourceId: '6500000028' adSourceClickId: Cj0KxxxxxxKQBhCNARIsACUEW_bnG48 email: new.lead@mail.com phones: ['202-555-0139'] responses: '200': description: Click created successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. Also returned when the request carries a blacklisted email address, with the message `The event was discarded because an email or phone number it carries is blacklisted for this account. This is set when a lead is permanently deleted.` No `request_id` is issued in that case, so there is nothing to poll. See *Delete Lead*. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/carts: get: tags: [Carts] summary: Retrieve Carts description: | > [!NOTE] > **Required role** — Get Carts Search and retrieve carts by creation date, purchased state (whether the cart has an associated order or not), email and lead id. A cart created through `POST /api/v1.0/carts` has no associated order yet, so it is returned by this endpoint with no filter and by `purchased=false`, but **not** by `purchased=true`. It starts being returned by `purchased=true` only once a sale is registered for it, which happens through a separate path after the cart write has already completed. parameters: - name: emails in: query description: Array of emails or prefixes. Maximum of 50 emails. schema: type: string - name: leadIds in: query description: Array of leadIds. Maximum of 50 lead ids. schema: type: string - name: purchased in: query description: Filter by purchased state. `true` returns only purchased carts (with orderId); `false` returns carts without orderId. schema: type: boolean - name: fromDate in: query description: ISO 8601 date. Only carts whose creation date is more recent than this will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or later than toDate. schema: type: string example: '2023-08-24T00:00:00-03:00' - name: toDate in: query description: ISO 8601 date. Only carts whose creation date is older than this will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or earlier than fromDate. schema: type: string example: '2023-08-24T00:00:00-03:00' - name: pageSize in: query description: Maximum number of carts per page. Range 1-250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query description: ID of the next page, taken from the `nextPageId` of a previous response. Omit it to fetch the first page. An invalid or expired pagination cursor is rejected with `400 Bad Request`. schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/Cart' nextPageId: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: [Carts] summary: Create Cart description: | > [!NOTE] > **Required role** — Create Carts > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~10 seconds. Create a cart. Additionally, creates the lead if not already present. A cart sent with no email and no phone number is recorded without a lead. When the cart cannot be created, the request reaches `FAILED` with one of these `errorMessage` values: `The cart total exceeds the maximum allowed amount.`, `The provided email address is invalid.`, `The cart could not be processed for this account.`, or `An internal error occurred while processing the request.` for internal failures. Requests rejected by validation, such as a cart with no items, return `400` synchronously and never reach `FAILED`. A cart carrying a blacklisted email address is also refused synchronously with a `400`, and no `request_id` is issued for it. Re-sending the same `cartId` is not an error: the request reaches `PROCESSED` with the already-existing cart as its `result`, instead of a duplicate-resource error. requestBody: required: true content: application/json: schema: type: object required: [items] properties: cartId: type: string description: ID of the cart. A default one will be created if not included. email: type: string firstName: type: string lastName: type: string leadIps: type: array items: type: string description: IPs of the customer that owns the cart for ad attribution. Maximum of 3 ips. maxItems: 3 phoneNumbers: oneOf: - type: string - type: array items: type: string date: type: string description: ISO 8601 date. Timezone recommended. Without a time zone offset the date is stored as UTC; see **Date Formats**. example: '2021-04-16T20:35:00-06:00' priceFormat: type: string enum: [DECIMAL, INTEGER] default: DECIMAL currency: type: string items: type: array items: $ref: '#/components/schemas/CartItem' minItems: 1 example: cartId: d49b708b3df50505869ca54f026e7c97a4959b587605f14f91c7e289de9f80bd email: john@doe.com firstName: John lastName: Doe date: '2021-09-06T12:00:12Z' currency: USD items: - name: T-shirt-blue price: 9.5 externalId: '23456798' quantity: 3 sku: DEPOR-XYZ-BLN-41 isRebill: false priceFormat: DECIMAL phoneNumbers: ['2345678901'] leadIps: ['70.107.190.180'] responses: '200': description: Cart created successfully. content: application/json: schema: type: object properties: request_id: type: string result: type: string message: type: array items: type: string '400': description: Bad Request. Also returned when the request carries a blacklisted email address, with the message `The event was discarded because an email or phone number it carries is blacklisted for this account. This is set when a lead is permanently deleted.` No `request_id` is issued in that case, so there is nothing to poll. See *Delete Lead*. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' put: tags: [Carts] summary: Update Cart description: | > [!NOTE] > **Required role** — Update Carts > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~5 minutes. Update a cart. An item already on the cart is updated in place rather than added again. Items are matched by `sku` when both the request and the stored item carry one, and by `name` otherwise. Items the request does not mention are left on the cart. When the update cannot be applied, the request reaches `FAILED` with one of these `errorMessage` values: `The cart total exceeds the maximum allowed amount.`, `The cart could not be processed for this account.`, or `An internal error occurred while processing the request.` for internal failures. Requests rejected by validation, such as a cart with no items, return `400` synchronously and never reach `FAILED`. Re-sending the same update is not an error: it reaches `PROCESSED` with the current cart as its `result`. requestBody: required: true content: application/json: schema: type: object required: [cartId, items] properties: cartId: type: string description: ID of the cart to update. items: type: array items: $ref: '#/components/schemas/CartItem' minItems: 1 date: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. priceFormat: type: string enum: [DECIMAL, INTEGER] default: DECIMAL currency: type: string example: cartId: d49b708b3df50505869ca54f026e7c97a4959b587605f14f91c7e289de9f80bd date: '2021-09-06T12:00:12Z' currency: USD items: - name: T-shirt-red price: 9.5 externalId: '23456333' quantity: 1 sku: DEPOR-XYZ-RED-41 isRebill: false priceFormat: DECIMAL responses: '200': description: Cart updated successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/user-info: get: tags: [User Info] summary: Retrieve User Information description: | > [!NOTE] > **Required role** — Get User Information Retrieve user profile, connected accounts, and true tracking configuration. responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: object properties: userProfile: type: object properties: email: type: string firstName: type: string lastName: type: string phoneNumber: type: integer companyName: type: string profilePicture: type: string vat: type: string helpNotes: type: boolean notificationsEnabled: type: string timezone: type: string userAddress: type: object properties: street: type: string city: type: string state: type: string zipCode: type: string allowedAccounts: type: array items: type: object properties: firstName: type: string lastName: type: string companyName: type: string email: type: string pictureUrl: type: string status: type: string accessibleAccounts: type: array items: type: object properties: accountId: type: string description: Opaque, stable identifier of the account. Use it to identify that account on other operations. example: 3fbcb1563c51a0e0c7698680a9c3d51161f48c60db57699e3636885eb72c64b4 firstName: type: string lastName: type: string companyName: type: string email: type: string pictureUrl: type: string status: type: string trueTrackingData: type: object additionalProperties: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/keywords: get: tags: [Keywords] summary: Retrieve Keywords description: | > [!NOTE] > **Required role** — Get Attribution List all keywords, or those associated with a specific Google Ad Group Id. parameters: - name: adgroupId in: query description: The ad group id. schema: type: string - name: pageSize in: query description: Between 1 and 250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: type: object properties: id: type: string name: type: string adGroupId: type: string adGroupName: type: string nextPageId: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/subscriptions: get: tags: [Subscriptions] summary: Retrieve Subscriptions description: | > [!NOTE] > **Required role** — Get Subscriptions Search and retrieve subscriptions by start/end date, last-modified date, email, lead id, product tag, id, and states. parameters: - name: ids in: query description: Array of subscription ids. Maximum of 50 ids. schema: type: string - name: emails in: query description: Array of emails or prefixes. Maximum of 50 emails. schema: type: string - name: leadIds in: query description: Array of leadIds. Maximum of 50 lead ids. schema: type: string - name: productTags in: query description: Array of product tags. Maximum of 20 product tags. schema: type: string - name: subscriptionStates in: query description: >- Filter by subscription status, comma separated. Defaults to all states. An unrecognized value is rejected with `Invalid subscriptionStates ''. Accepted values: ACTIVE, TRIALING, CANCELED, PAST_DUE, INCOMPLETE, INCOMPLETE_EXPIRED, UNPAID, COMPLETED, PAUSED, UNKNOWN`. schema: type: array items: $ref: '#/components/schemas/SubscriptionStatus' - name: fromDate in: query description: ISO 8601 date. Only subscriptions whose join date is more recent than this will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or later than toDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: toDate in: query description: ISO 8601 date. Only subscriptions whose join date is older than this will be retrieved. If the date does not include a timezone, the configured account timezone will be assumed. Cannot be in a future month, or earlier than fromDate. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: updatedFromDate in: query description: ISO 8601 date. Only subscriptions updated on or after this date will be retrieved. Use it to fetch just the subscriptions that changed since your last request. Cannot be later than `updatedToDate`. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: updatedToDate in: query description: ISO 8601 date. Only subscriptions updated on or before this date will be retrieved. Cannot be earlier than `updatedFromDate`. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: pageSize in: query description: Range 1-250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query description: ID of the next page, taken from the `nextPageId` of a previous response. Omit it to fetch the first page. An invalid or expired pagination cursor is rejected with `400 Bad Request`. schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/Subscription' nextPageId: type: string request_id: type: string '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: [Subscriptions] summary: Create Subscription description: | > [!NOTE] > **Required role** — Create Subscriptions > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~10 seconds. Create a subscription. Additionally, creates the lead if not already present. requestBody: required: true content: application/json: schema: type: object required: [status, startDate, price, periodicity] properties: email: type: string description: Required if no phone number is provided. parentEmail: type: string firstName: type: string lastName: type: string leadIps: type: array items: type: string stage: type: string phoneNumbers: oneOf: - type: string - type: array items: type: string description: Required if no email is provided. subscriptionId: type: string name: type: string status: $ref: '#/components/schemas/SubscriptionStatusWrite' startDate: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. example: '2021-04-16T20:35:00' endDate: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. cancelAtDate: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. trialStartDate: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. trialEndDate: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. planId: type: string price: type: number periodicity: type: string enum: [DAY, WEEK, MONTH, QUARTER, YEAR] example: email: john@doe.com parentEmail: jane@doe.com firstName: John lastName: Doe leadIps: ['172.8.105.28'] stage: Customer phoneNumbers: ['1105385366'] endDate: '2025-03-16T20:35:00' subscriptionId: '18294892740' startDate: '2025-01-16T20:35:00' trialStartDate: '2025-02-16T20:35:00' planId: IOJNF0293IRD9023JF0D price: 8.9 periodicity: MONTH status: ACTIVE name: Monthly Subscription Active responses: '200': description: Subscription created successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. Also returned when the request carries a blacklisted email address, with the message `The event was discarded because an email or phone number it carries is blacklisted for this account. This is set when a lead is permanently deleted.` No `request_id` is issued in that case, so there is nothing to poll. See *Delete Lead*. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' put: tags: [Subscriptions] summary: Update Subscription description: | > [!NOTE] > **Required role** — Update Subscriptions > [!NOTE] > **Asynchronous** — effect is not immediate; changes typically appear within ~5 minutes. Update a subscription. requestBody: required: true content: application/json: schema: type: object required: [ids] properties: ids: type: array items: type: string description: Array of subscription ids. Maximum of 50 ids. maxItems: 50 name: type: string status: $ref: '#/components/schemas/SubscriptionStatusWrite' startDate: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. endDate: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. cancelAtDate: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. trialStartDate: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. trialEndDate: type: string description: ISO 8601 date. Without a time zone offset the date is stored as UTC; see **Date Formats**. price: type: number example: ids: ['subscriptionId'] status: CANCELED name: Monthly Subscription Active startDate: '2025-01-16T20:35:00' endDate: '2025-03-16T20:35:00' trialStartDate: '2025-02-16T20:35:00' trialEndDate: '2025-03-16T20:35:00' cancelAtDate: '2025-03-16T20:35:00' responses: '200': description: Subscription updated successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/tracking-script: get: tags: [Tracking Script] summary: Get Tracking Script description: | > [!NOTE] > **Required role** — Get Lead Clicks Retrieves the tracking script for a given domain. If no domain is provided, returns the default tracking script. The script can be customized with optional parameters to enable SPA tracking, ignore previous URLs, embed on iframes, or hide tracking parameters. parameters: - name: domain in: query description: Domain for which to retrieve the tracking script. schema: type: string - name: spa in: query description: Enables tracking of clicks and emails on URL change for Single Page Applications (SPA). Defaults to false. schema: type: boolean default: false - name: ignorePrevUrl in: query description: When true, ignores sources from the previous URL during attribution. Defaults to false. schema: type: boolean default: false - name: embed in: query description: When true, embeds the Universal script on iframes. Defaults to false. schema: type: boolean default: false - name: deleteTrackingScriptParams in: query description: When true, the Universal Script will automatically hide the tracking parameters in the URL after use. This setting is persisted as user metadata. schema: type: boolean responses: '200': description: Tracking script HTML. content: text/plain: schema: type: string example: | '400': description: Invalid domain. content: text/plain: schema: type: string /api/v1/domains: get: tags: [Domains] summary: Get Domains description: | > [!NOTE] > **Required role** — Get Lead Clicks Retrieves a list of verified domains associated with the product. This endpoint is served under `/api/v1/` (not `/api/v1.0/` like the other endpoints). responses: '200': description: List of verified domains. content: application/json: schema: type: array items: type: string example: ['domain1.example.com', 'domain2.example.com'] '401': description: Unauthorized. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/stages: get: tags: [Stages] summary: Retrieve Lead Stages description: | > [!NOTE] > **Required role** — Get Leads Retrieves all lead stages for the account, along with a count of leads for each one. By default that count is the leads whose *current* stage it is. `stageFromDate` and `stageToDate` scope it instead to the leads the stage was applied to inside a period, which is how you answer "how many leads entered this stage this week". The two counts are not comparable. Stage applications accumulate: a lead that moved from one stage to another inside the period is counted under *both*, while the default count attributes it only to the stage it ended in. So the sum of the counts in a period can exceed your number of leads, and a wide period generally reports more leads per stage than the unfiltered call does. Deleted leads are left out of both counts. The generic `fromDate` and `toDate` parameters are not honoured on this endpoint; use `stageFromDate` and `stageToDate`. parameters: - name: name in: query description: Name to search stages by. schema: type: string - name: stageFromDate in: query description: | An ISO 8601 formatted date. Only the leads the stage was applied to on or after this date are counted. The stages returned are unaffected, so a stage nobody entered inside the period is still listed, with an `amount` of `0`. A lead whose stage was later removed is not counted, even if it was applied inside the period. Counts above roughly 40,000 leads in a period are close estimates rather than exact totals; the default count is always exact. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: stageToDate in: query description: | An ISO 8601 formatted date. Only the leads the stage was applied to on or before this date are counted. If the date does not include a timezone, the configured account timezone will be assumed. schema: type: string example: '2021-04-16T20:35:00-05:00' - name: pageSize in: query description: Range 1-250. schema: type: integer minimum: 1 maximum: 250 - name: pageId in: query schema: type: string responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: type: object properties: name: type: string amount: type: integer nextPageId: type: string request_id: type: string example: result: - name: SQL amount: 1 - name: MQL amount: 5 nextPageId: 1073e129b360b78db3508bea584d1f295c7851c3d9b290308ac57528b6e38a21 request_id: 5696f9a3524e42318f3cdf40176e70e3 '400': description: Bad Request. `stageFromDate` later than `stageToDate`, an invalid date format, or an invalid `pageSize`/`pageId`. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: result: ERROR message: ["stageFromDate cannot be later than stageToDate"] '401': description: Unauthorized. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/webhook-subscriptions: post: tags: [Webhook Subscriptions] summary: Create Webhook Subscription description: | > [!NOTE] > **Required role** — Create Webhook Subscriptions Create a webhook subscription. The subscription tells Hyros which event types to send, via **POST**, to the provided `targetUrl`. The response includes the generated `externalId` and the `secretKey` used to validate the HMAC signature of delivered events. The `secretKey` is returned only in this create response — store it, as it is not returned by the list endpoint. requestBody: required: true content: application/json: schema: type: object required: [name, targetUrl, eventTypes] properties: name: type: string description: Name of the subscription. targetUrl: type: string description: | URL where the event payloads are sent. Must be a public `http`/`https` URL and cannot contain template placeholders like `{{placeholder}}`. Non-HTTP schemes and internal/private hosts (loopback, link-local, private RFC-1918 ranges, wildcard, multicast, and cloud-metadata addresses) are rejected. eventTypes: type: array description: Event types to subscribe to. Must contain at least one known event type. items: type: string enum: - sale.attributed - sale.refunded - call.attributed - lead.opted.in - lead.opted.in.first.time - lead.origin.assigned - lead.stage.changed - lead.tag.added - lead.tag.removed - subscription.created - subscription.status.changed example: name: My CRM sync targetUrl: https://example.com/hooks/hyros eventTypes: ['sale.attributed', 'lead.opted.in'] responses: '200': description: Subscription created successfully. content: application/json: schema: type: object properties: result: $ref: '#/components/schemas/WebhookSubscription' request_id: type: string example: result: externalId: sub-2a475f6baf8f416bac9ff60e1a0fabb5 name: My CRM sync targetUrl: https://example.com/hooks/hyros eventTypes: ['sale.attributed', 'lead.opted.in'] state: ACTIVE secretKey: ssk-244e8d359f67456cb9efac27913283fb creationDate: '2026-07-09T14:28:15Z' lastDeliveryDate: null request_id: 143d65f8d2654e16a9dab64b20176f9c '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Unauthorized. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' get: tags: [Webhook Subscriptions] summary: Retrieve Webhook Subscriptions description: | > [!NOTE] > **Required role** — Get Webhook Subscriptions Retrieve all non-deleted webhook subscriptions for the account. responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: type: array items: $ref: '#/components/schemas/WebhookSubscription' request_id: type: string example: result: - externalId: sub-2a475f6baf8f416bac9ff60e1a0fabb5 name: My CRM sync targetUrl: https://example.com/hooks/hyros eventTypes: ['sale.attributed', 'lead.opted.in'] state: ACTIVE creationDate: '2026-07-09T14:28:15Z' lastDeliveryDate: '2026-07-09T15:13:20Z' request_id: c9e86849464545d9b6b24d8039fa38d8 '401': description: Unauthorized. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/webhook-subscriptions/{externalId}: delete: tags: [Webhook Subscriptions] summary: Delete Webhook Subscription description: | > [!NOTE] > **Required role** — Delete Webhook Subscriptions Delete a webhook subscription by its `externalId`. parameters: - name: externalId in: path required: true description: Opaque identifier of the subscription to delete. schema: type: string example: sub-2a475f6baf8f416bac9ff60e1a0fabb5 responses: '200': description: Subscription deleted successfully. content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Bad Request. Returned when the `externalId` does not exist for this account. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Unauthorized. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/reports/generate: post: tags: [Reports] summary: Generate Report description: | > [!NOTE] > **Required role** — Generate Reports > [!NOTE] > **Asynchronous** — the report is computed in the background. This call answers a report id; read the report with `GET /api/v1.0/reports/poll/{externalId}`. Start a report over the account's attribution and revenue data, grouped by source, ad, creative, keyword, geography, referrer, cohort, lead journey or sale item, over a date range. `groupBy` names the report family: it decides what a row is and which options apply. `JOURNEY` reports the paths leads took before converting, one row per journey, each carrying the steps it went through in `journeySteps` and the id of that path in `journeyId`. A journey report is read at the level of the narrowest source filter the request names — ads, then source links, then source categories — and at the traffic source level when it names none. requestBody: required: true content: application/json: schema: type: object required: [configuration] properties: configuration: type: object required: [groupBy, startDate, endDate] properties: groupBy: type: string description: > Report family. Determines what a row is and which options apply. A `COHORT` row carries one ROI per period in `cohortRoi`, and every one of them has no value when the row has no ad spend, so a `0` there always reflects real spend. enum: [SOURCE, SOURCE_CATEGORY, TRAFFIC_SOURCE, AD_ACCOUNT, GOAL, AD, CREATIVE, KEYWORD, SALE_ITEM_LTV, SALE_ITEM_TOTAL, COUNTRY, REGION, CITY, DEVICE_TYPE, DEVICE_PLATFORM, REFERRER_DOMAIN, REFERRER_URL, JOURNEY, COHORT] startDate: type: string description: Start date, inclusive, as ISO 8601 `yyyy-MM-dd`. Cannot be in a future month. example: '2026-09-01' endDate: type: string description: End date, inclusive, as ISO 8601 `yyyy-MM-dd`. Cannot be before startDate, nor in a future month. example: '2026-09-07' timeZone: type: string description: Requester's UTC offset as `+HH:mm` / `-HH:mm`. Start and end dates resolve against it; when omitted the account's own time zone is used. example: '-03:00' options: type: object description: > Settings for specific report families. Report families that do not use an option ignore it, unless its description says otherwise. properties: journeyKeyMetric: type: string description: What a lead journey report measures. Defaults to TOTAL_REVENUE. enum: [TOTAL_REVENUE, SALES, CALLS, CUSTOMERS] ltvDays: type: integer description: > LTV window in days: only sales made within this many days of the lead joining are counted. Defaults to 30. Applies only to `SALE_ITEM_LTV`. creativeGroupType: type: string description: > Attribute creatives are grouped by. Defaults to `CREATIVE`. Applies only to `CREATIVE`. enum: [CREATIVE, IMAGE, VIDEO, HEADLINE, CONTENT, AD_NAME] creativeViewType: type: string description: > Layout creatives are returned in. Defaults to `GALLERY`. Applies only to `CREATIVE`. enum: [GALLERY, TABLE] excludeLeadsWithoutSales: type: boolean description: > Leaves out leads that produced no sale. Applies to `SALE_ITEM_LTV` and `COHORT`. onlyIncludeSourcelinksOfProducts: type: boolean description: > Limits the report to sources with sales of the filtered products, or with at least one sale when no product filter is set. Applies to the source families (`SOURCE`, `SOURCE_CATEGORY`, `TRAFFIC_SOURCE`, `AD_ACCOUNT`, `GOAL`), `AD`, `KEYWORD`, `COHORT`, `COUNTRY`, `REGION`, `CITY`, `DEVICE_TYPE` and `DEVICE_PLATFORM`. totalSalesCallOption: type: string description: > Counts sales, calls, or both. Defaults to `ONLY_SALES`. Applies to `SALE_ITEM_TOTAL` and `SALE_ITEM_LTV`. enum: [ONLY_SALES, ONLY_CALLS, SALES_AND_CALLS] segment: type: object description: > Limits an LTV report to a group of leads. Applies only to `SALE_ITEM_LTV`. properties: origin: type: string description: > How leads are selected. `SOURCE`: by the source they first opted in through. `SALE`: by the products they bought. Defaults to `SOURCE`. enum: [SOURCE, SALE] initialSources: type: array items: type: string description: > Tags that select the leads: source tags, such as `@my-source`, when 'origin' is `SOURCE`; product tags, such as `$my-product`, when 'origin' is `SALE`. Tags must be complete; source tags ignore letter case, product tags do not. If no tag matches, the report returns no rows. salesRelatedToSourceTags: type: array items: type: string description: > Source tags. Only sales linked to these sources are counted. comparison: type: object description: > Compares two groups of sales in one report. Rows include every product sold in either group. Applies only to `SALE_ITEM_TOTAL`, and only when 'enabled' is true. properties: enabled: type: boolean description: > Turns the comparison on. Must be true; otherwise the comparison settings are ignored. compareBy: type: string description: > What differs between the two groups. `DATES`: each group covers its own date range, set by its 'start' and 'end'. The report's `startDate` and `endDate` are used only as defaults. `TAGS`: both groups cover the report's date range, and each group applies its own tag filters. Defaults to `DATES`. enum: [DATES, TAGS] groupA: type: object description: > First group. Its results fill the report rows and totals. Required when 'enabled' is true; a request without it is rejected. properties: start: type: string description: > Start of the group's date range, `yyyy-MM-dd`. Used only when `compareBy` is `DATES`, and only together with 'end'. When either is missing, the group uses its default range: the report's date range for group A, and the period of the same length just before it for group B. end: type: string description: > End of the group's date range, `yyyy-MM-dd`. Used only when `compareBy` is `DATES`, and only together with 'start'. For the default, see 'start'. leadTags: type: array items: type: string description: > Lead tags to include. Used only when `compareBy` is `TAGS`. notLeadTags: type: array items: type: string description: > Lead tags to exclude. Used only when `compareBy` is `TAGS`. productTags: type: array items: type: string description: > Product tags to include. Used only when `compareBy` is `TAGS`. noSaleItemTags: type: array items: type: string description: > Product tags to exclude. Used only when `compareBy` is `TAGS`. categoryIds: type: array items: type: integer description: > Product category ids to include. Used only when `compareBy` is `TAGS`. ignoreRecurringSales: type: boolean description: > Ignore recurring sales. Used only when `compareBy` is `TAGS`. excludeHardCosts: type: boolean description: > Exclude hard costs. Used only when `compareBy` is `TAGS`. excludeRefunds: type: boolean description: > Exclude refunds. Used only when `compareBy` is `TAGS`. leadTagSearchType: type: string description: > How 'leadTags' match. Used only when `compareBy` is `TAGS`. enum: [ANY_OF_THEM, ALL_OF_THEM, NO_ANY_OF_THEM, NO_ALL_OF_THEM] notLeadTagSearchType: type: string description: > How 'notLeadTags' match. Used only when `compareBy` is `TAGS`. enum: [ANY_OF_THEM, ALL_OF_THEM, NO_ANY_OF_THEM, NO_ALL_OF_THEM] groupB: type: object description: > Second group. Its results appear in the `comparison` field of each row and of the totals. Required when 'enabled' is true; a request without it is rejected. Takes the same fields as 'groupA'. referrerDomains: type: array items: type: string description: > Referrer domains to include, such as `google.com`. Exact, case-sensitive match. Applies to `REFERRER_DOMAIN` and `REFERRER_URL`. baseReferrerUrls: type: array items: type: string description: > Referrer URLs to include, without the query string, such as `https://google.com/search`. Exact match. Applies only to `REFERRER_URL`; with `REFERRER_DOMAIN`, the report returns no rows. normalizedReferrerUrls: type: array items: type: string description: > Full referrer URLs to include, as shown in the `name` of `REFERRER_URL` rows. Exact match. Applies only to `REFERRER_URL`; with `REFERRER_DOMAIN`, the report returns no rows. pagination: type: object description: Lead journey report state carried over from previous pages. properties: alreadyProcessedJourneyIds: type: array items: type: string description: Journey ids returned by previous pages, excluded from this one. A journey report answers 30 journeys at a time. reportAttribution: type: object description: > Attribution model and its options; required for attribution-based groupings. `attributionModel` accepts `FIRST_CLICK`, `LAST_CLICK`, `SCIENTIFIC`, `LINEAR`, `DEPRECIATION` and `U_SHAPED`. `U_SHAPED` gives 40% of a sale to the first click, 40% to the last and splits the remaining 20% across the clicks in between; it is supported for the `SOURCE`, `SOURCE_CATEGORY`, `TRAFFIC_SOURCE`, `AD_ACCOUNT`, `GOAL` and `AD` groupings. Multi-touch models (`LINEAR`, `DEPRECIATION`, `U_SHAPED`) do not support `timeSegmentation`. filters: type: object description: > Narrows the report by source, product and lead. It has three groups, `sourceFilters`, `productFilters` and `leadFilters`, and each holds an include object and an exclude object. Every field is optional; the conditions you set all have to hold together, and a field left out does not narrow the report. Field names are checked: one the object does not take, such as a field that is only accepted on the include side sent on the exclude side, is rejected with `400 Bad Request`. Tags are matched exactly, including their prefix (`!` for action tags, `@` for source tags, `$` for product tags). properties: sourceFilters: type: object description: Which sources the report counts. properties: includeSourceFilters: type: object description: Only count these sources. properties: sourceLinkIds: type: array items: { type: integer } description: Source link ids. sourceLinkAdIds: type: array items: { type: integer } description: Ad ids. sourceCategoryIds: type: array items: { type: integer } description: Source category ids. trafficSourceCategoryIds: type: array items: { type: integer } description: Traffic source ids. goalCategoryIds: type: array items: { type: integer } description: Goal ids. customerIds: type: array items: { type: string } description: Ad account ids. externalIntegrationIds: type: array items: { type: integer } description: Integration ids. namePrefixes: type: array items: { type: string } description: Sources whose name starts with any of these. Include only. nameSubstrings: type: array items: { type: string } description: Sources whose name contains any of these. Include only. adNamePrefixes: type: array items: { type: string } description: Ads whose name starts with any of these. Include only. adNameSubstrings: type: array items: { type: string } description: Ads whose name contains any of these. Include only. excludeSourceFilters: type: object description: > Leave these sources out. Takes the id fields of `includeSourceFilters` but none of the name fields, and names the source category ids `sourceLinkCategoryIds` rather than `sourceCategoryIds`. properties: sourceLinkIds: type: array items: { type: integer } sourceLinkAdIds: type: array items: { type: integer } sourceLinkCategoryIds: type: array items: { type: integer } description: Source category ids. trafficSourceCategoryIds: type: array items: { type: integer } goalCategoryIds: type: array items: { type: integer } customerIds: type: array items: { type: string } externalIntegrationIds: type: array items: { type: integer } productFilters: type: object description: Which sales the report counts, by what was sold. properties: includeProductFilters: type: object description: Only count sales of these products. properties: categoryIds: type: array items: { type: integer } description: Product category ids. skus: type: array items: { type: string } description: Product SKUs. tags: type: array items: { type: string } description: Product tags. tagPrefixes: type: array items: { type: string } description: Product tags starting with any of these. tagSubstrings: type: array items: { type: string } description: Product tags containing any of these. packageIds: type: array items: { type: integer } description: Package ids. subscriptionTags: type: array items: { type: string } description: Subscription tags. subscriptionTagPrefixes: type: array items: { type: string } description: Subscription tags starting with any of these. Include only. excludeProductFilters: type: object description: Leave out sales of these products. Takes every field of `includeProductFilters` except `subscriptionTagPrefixes`. properties: categoryIds: type: array items: { type: integer } skus: type: array items: { type: string } tags: type: array items: { type: string } tagPrefixes: type: array items: { type: string } tagSubstrings: type: array items: { type: string } packageIds: type: array items: { type: integer } subscriptionTags: type: array items: { type: string } leadFilters: type: object description: > Which leads the report counts. Leads, sales and revenue are narrowed to the matching leads; ad spend is not, so cost per lead, cost per acquisition and ROAS read against the filtered counts. properties: leadIncludeFilters: type: object description: Only count these leads. properties: tags: type: array items: { type: string } description: Lead tags. tagSearchType: type: string enum: [ANY_OF_THEM, ALL_OF_THEM] description: > How `tags` is matched. `ANY_OF_THEM` (the default) keeps a lead holding at least one of the tags, `ALL_OF_THEM` a lead holding every one of them. tagPrefixes: type: array items: { type: string } description: Leads holding a tag that starts with any of these. Include only. tagSubstrings: type: array items: { type: string } description: Leads holding a tag that contains the given text. Include only. stageIds: type: array items: { type: integer } description: Leads whose current stage is any of these lead stage ids. leadExcludeFilters: type: object description: Leave these leads out. properties: tags: type: array items: { type: string } description: Lead tags. tagSearchType: type: string enum: [NO_ANY_OF_THEM, NO_ALL_OF_THEM] description: > How `tags` is matched. `NO_ANY_OF_THEM` (the default) leaves out a lead holding any of the tags, `NO_ALL_OF_THEM` only a lead holding every one of them at once. stageIds: type: array items: { type: integer } description: Leads whose current stage is any of these lead stage ids. fields: type: array items: type: string description: > Metrics to include as columns; omit to get every metric that carries a value. A metric asked for is always present and reads null where the report found no value for it; one that was not asked for is left out. `CAC` is null when the row has spend but no unique customers. A `0` means the customers came without ad spend. `ROAS`, `NEW_CUSTOMERS_ROAS` and `ROI` have no value when the row has no ad spend, so they read `null` when asked for and are left out when `fields` is omitted. A `0` on any of them always reflects real spend. customMetricIds: type: array items: type: integer description: Custom metric ids to include as columns. settings: type: object description: Calculation settings — recurring sales, refunds, hard costs. sorting: type: object description: Field and order to sort rows by. Traffic, cohort, journey and sale-item reports keep the engine's own order and ignore it. timeSegmentation: type: object description: Splits metrics into time buckets, by interval or window. Only the source family supports it. examples: journey: summary: Lead journey report over two source categories value: configuration: groupBy: JOURNEY startDate: '2026-09-01' endDate: '2026-09-07' timeZone: '-03:00' options: journeyKeyMetric: TOTAL_REVENUE filters: sourceFilters: includeSourceFilters: sourceCategoryIds: [4412, 4413] scientificByLeadTags: summary: Source report under the scientific model, counting only leads with a given tag description: > Keeps the leads holding at least one of the two tags. Send `tagSearchType: ALL_OF_THEM` instead to keep only the leads holding both. value: configuration: groupBy: SOURCE startDate: '2026-09-01' endDate: '2026-09-30' timeZone: '-05:00' reportAttribution: attributionModel: SCIENTIFIC filters: leadFilters: leadIncludeFilters: tags: ['!application', '!stacey-application'] tagSearchType: ANY_OF_THEM totalSalesComparison: summary: Total sales, this quarter against the previous one value: configuration: groupBy: SALE_ITEM_TOTAL startDate: '2026-01-01' endDate: '2026-03-31' options: comparison: enabled: true compareBy: DATES groupA: {} groupB: {} responses: '200': description: Report accepted for generation. `result` is the report id to poll with. content: application/json: schema: type: object properties: request_id: type: string result: type: string description: Report id. example: request_id: 0f8c1a1e3f1e4a0e9d1a6b0a2f6b0d21 result: c81c6061f1f74ba4e338f7c1010493ba23da23440c00a896edddd51e5815ae21 '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Unauthorized. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Forbidden. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/reports/poll/{externalId}: get: tags: [Reports] summary: Poll Report Result description: | > [!NOTE] > **Required role** — Get Reports Read a report started with `POST /api/v1.0/reports/generate`. Poll again while the status is `IN_PROGRESS`. A report generated too long ago comes back `EXPIRED` and has to be generated again. `totalRows` is the size of the whole report, not of the page returned, so it is what tells you whether there are more pages to read. parameters: - name: externalId in: path required: true description: Report id answered by the generate call. schema: type: string - name: pageSize in: query description: Rows per page. Range 1-250. Omit to get every row of the report. schema: type: integer - name: pageOffset in: query description: Zero-based index of the page to read. There are more pages while `(pageOffset + 1) * pageSize` is below `totalRows`. schema: type: integer responses: '200': description: Report status, and the report itself once the status is SUCCESSFUL. content: application/json: schema: type: object properties: request_id: type: string result: type: object properties: status: type: string enum: [IN_PROGRESS, SUCCESSFUL, FAILED, EXPIRED] report: type: object description: Absent until the status is SUCCESSFUL. properties: rows: type: array items: type: object totals: type: object description: A row carrying the totals of the report, on the families that publish one. totalRows: type: integer description: Size of the whole report, not of the page returned. example: request_id: 0f8c1a1e3f1e4a0e9d1a6b0a2f6b0d21 result: status: SUCCESSFUL report: rows: - journeyId: 8a1f0c9d21 totalRevenue: 600.00 journeySteps: - name: facebook adSourceLevelId: 4412 adspendType: FACEBOOK totalRows: 5 '400': description: Bad Request. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Unauthorized. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Forbidden. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/v1.0/requests/{request_id}: get: tags: [Request Status] summary: Retrieve Request Status description: | > [!NOTE] > **Required role** — Get Request Status Write operations are processed **asynchronously** (see *Asynchronous Processing*), so a read-back immediately after a `200` may not yet reflect the change. Instead of retrying the write — which can create duplicates — poll this endpoint with the `request_id` the write returned to learn whether it is still `PENDING`, has been `PROCESSED`, or `FAILED`. Statuses are retained for 2 hours. `PROCESSED` means the resource has been stored **and** indexed; it will appear on its own endpoint shortly, though a read-back within a few seconds of the `PROCESSED` status may not yet reflect it. `PENDING` means the write has not been confirmed yet; it is not a failure, and it is never a reason to resend the write. A request that stays `PENDING` until it expires should be reported to support rather than retried — the resource was most likely created, and sending it again would duplicate it. Once the status is `PROCESSED`, the response also carries a nested `result` holding the resource the write produced, in the same shape its own endpoint returns: for a lead, the `lead` object of `GET /leads`; for a bulk tag request (`POST /leads/tags`), the array of the tagged leads as `GET /leads` returns them; for an order, the array of its sales as `GET /sales` returns them; for a cart, the `cart` object of `GET /carts`; for a call, the `call` object of `GET /calls`, or the array of them when the write targeted several ids; for a source, the `source` object of `GET /sources`; for a custom cost, the `custom cost` object of `GET /custom-costs`; for a product, the `product` object of `GET /products`; for a subscription, the `subscription` object of `GET /subscriptions`, or the array of them when the write targeted several ids; for a click, the `click` object of `GET /leads/clicks`, with `leadId` and `email` empty until the click is connected to a lead. This body is a **snapshot taken at the moment the write completed**, not a live read: it reflects the state right after your operation and does not pick up later changes. Use it to confirm what your write produced; use the resource's own endpoint for its current state. A write that produces nothing readable — a deletion — is reported as `PROCESSED` with no `result` body. A `request_id` that does not belong to your account is reported as not found. parameters: - name: request_id in: path required: true description: The `request_id` returned when the original write request was accepted. schema: type: string example: 251b9b0e20a441fba3d3c346579e7105 responses: '200': description: Successful response. content: application/json: schema: type: object properties: result: $ref: '#/components/schemas/RequestStatus' request_id: type: string examples: processed: summary: A processed lead creation value: result: requestId: 251b9b0e20a441fba3d3c346579e7105 status: PROCESSED eventType: CREATE_LEAD createdAt: '2026-07-21T21:17:03Z' processedAt: '2026-07-21T21:20:41Z' result: email: lead1@email.com id: 40b5af5444756c2b5e666fcb658affd2a4b455bce3711c43f88763147381e368 creationDate: '2023-01-04T04:36:41-05:00' isOriginLead: true tags: ['$ettst'] adOptimizationConsent: GRANTED currentStage: name: SQL date: '2023-01-06T09:12:00-05:00' request_id: 9f2c1d7e4b8a4c1d9e0f5a6b7c8d9e0f processedCustomCost: summary: A processed custom cost creation value: result: requestId: 43573923369e40bbafd46925a5c15ff5 status: PROCESSED eventType: CREATE_CUSTOM_COST createdAt: '2026-08-20T14:02:11Z' processedAt: '2026-08-20T14:02:19Z' result: id: '12345' name: Monthly Agency Fee cost: 999.5 frequency: DAILY startDate: '2026-08-01T00:00' tags: ['@instagram', '@facebook'] request_id: 9f2c1d7e4b8a4c1d9e0f5a6b7c8d9e0f processedOrder: summary: A processed order creation (result is the array of the order's sales) value: result: requestId: 7c3e1a9d54b2489fae0c2b6d1f3a8e10 status: PROCESSED eventType: CREATE_ORDER createdAt: '2026-08-20T14:02:11Z' processedAt: '2026-08-20T14:02:19Z' result: - id: sale-a1 orderId: order-abc qualified: true score: 1 recurring: false quantity: 1 - id: sale-a2 orderId: order-abc qualified: true score: 1 recurring: false quantity: 2 request_id: 9f2c1d7e4b8a4c1d9e0f5a6b7c8d9e0f processedCart: summary: A processed cart creation value: result: requestId: 5a1c8e2b7d9f43c0b6e4a1d2f8c3b5a7 status: PROCESSED eventType: CREATE_CART createdAt: '2026-08-20T14:02:11Z' processedAt: '2026-08-20T14:02:19Z' result: id: cart-abc orderId: order-xyz creationDate: '2022-11-17T13:51:54Z' events: 3 request_id: 9f2c1d7e4b8a4c1d9e0f5a6b7c8d9e0f processedSource: summary: A processed source creation value: result: requestId: 2f9b4c1e6a3d47f8b0c5e2a1d9f3b7c4 status: PROCESSED eventType: CREATE_SOURCE_LINK createdAt: '2026-08-20T14:02:11Z' processedAt: '2026-08-20T14:02:19Z' result: name: FB - Prospecting tag: fb-prospecting disregarded: false organic: false creationDate: 1677151375000 request_id: 9f2c1d7e4b8a4c1d9e0f5a6b7c8d9e0f processedCall: summary: A processed call creation value: result: requestId: 8d2a6f0c9b1e45d3a7c4b2e1f6a9d0c5 status: PROCESSED eventType: CREATE_CALL createdAt: '2026-08-20T14:02:11Z' processedAt: '2026-08-20T14:02:19Z' result: id: call-123 tag: $sales-call qualified: true name: Discovery Call externalId: ext-987 score: 1 creationDate: 'Thu Nov 17 10:51:54 ART 2022' request_id: 9f2c1d7e4b8a4c1d9e0f5a6b7c8d9e0f processedProduct: summary: A processed product creation value: result: requestId: 3e7b1d9a2c8f46b0a5d3c1e2f7b4a6d8 status: PROCESSED eventType: CREATE_PRODUCT createdAt: '2026-08-20T14:02:11Z' processedAt: '2026-08-20T14:02:19Z' result: id: px-abc123 name: Pro Plan tag: $pro sku: SKU-001 price: 49.00 customCost: 5.00 recurring: true callProduct: false category: Software request_id: 9f2c1d7e4b8a4c1d9e0f5a6b7c8d9e0f processedClick: summary: A processed click creation (leadId and email empty until connected to a lead) value: result: requestId: 1b6d3f8a0c2e49b7a4d1c5e3f9b2a7d6 status: PROCESSED eventType: CREATE_CLICK createdAt: '2026-08-20T14:02:11Z' processedAt: '2026-08-20T14:02:19Z' result: id: click-abc leadId: '' email: '' date: 'Thu Nov 17 10:51:54 ART 2022' trackedUrl: https://example.com/lp?utm_source=fb page: https://example.com/lp adspendType: FACEBOOK sourceLinkName: FB - Prospecting ip: 203.0.113.5 agent: Mozilla/5.0 request_id: 9f2c1d7e4b8a4c1d9e0f5a6b7c8d9e0f processedSubscription: summary: A processed subscription creation value: result: requestId: 9c4e2a7b1d6f43a8b0c3e5d2f1a9b8c7 status: PROCESSED eventType: CREATE_SUBSCRIPTION createdAt: '2026-08-20T14:02:11Z' processedAt: '2026-08-20T14:02:19Z' result: id: sub-abc startDate: 'Thu Nov 17 10:51:54 ART 2022' price: 49.00 status: ACTIVE periodicity: MONTHLY planId: plan-pro tag: $pro name: Pro Plan request_id: 9f2c1d7e4b8a4c1d9e0f5a6b7c8d9e0f failed: summary: A failed write value: result: requestId: 251b9b0e20a441fba3d3c346579e7105 status: FAILED eventType: CREATE_LEAD errorMessage: Invalid email format createdAt: '2026-07-21T21:17:03Z' processedAt: '2026-07-21T21:17:44Z' request_id: 9f2c1d7e4b8a4c1d9e0f5a6b7c8d9e0f '400': description: Bad Request. Returned when the `request_id` does not exist for this account. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: message: ["The externalId 251b9b0e20a441fba3d3c346579e7105 does not exist for this user"] '401': description: Unauthorized. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse'