GraphQL API Documentation
Overview
A single GraphQL endpoint that mirrors the core REST resources so callers can fetch exactly the fields they need in one round trip. Authentication, authorization, and the underlying Searchkick services are shared with the REST API, so a GraphQL query returns the same records the equivalent REST call would.
Endpoint
URL
POST /api/v1/graphql
Body: { "query": "…", "variables": {…}, "operationName": "…" }
Authentication
X-Api-Key: YOUR_API_KEY
Same as REST. Bearer (Auth0) is also accepted.
API Playground
Build and run GraphQL queries against your account in the browser. Requests use your API key when signed in. Sign in to enable live testing with your API key.
Response
—
Equivalent curl
Authentication
GraphQL uses the same credentials and rate-limits as the REST API. Send your key on every request:
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{"query":"{ __typename }"}'
Missing or invalid credentials return 401 Unauthorized in the standard REST error envelope
({ error: { code, message, … } }) rather than the GraphQL errors array, so existing
tooling that branches on HTTP status keeps working.
What's exposed
Run an __schema introspection query (or the "Schema introspection" example in the playground)
for the full field/argument list. The core surface:
Single-record lookups
advertiser(id, slug)deal(id)transaction(id)term(id)publisher(id, slug)network(id, slug)tag(id, slug)accountActivitySummary(id)
Paginated lists
All list fields return { nodes, pageInfo }. pageInfo always carries
currentPage, perPage, nextPage, prevPage, and
hasNextPage. totalCount / totalPages are only populated when
includeTotal: true is requested (capped at 100,000).
advertisers(publisherId, networkId, status, query, …)— Searchkick-backeddeals(advertiserId, exclusive, featured, device, activeOn, includeHidden, …)— visible deals by default; opt in to hidden withincludeHidden: true(full reference)transactions(advertiserId, publisherId, networkId, startDate, endDate, status, …)— Searchkick-backed, bounded query required (full reference)contacts(advertiserId, advertiserName, domain, email, query, …)— Searchkick-backed HiEnergy contacts, bounded query required (full reference)contact(id)— single contact by idclicks(startDate!, endDate!, publisherId, advertiserId, …)— Searchkick-backed, 90-day window (full reference)terms(advertiserId, campaignId, query, …)— commission terms, paid-account scope (full reference)publishers(query, …)networks(active, …)tags(query, …)accountActivitySummaries(summaryDate, accountType, accountId, …)— paid accounts only
Resource references
Every resource below has an argument table, field list, and parity notes vs REST: Advertisers, Deals, Clicks, Transactions, Terms, Publishers / Networks / Tags, and Account activity summaries. New to the schema? Start with Writing queries and Types & scalars.
Writing queries
The endpoint accepts a JSON body with three keys: query (the GraphQL document,
required), variables (a JSON object, optional), and operationName
(required only when a document defines more than one operation). Everything is a
query operation — the schema exposes no mutations or subscriptions.
Variables
Prefer variables over string interpolation so values are typed and escaped by the server.
Declare them in the operation signature and pass them in the variables object.
{
"query": "query Deals($advertiserId: ID, $limit: Int) { deals(advertiserId: $advertiserId, perPage: $limit) { nodes { id name } pageInfo { hasNextPage } } }",
"variables": { "advertiserId": "123", "limit": 10 }
}
Aliases & multiple root fields
Fetch several resources in one round trip, and use aliases to disambiguate repeated fields.
query Dashboard($advertiserId: ID!, $from: IsoDate!, $to: IsoDate!) {
advertiser(id: $advertiserId) { id name rocketScore }
activeDeals: deals(advertiserId: $advertiserId) { pageInfo { totalCount } }
clicks(startDate: $from, endDate: $to, advertiserId: $advertiserId) {
nodes { clickDate clickCount }
}
}
Fragments
Reuse a selection set across fields with a fragment.
query($advertiserId: ID) {
deals(advertiserId: $advertiserId) { nodes { ...DealCard } }
}
fragment DealCard on Deal {
id
name
exclusive
advertiser { id name }
}
Only select what you need
Resolvers use lookahead: nested associations (e.g. advertiser,
network, publisher) are only eager-loaded when your selection set
includes them, so asking for { id } avoids the joins that
{ id advertiser { network { name } } } triggers. Deeper/wider selections also
cost more against the depth (12) and complexity (5,000) budgets — see
Errors & limits.
Types & scalars
Alongside the GraphQL built-ins (ID, String, Int,
Float, Boolean), the schema defines a few custom scalars and enums.
A trailing ! means non-null; [Type!] is a list.
| Type | Kind | Description |
|---|---|---|
ID | scalar | Serialized as a string. Numeric ids are returned as strings (e.g. "123"); most id arguments also accept a slug where noted. |
IsoDate | scalar | ISO-8601 calendar date YYYY-MM-DD. Invalid input returns a coercion error. |
IsoTime | scalar | ISO-8601 timestamp (date + time, UTC) for createdAt/updatedAt/window fields. |
TransactionStatus | enum | PENDING, APPROVED, PAID, REJECTED. |
*Connection | object | List wrapper: { nodes, pageInfo } (e.g. DealConnection, TransactionConnection). |
PageInfo | object | Pagination metadata — see Pagination & totals. |
PageInfo fields
| Field | Type | Description |
|---|---|---|
currentPage | Int | The page that was returned |
perPage | Int | Page size in effect (after clamping) |
nextPage, prevPage | Int | Adjacent page numbers, or null at the ends |
hasNextPage | Boolean | Always populated (n+1 lookahead) |
totalCount, totalPages | Int | Only when includeTotal: true (capped at 100,000) |
Advertisers
Look up a single advertiser with advertiser(id:, slug:) or search the paginated
advertisers list. Search is served by the same Searchkick/Elasticsearch service as
GET /api/v1/advertisers, and results are
scoped by AdvertiserPolicy.
Queries
| Field | Arguments | Returns |
|---|---|---|
advertiser |
id: ID and/or slug: String (one required) |
Advertiser or null when not found or not authorized |
advertisers |
See filters below + page, perPage, includeTotal |
AdvertiserConnection — { nodes, pageInfo } |
List filters (advertisers)
| Argument | Type | Description |
|---|---|---|
query | String | Free-text name/domain search (Searchkick) |
publisherId | ID | Restrict to a publisher (admins; non-admins stay in scope) |
networkId | ID | Restrict to a network |
status | String | Advertiser status filter |
page, perPage, includeTotal | pagination | Defaults 1 / 25 (max 200) / false |
Advertiser fields
Grouped for readability; run introspection for the exhaustive list.
| Group | Fields |
|---|---|
| Identity | id, cid, name, displayName, slug, url, domain, description, status, programStatus, programDetails, closureDate, contactEmail |
| Media | iconUrl, logoUrl, largeLogoUrl |
| Relationships | network, networkId, networkName, publisher, publisherId, publisherName, masterAdvertiserId |
| Commission | commissionRate, averageCommissionRate, defaultCommissionRate, maxCommissionRate, minCommissionRate, medianCommissionRate, networkDefaultCommission, networkMaxCommission, networkMinCommission, commissionSummary, flatRateCommissionCents, flatRateCommissionCurrency, exclusionsDescription |
| Counts | transactionsCount, campaignsCount, contactsCount, statusChangesCount, termsCount, dealsCount |
| Metrics | totalSales, totalCommissions, estimatedGrossRevenue, estimatedSalesYearly, estimatedPageViews, trustPilotReviewCount, trustPilotAverageRating, rocketScore, rankPercentile, pageViewPercentile, revenuePercentile |
| Social | linkedinUrl, facebookUrl, twitterUrl, instagramUrl, pinterestUrl, youtubeUrl |
| Taxonomy lists | countryList, verticalList, domainList, vibeList, demographicList, seasonList, riskList |
| Timestamps | lastNetworkSyncAt, createdAt, updatedAt |
Deals
Query affiliate deals through deal(id) or the paginated deals list.
Both use Pundit DealPolicy scopes, matching
GET /api/v1/deals.
deals list defaults to
hidden: false (same as REST GET /api/v1/deals). To include hidden
deals you are authorized to see, pass includeHidden: true — results stay scoped
by DealPolicy either way. deal(id:) returns a hidden deal directly
when your API key is allowed to see it.
Queries
| Field | Arguments | Returns |
|---|---|---|
deal |
id: ID! |
Deal or null when not found or not authorized |
deals |
See filter table below + page, perPage, includeTotal |
DealConnection — { nodes, pageInfo } |
List filters (deals)
| Argument | Type | Description |
|---|---|---|
advertiserId |
ID |
Restrict to one advertiser |
exclusive |
Boolean |
Exact match on exclusive flag |
featured |
Boolean |
Exact match on featured flag |
device |
String |
Exact device targeting match: all, desktop, mobile, or tablet |
activeOn |
IsoDate |
Deals effective on/before end-of-day and not expired before start-of-day |
includeHidden |
Boolean |
Include deals flagged hidden. Defaults to false (hidden deals omitted). When true, hidden deals the caller can access are returned alongside visible ones. |
page |
Int |
Page number (default 1) |
perPage |
Int |
Page size (default 25, max 200) |
includeTotal |
Boolean |
When true, populates pageInfo.totalCount (capped at 100,000) |
Deal fields
| Field | Type | Description |
|---|---|---|
id | ID! | Deal ID |
name, description, terms | String | Deal copy |
descriptionIntentionallyBlank | Boolean | Empty description is intentional when true |
link, code | String | Tracking URL / promo code when present |
exclusive, featured, hidden | Boolean | Deal flags (deals list omits hidden deals unless includeHidden: true) |
dealKind | String | Deal kind / type label |
device | String | Device targeting: all, desktop, mobile, or tablet |
effectiveAt, expiresAt | IsoTime | Availability window |
advertiser | Advertiser | Nested advertiser (loaded when selected) |
advertiserId, campaignId | ID | Foreign keys |
countries | [String!] | Country codes when scoped geographically |
createdAt, updatedAt | IsoTime | Record timestamps |
Clicks
Query daily advertiser click roll-ups through the paginated clicks field.
Uses the same Searchkick-backed ClicksSearchkickService as
GET /api/v1/clicks, with
ClickPolicy#index? and post-load advertiser visibility filtering.
There is no click(id) singleton (parity with REST).
startDate and endDate are required,
endDate must be ≥ startDate, and the inclusive range may not exceed
90 days. Filter by advertiserId and/or publisherId when you need a tighter slice.
Queries
| Field | Arguments | Returns |
|---|---|---|
clicks |
startDate!, endDate!, optional filters + pagination |
ClickConnection — { nodes, pageInfo } |
List filters (clicks)
| Argument | Type | Description |
|---|---|---|
startDate, endDate |
IsoDate! |
Required inclusive click-date range (YYYY-MM-DD); max 90 days |
advertiserId |
ID |
Numeric advertiser ID — applied in Searchkick and SQL fallback |
publisherId |
ID |
Publisher scope (admins only; non-admins stay on visible publishers) |
page |
Int |
Page number (default 1) |
perPage |
Int |
Page size (default 25, max 200) |
includeTotal |
Boolean |
When true, populates pageInfo.totalCount (capped at 100,000) |
Click fields
| Field | Type | Description |
|---|---|---|
id | ID! | Roll-up row ID |
clickDate | IsoDate | UTC date of the aggregated clicks |
clickCount | Int | Number of clicks that day |
advertiserId, advertiserName, advertiserSlug | scalars | Advertiser identity on the roll-up |
publisherId, publisherName | scalars | Publisher identity (when in scope) |
networkId, networkName | scalars | Network identity (loaded when selected) |
Transactions
Query affiliate transactions through transaction(id) or the paginated
transactions list. Both use the same Searchkick-backed
TransactionsSearchkickService as GET /api/v1/transactions,
with Pundit authorization applied after Searchkick returns matching IDs.
advertiserId, publisherId, networkId, or
both startDate and endDate.
Explicit date windows cannot exceed 90 days. When dates are omitted
but another bound is present, Elasticsearch is limited to the last 30 days.
Unbounded transactions queries return a GraphQL validation error, and
the resolver never falls back to scanning SQL.
q, sort_by, currency, campaign_id, and
amount/rate ranges are not yet on the GraphQL transactions field — use REST when you need those.
Queries
| Field | Arguments | Returns |
|---|---|---|
transaction |
id: ID! |
Transaction or null when not found or not authorized |
transactions |
See filter table below + page, perPage, includeTotal |
TransactionConnection — { nodes, pageInfo } |
List filters (transactions)
| Argument | Type | Description |
|---|---|---|
advertiserId |
ID |
Numeric advertiser ID or slug |
publisherId |
ID |
Publisher scope (admins only; non-admins are always scoped to visible publishers) |
networkId |
ID |
Numeric network ID or slug |
startDate, endDate |
IsoDate |
Inclusive transaction-date range (YYYY-MM-DD). Providing one without the other is invalid; both count as a query bound. |
status |
[TransactionStatus!] |
One or more of PENDING, APPROVED, PAID, REJECTED |
page |
Int |
Page number (default 1) |
perPage |
Int |
Page size (default 25, max 200) |
includeTotal |
Boolean |
When true, populates pageInfo.totalCount (capped at 100,000) |
Transaction fields
| Field | Type | Description |
|---|---|---|
id | ID! | Hi Energy transaction ID |
cid | String | Network-side transaction identifier |
transactionDate | IsoDate | Transaction date |
saleAmount, commissionAmount | Float | Decimal amounts (not cents) |
commissionRate | Float | Commission rate percentage |
currency | String | ISO currency code |
quantity | Int | Line-item quantity when reported by the network |
status | TransactionStatus! | Normalized lifecycle status |
rawStatus | String | Original status token from the network payload |
statusSource | String | rawResponse field path used for normalization |
statusTimestamps | TransactionStatusTimestamps! | Per-status timestamps and *Source provenance fields |
advertiser, network, publisher | nested types | Nested associations (loaded only when selected) |
campaignId | ID | Linked campaign ID when present |
createdAt, updatedAt | IsoTime | Record timestamps |
TransactionStatus enum
PENDING, APPROVED, PAID, REJECTED — derived from each network's
rawResponse using the same normalization rules as the REST API.
statusTimestamps sources
sync_observed— Hi Energy saw the status transition during syncbackfill_network— date reported by the affiliate network inrawResponsebackfill_transaction_date,backfill_created_at,backfill_updated_at— best-effort historical inference
Contacts
Query HiEnergy marketing contacts through contact(id) or the paginated
contacts list. Both use the same Searchkick-backed
ContactSearchService as GET /api/v1/contacts,
so list queries stay fast against millions of rows without falling back to SQL.
Results always require rating >= 1. MCP-created contacts are stamped with
source HiEnergy MCP (caller email) and
lastEditedBy set to the authenticated MCP caller.
advertiserId, advertiserName, domain,
email, or query. Unbounded contacts requests return a GraphQL error.
| Field | Purpose |
|---|---|
contact(id) |
Single contact by id (null when missing / unauthorized) |
contacts |
Paginated HiEnergy contacts (rating >= 1), ordered by rating then updatedAt |
List filters
| Argument | Notes |
|---|---|
advertiserId | HiEnergy advertiser id or slug |
advertiserName | Free-text advertiser name match |
domain | Advertiser domain (e.g. aloyoga.com) |
email | Exact email match |
query | Free-text over name/email/title/LinkedIn |
source | Exact contacts.source value (MCP creates use HiEnergy MCP (caller email)) |
hasLinkedin | Only contacts with a LinkedIn URL |
includeUnverified | Include status unverified (default false); does not relax rating ≥ 1 |
page / perPage / includeTotal | Same pagination contract as other list fields |
Terms
Query AI-extracted advertiser commission terms through term(id) or the
paginated terms list. Both use Pundit TermPolicy scopes.
createdAt desc.
Queries
| Field | Arguments | Returns |
|---|---|---|
term |
id: ID! |
Term or null when not found or not authorized |
terms |
See filter table below + page, perPage, includeTotal |
TermConnection — { nodes, pageInfo } |
List filters (terms)
| Argument | Type | Description |
|---|---|---|
advertiserId |
ID |
Restrict to one advertiser |
campaignId |
ID |
Restrict to one campaign |
query |
String |
Free-text filter matched against name, cid, and summary |
page |
Int |
Page number (default 1) |
perPage |
Int |
Page size (default 25, max 200) |
includeTotal |
Boolean |
When true, populates pageInfo.totalCount (capped at 100,000) |
Term fields
| Field | Type | Description |
|---|---|---|
id | ID! | Terms record ID |
cid | String | Network-side identifier the terms were ingested under |
name | String | Terms name/label |
summary | String | AI-generated plain-language summary of the commission terms |
advertiser | Advertiser | Nested advertiser (loaded when selected) |
advertiserId, campaignId | ID | Foreign keys |
createdAt, updatedAt | IsoTime | Record timestamps |
Publishers, networks & tags
Reference resources for resolving ids and building filters. Publishers are scoped by
PublisherPolicy (you see your own publisher plus any you manage; admins see all);
networks and tags are global catalogs.
Queries
| Field | Arguments | Returns |
|---|---|---|
publisher | id: ID and/or slug: String | Publisher or null |
publishers | query: String + pagination | PublisherConnection |
network | id: ID and/or slug: String | Network or null |
networks | active: Boolean + pagination | NetworkConnection |
tag | id: ID and/or slug: String | Tag or null |
tags | query: String + pagination | TagConnection |
Publisher fields
id, name, slug, domain, description |
publisherType, companyType, headquarters |
linkedinProfileUrl |
network, networkId |
hideDeals, linkGeneratorEnabled, applicationRequestsEnabled |
lastPaidAt, createdAt, updatedAt |
Network fields
id, name, slug |
active, subaffiliate |
createdAt, updatedAt |
Tag fields
id, name, slug |
taggingsCount |
createdAt, updatedAt |
Account activity summaries
Daily Markdown digests of the preceding 7 days for a publisher or agency, via
accountActivitySummary(id) or the paginated accountActivitySummaries
list. Scoped by AccountActivitySummaryPolicy to the caller's paid publishers and
managed agencies.
403). Admins see all accounts.
Queries & filters
| Field / Argument | Type | Description |
|---|---|---|
accountActivitySummary | id: ID! | One summary, or null |
accountActivitySummaries | filters + pagination | AccountActivitySummaryConnection |
summaryDate | IsoDate | Filter to a single day |
accountType | String | Publisher or Agency |
accountId | ID | A publisher or agency id within your scope |
AccountActivitySummary fields
| Field | Type | Description |
|---|---|---|
id | ID! | Summary ID |
accountType, accountId, accountName | scalars | The publisher or agency the digest is for |
summaryDate | IsoDate! | Digest day |
periodStart, periodEnd, generatedAt | IsoTime! | 7-day window covered and when it was built |
statusChangesText, performanceText, connectedNetworksText, commissionDropsText, keyPointsText | String! | Individual Markdown sections |
markdown | String! | Combined Markdown digest (sections + transactions, deals, recommendations) |
Pagination & totals
Every list field accepts page, perPage, and includeTotal:
pagedefaults to1.perPagedefaults to25, clamped at200.includeTotal: truereturnspageInfo.totalCount/totalPages. Off by default — counting on huge tables is slow.pageInfo.hasNextPageis always populated (using the standard n+1 trick).- Hard cap:
page * perPagemust be ≤ 10,000. Use filters (date range, IDs) instead of deep paging.
Examples
1) Search advertisers by name with totals
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{
"query": "query($q: String) { advertisers(query: $q, perPage: 5, includeTotal: true) { nodes { id name domain network { name } } pageInfo { hasNextPage totalCount } } }",
"variables": { "q": "nike" }
}'
2) Visible deals for an advertiser
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{
"query": "query($advertiserId: ID) { deals(advertiserId: $advertiserId, perPage: 10, includeTotal: true) { nodes { id name exclusive featured effectiveAt expiresAt advertiser { id name } } pageInfo { totalCount hasNextPage } } }",
"variables": { "advertiserId": "123" }
}'
2b) Include hidden deals (opt-in)
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{
"query": "query($advertiserId: ID) { deals(advertiserId: $advertiserId, includeHidden: true, perPage: 10) { nodes { id name hidden } pageInfo { hasNextPage } } }",
"variables": { "advertiserId": "123" }
}'
3) Clicks for a 7-day window, filtered by advertiser
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{
"query": "query($from: IsoDate!, $to: IsoDate!, $advertiserId: ID) { clicks(startDate: $from, endDate: $to, advertiserId: $advertiserId, perPage: 50) { nodes { id clickDate clickCount advertiserName networkName } pageInfo { hasNextPage } } }",
"variables": { "from": "2026-05-19", "to": "2026-05-26", "advertiserId": "123" }
}'
4) Transactions for a date range, with normalized status
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{
"query": "query($from: IsoDate!, $to: IsoDate!) { transactions(startDate: $from, endDate: $to, perPage: 25) { nodes { id transactionDate saleAmount commissionAmount status rawStatus statusSource statusTimestamps { pendingAt approvedAt paidAt rejectedAt pendingAtSource } advertiser { id name } } pageInfo { hasNextPage } } }",
"variables": { "from": "2026-04-01", "to": "2026-04-30" }
}'
5) Filter transactions by normalized status
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{
"query": "query($from: IsoDate!, $to: IsoDate!, $statuses: [TransactionStatus!]) { transactions(startDate: $from, endDate: $to, status: $statuses, perPage: 25, includeTotal: true) { nodes { id status rawStatus } pageInfo { totalCount } } }",
"variables": { "from": "2026-04-01", "to": "2026-04-30", "statuses": ["APPROVED", "PAID"] }
}'
6) Fetch a single transaction by ID
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{
"query": "query($id: ID!) { transaction(id: $id) { id transactionDate saleAmount commissionAmount status rawStatus statusSource statusTimestamps { pendingAt approvedAt paidAt rejectedAt pendingAtSource } advertiser { id name } network { name } } }",
"variables": { "id": "12345" }
}'
7) Commission terms for an advertiser
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{
"query": "query($advertiserId: ID) { terms(advertiserId: $advertiserId, perPage: 10, includeTotal: true) { nodes { id cid name summary advertiser { id name } } pageInfo { totalCount hasNextPage } } }",
"variables": { "advertiserId": "123" }
}'
8) Look up a publisher and a network by slug
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{
"query": "{ publisher(slug: \"acme-media\") { id name network { id name } } network(slug: \"awin\") { id name active } }"
}'
9) Account activity summary for a day
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{
"query": "query($day: IsoDate) { accountActivitySummaries(summaryDate: $day, perPage: 5) { nodes { id accountType accountName summaryDate markdown } pageInfo { hasNextPage } } }",
"variables": { "day": "2026-07-14" }
}'
10) Introspect the available query fields
curl -X POST https://app.hienergy.ai/api/v1/graphql \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ROCKET_API_KEY" \
-d '{
"query": "{ __schema { queryType { fields { name args { name type { name ofType { name } } } } } } }"
}'
Errors & limits
Two error channels
Authentication failures short-circuit before GraphQL runs and return the standard REST error envelope with an HTTP error status — so status-code branching keeps working:
HTTP/1.1 401 Unauthorized
{ "error": { "code": "unauthorized", "message": "Missing or invalid credentials" } }
Validation / business errors (unbounded transactions, click window
> 90 days, depth/complexity exceeded, search temporarily unavailable, coercion failures) follow
the GraphQL convention: HTTP 200 with an errors array and
data: null for the failed field.
HTTP/1.1 200 OK
{
"data": null,
"errors": [
{ "message": "Transactions require advertiserId, publisherId, networkId, or both startDate and endDate." }
]
}
Always check for a non-empty errors array even on a 200 response. A
partial success (some fields resolved, others errored) returns both data and
errors.
Limits
- Query depth is capped at
12for data queries. Introspection (__schema/__type) is exempt, so standard client/tooling introspection works. - Query complexity is capped at
5,000(list fields weight10; keep selections focused). Introspection fields don't count toward this budget. - Pagination depth (
page * perPage) is capped at10,000. Use filters (date range, ids) instead of deep paging. - Page size (
perPage) is clamped to200. - Reported totals are capped at
100,000to keep counts bounded. - Rate limiting uses the same per-key limits and API-call accounting as the REST API; a GraphQL request counts as one API call.
Per-resource rules
- Deals list returns visible deals only (
hidden: false) by default, matching REST; passincludeHidden: trueto include authorized hidden deals. - Clicks date range is required and limited to
90days. - Transactions require at least one bound (
advertiserId,publisherId,networkId, or both dates); explicit date windows are capped at 90 days, and omitted dates default to the last 30 days in Elasticsearch. - Terms and account activity summaries require paid-account access; free/agency-only users may receive an empty list or a
403.