Analytics

LinkedIn Ads data into PostgreSQL: four API limits

No pagination, a 15,000-element cap, 45 million metric values per five minutes and silent month rounding: the LinkedIn reporting API punishes a naive nightly dump. Here is the Postgres design that survives it.

LinkedIn Ads data into PostgreSQL: isometric data drawers feeding a conveyor into a cylindrical tank, one drawer orange.

Bottom line

Getting your LinkedIn Ads data into PostgreSQL reliably means designing around four documented limits. They are no pagination, a 15,000-element cap, 45 million metric values per five minutes, and month rounding past six months.

  • Key your facts by pivot URN, date, grain and API version; keep your raw JSONB with its element count.
  • Size your calls at 12,000 elements or fewer.
  • Reconcile at full period and account level, never by summing days.
  • Pin your Linkedin-Version header: 202511 stops on 16 November 2026.
LinkedIn Ads into PostgreSQL at a glance Four figures. 15,000: the most elements one adAnalytics response returns, with no pagination. 45M: metric values allowed per 5 minutes across all queries, with HTTP 429 above it. 6: months of daily retention; with ALL granularity older dates round to month boundaries. 16 November 2026: version 202511 sunsets, so move the version pin before then. LinkedIn Ads into PostgreSQL at a glance 15,000 elements max per response, and no pagination 45M metric values per 5 minutes, all queries; above it, HTTP 429 6 months of daily retention; with ALL, older dates round to month boundaries 16 Nov 2026: version 202511 sunsets; move the pin before then Each figure is stated and sourced in the sections below.
Read on 26 October 2026: LinkedIn's reporting API and recent changes pages.

LinkedIn Ads data into PostgreSQL: the four API limits that decide the design

Four limits shape your LinkedIn Ads data into PostgreSQL design: no pagination, 15,000 elements per response, 45 million metric values per five minutes, and month rounding past six months.

The limits in LinkedIn's words, as a table
adAnalytics limits in LinkedIn's words (reporting API page, version 202610, read on 26 October 2026)
Limit, in LinkedIn's wordsWhat breaks in a nightly dump
"does not support pagination"; "limited to 15,000 elements"Your rows past 15,000 vanish, with no documented error
"45 million metric values" per 5 minutes, across all your queriesOver it, HTTP 429; your stalled backfill looks complete
Impressions and clicks by default; "up to 20 metrics"Spend and conversions missing unless you name them
Under ALL, older dates "rounded to month boundaries"Your old rows are silently monthly
Demographics: top 100 values per creative per day, 3-event minimumApproximate, so your daily sums may not match period totals
LinkedIn Ads to PostgreSQL pipeline Five stages from the adAnalytics API through a chunker, a raw landing table and an upsert into a fact table, to reconciliation against Campaign Manager. Each stage names the limit it absorbs; the 12,000 chunk size is Leapbuzz headroom, not a LinkedIn limit. PIPELINE STAGES AND THE LIMIT EACH ONE ABSORBS adAnalytics API, version header pinned Absorbs sunset 426 errors Chunker under 12,000 elements a call (our headroom) Absorbs 15,000 cap and 45M per 5 min Raw landing one JSONB row per response Absorbs schema changes per version Fact upsert URN, date, grain api_version Absorbs re-pulls and month rounding Reconcile vs Campaign Manager totals Absorbs delays and approximation Orange: the stage most teams skip. Limits: LinkedIn reporting API, 202610; 12,000 is ours.
Limits per LinkedIn's reporting API page (version 202610), read on 26 October 2026. 12,000 is our headroom.

LinkedIn documents no truncation flag, so you should assume none and test. This post covers extraction; our LinkedIn Conversions API pipeline guide covers upload, and both feed one marketing analytics warehouse.

What schema should a LinkedIn Ads warehouse in Postgres use?

You need three tables: raw JSONB responses, a fact table keyed by pivot URN, date, grain and API version, and a URN lookup. No response says which version made it, or if a row is a rounded month.

PostgreSQL schema for LinkedIn Ads data Three PostgreSQL tables. li_ads_raw holds each API response as JSONB with pulled_at, api_version, request, response and a generated element_count. An upsert keeps the newest raw row per key and writes li_ads_fact, whose primary key is pivot_type, pivot_urn, period_start, grain and api_version, with metrics as JSONB and loaded_at. pivot_urn joins li_urn_lookup on urn for display names. THREE TABLES, AND THE KEY THAT MAKES RE-PULLS SAFE li_ads_raw pulled_at api_version request (jsonb) response (jsonb) element_count (generated) li_ads_fact PK pivot_type PK pivot_urn PK period_start PK grain PK api_version metrics (jsonb) loaded_at li_urn_lookup PK urn display_name resolved_at Raw to fact: upsert keeps the newest raw row per key. Orange: alert when element_count is 15,000. grain is DAILY or MONTHLY, never ALL. Typed views filter to one api_version. PK = primary key.
Our design; field names per LinkedIn's reporting schema page, read on 26 October 2026.
Source

"ON CONFLICT DO UPDATE guarantees an atomic INSERT or UPDATE outcome; provided there is no independent error, one of those two outcomes is guaranteed, even under high concurrency."

PostgreSQL Global Development Group, PostgreSQL 18 documentation, INSERT

So your same-version re-pull corrects rows and never duplicates them. One statement may not touch a row twice, so DISTINCT ON keeps the newest raw row per key after a retry.

Show our SQL (tables, single-pivot upsert)
CREATE TABLE li_ads_raw (
  pulled_at     timestamptz NOT NULL DEFAULT now(),
  api_version   char(6) NOT NULL,          -- Linkedin-Version header
  request       jsonb   NOT NULL,
  response      jsonb   NOT NULL,
  element_count int GENERATED ALWAYS AS
                (jsonb_array_length(response->'elements')) STORED
);

CREATE TABLE li_ads_fact (
  pivot_type   text    NOT NULL,           -- CAMPAIGN, CREATIVE, MEMBER_JOB_TITLE
  pivot_urn    text    NOT NULL,
  period_start date    NOT NULL,
  grain        text    NOT NULL,           -- DAILY or MONTHLY, never ALL
  api_version  char(6) NOT NULL,
  metrics      jsonb   NOT NULL,
  loaded_at    timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (pivot_type, pivot_urn, period_start, grain, api_version)
);

CREATE TABLE li_urn_lookup (urn text PRIMARY KEY, display_name text, resolved_at timestamptz);
INSERT INTO li_ads_fact (pivot_type, pivot_urn, period_start, grain, api_version, metrics)
SELECT DISTINCT ON (pivot_type, pivot_urn, period_start)   -- single-pivot example
       'CAMPAIGN' AS pivot_type, e->'pivotValues'->>0 AS pivot_urn,
       make_date((e->'dateRange'->'start'->>'year')::int,
                 (e->'dateRange'->'start'->>'month')::int,
                 (e->'dateRange'->'start'->>'day')::int) AS period_start,
       'DAILY', r.api_version, e - 'pivotValues' - 'dateRange'
FROM li_ads_raw r, jsonb_array_elements(r.response->'elements') e
WHERE r.pulled_at >= now() - interval '1 hour'
ORDER BY pivot_type, pivot_urn, period_start, r.pulled_at DESC
ON CONFLICT (pivot_type, pivot_urn, period_start, grain, api_version)
DO UPDATE SET metrics = EXCLUDED.metrics, loaded_at = now();
Our schema rules, and why
Schema rules and the reason for each (LinkedIn reporting schema page, read on 26 October 2026)
RuleWhy
Your metrics as JSONB, typed views on top202601 added videoWatchTime, 202605 appointmentsScheduled, 202610 memberFollows
Your views filter to one api_versionA new-version re-pull adds rows, so your sums double-count
Two-pivot rows: pivotValues as a text[] keyCreative by job title returns two URNs
Spend from your CAMPAIGN or ACCOUNT pivot onlyDemographic cost "is not adjusted for over delivery"
Keep costInLocalCurrency and costInUsdIf you run accounts out of Singapore, Sydney and Toronto

How do you backfill history without hitting the 15,000-element cap or the 429?

Size each of your calls first: elements are pivot values times days; metric values are elements times fields.

Throttle and chunk estimator (illustrative heuristic)

Calls = elements / 12,000 (our headroom under 15,000), rounded up. Windows = metric values / 45 million, rounded up. Made-up defaults; enter your own.

CHUNK: at least 3 calls, because one call would pass LinkedIn's 15,000-element cap. It fits one 5-minute window, which your app shares with all its other adAnalytics queries.

The same estimator as a table: three illustrative runs
Three illustrative runs (made-up numbers; limits per LinkedIn, 26 October 2026)
RunElementsMetric valuesVerdict
400 campaigns, 20 metrics, 90 days36,000720,000CHUNK: 3 calls
2,000 campaigns, 20 metrics, 366 days732,00014,640,000CHUNK: 61 calls (49 at 15,000)
5,000 creatives x 100 job titles, 20 metrics, 30 days15,000,000300,000,000PACE: 7+ windows, 35 minutes

Backfill your MEMBER_ pivots first, because demographics expire at two years. For periods older than six months, you request MONTHLY with grain = 'MONTHLY'. LinkedIn documents the rounding for ALL only, so test DAILY on your account.

Show our Python planner and the 414 fix
HEADROOM, WINDOW_VALUES = 12_000, 45_000_000

def plan(urns, days, fields, version="202610"):   # days are offsets from dateRange.start
    fields = fields[:20]
    options = [(max(1, HEADROOM // days), min(days, HEADROOM)),             # campaign batches
               (min(len(urns), HEADROOM), max(1, HEADROOM // len(urns)))]   # date slices
    per_call, slice_days = min(options, key=lambda o: -(-len(urns) // o[0]) * -(-days // o[1]))
    for i in range(0, len(urns), per_call):
        for d in range(0, days, slice_days):
            batch, span = urns[i:i + per_call], min(slice_days, days - d)
            assert len(batch) * span <= HEADROOM
            yield {"q": "analytics", "pivot": "CAMPAIGN", "timeGranularity": "DAILY",
                   "campaigns": batch, "fields": ",".join(fields), "dateRange": (d, d + span - 1),
                   "headers": {"Linkedin-Version": version, "X-Restli-Protocol-Version": "2.0.0"},
                   "cost": len(batch) * span * len(fields)}
# Keep sum(cost) per 5 minutes under WINDOW_VALUES; on 429 wait, resend.
# Assert len(response["elements"]) < 15_000 before writing li_ads_raw.

Long URN lists push the query string past 4 KB and LinkedIn answers 414. Use query tunneling: POST with X-HTTP-Method-Override: GET and the query string in a form-encoded body.

Ask Leapbuzz

Never tested your loader against the 15,000 cap? Leapbuzz audits it and writes up your gaps; start the review.

How do you reconcile the API against Campaign Manager totals?

Reconcile at full period and account level, never by summing days: we aim for exact agreement on spend, impressions and clicks, and you allow three units on demographic rows.

What should agree with what (LinkedIn reporting page and Campaign Manager help, read on 26 October 2026)
ComparisonWhat to expect
Account spend, impressions, clicks, full periodExact (our aim; no LinkedIn figure)
Demographic rows, full periodWithin three units "in almost all cases"
Summed days or campaigns against totalsDrifts; LinkedIn's reporting page names both
API day against your account's dayAPI dates are UTC; check your time zone
How long LinkedIn reporting data takes to settle Three rows on an axis of hours from 0 to 48. Performance metrics arrive near real-time, shown as a short marker because LinkedIn gives no figure. Demographics lag 12 to 24 hours, highlighted. Video watch time can lag up to 48 hours. How long LinkedIn reporting data takes to settle 0 12 24 36 48 Performance metrics Demographics Video watch time near real-time (no figure) 12 to 24 hours up to 48 hours Hours after the event, as LinkedIn documents the delays
Source: LinkedIn's reporting API page (version 202610), read on 26 October 2026.

Your conversions keep arriving through the conversion rule's attribution window, up to 365 days (180 added in 202609, all versions). So re-pull your trailing 30 days nightly (our heuristic) and your whole year at yearly close.

Which LinkedIn Marketing API version should the pipeline pin?

Pin the newest version you have tested, put its sunset in your runbook, and move forward quarterly; 202511 sunsets on 16 November 2026.

LinkedIn Marketing API version calendar Timeline of LinkedIn Marketing API versions. 15 October 2026: 202510 sunset. 26 October 2026: our read, when 202610 was the latest version. 16 November 2026: 202511 sunsets, highlighted. 15 December 2026: the 202611 publisher-list migration window ends. October 2027 or later: 202610 sunset, our projection from LinkedIn's one-year minimum support. Version calendar: what the pin has to survive Oct Nov Dec 15 Oct 2026 202510 sunset 26 Oct 2026 our read: 202610 is the latest 16 Nov 2026 202511 sunsets; move the pin first 15 Dec 2026 202611 publisher-list window ends Oct 2027 or later 202610 sunset (our projection) Dates per LinkedIn's versioning and recent changes pages; 2027 is our projection.
Per LinkedIn's versioning page and recent changes page (last moved 19 October), read on 26 October 2026; support lasts "a minimum of one year".
What each version means for you
What each version changes for a reporting pipeline (recent changes page, read on 26 October 2026)
VersionChange for reportingYour pipeline action
202511Older baseline; sunsets 16 November 2026Move off now; re-pull your last 30 days and diff
202601videoWatchTime, averageVideoWatchTime (48-hour delay)Add to your video field set
202609MEMBER_DESIGNATED_MARKET_AREA pivot; Nielsen DMA notice requiredUS DMA geography, so mainly US accounts (our reading): new pivot and lookup rows
202610memberFollows and memberProfileClicks (scope: see FAQ)Pin at writing; add both to your engagement fields
202611 (due)Publisher lists need LAN or CTV; mismatches fail everywhere after 15 DecemberChange your campaign-creation code before 15 December 2026

"The latest version is not applied by default." LinkedIn's examples show 400 VERSION_MISSING if you send no header and 426 NONEXISTENT_VERSION for a sunset one (confirm on adAnalytics).

Should you build this or buy a connector?

Buy a connector if your LinkedIn Ads account is one of ten sources and nobody will own your script. Build if LinkedIn is your top-three channel, you need creative or demographic history, or finance audits the numbers.

Build or buy for LinkedIn Ads data into PostgreSQL (categories, not products; our assessment, 26 October 2026)
RouteWeak spot, and what you should ask
Managed ELT connectorFixed fields; ask how it spots 15,000-element truncation
Open-source framework, self-hostedMay sum demographics by day; ask if it requests ALL
In-house script (our design)Your team owns the sunset calendar; ask who gets 426s

We have not read vendors' documentation, so get your answers in writing. Paid social teams rarely ask; product analytics teams do. Next, see our SaaS page and MCP servers for marketing data.

Is your LinkedIn Ads loader already missing rows?

Your loader can lose rows without an error. Treat a 15,000-element response as truncated until you prove otherwise, then tick what your loader does.

Loader check (Leapbuzz heuristic: must-haves 3 points, others 2)

AT RISK: 0 of 20 points, 4 must-haves missing.

Fix first: pin 202610 or your newest tested version before 16 November 2026; re-pull 30 days, diff, and page someone on a 426.

This scores your answers, not your data. Only your reconciliation table, run on the GA4 reporting pack cadence, proves the warehouse complete; a failed row blocks the refresh.

Frequently asked questions

Does the LinkedIn Ads reporting API support pagination?

No. LinkedIn's reporting API page, read on 26 October 2026 at version 202610, says the adAnalytics endpoint does not support pagination and limits each response to 15,000 elements. It documents no truncation flag or error, so you should assume none and test your own loader. Keep your pivot values times periods at about 12,000 or fewer per call, store your element count, and treat exactly 15,000 elements as truncated until you re-pull in smaller chunks.

What is the LinkedIn adAnalytics rate limit?

LinkedIn limits your data volume, not your request count: 45 million metric values across all of your application's adAnalytics queries in any 5-minute window, then HTTP 429 TOO_MANY_REQUESTS. A metric value is one field on one returned record, so LinkedIn's example of 9 metrics across 2 daily records costs you 18. If you pull a year of daily rows for 2,000 campaigns at 20 metrics, you request about 14.6 million values: one window, but 61 calls at our 12,000-element headroom (49 at the hard 15,000 cap).

How far back can you pull LinkedIn Ads data through the API?

You get ten years of performance data by account, campaign or creative, and two years of professional demographics, per LinkedIn's reporting page as of 26 October 2026. If you set timeGranularity to ALL, dates outside the 6-month daily retention period round to month boundaries without warning. What DAILY returns past six months is not stated, so pull your last six months as DAILY, your older history as MONTHLY with a grain column, and your demographic pivots first.

Why do LinkedIn Ads API numbers not match Campaign Manager?

Usually one of four reasons. Demographic metrics are approximate, so your daily sums miss period totals; LinkedIn's help article puts them within three units of actual results in almost all cases. Demographics lag 12 to 24 hours and video watch time up to 48, so your early pull is stale. Your conversions keep arriving through the attribution window, up to 365 days (180 added in 202609). And if your loader stored a truncated 15,000-element response or lost a 429 slice, you have holes in your table.

Which Linkedin-Version header should a reporting integration use in November 2026?

Use the newest version you have tested, which on 26 October 2026 was 202610. LinkedIn sunset 202510 on 15 October 2026 and sunsets 202511 on 16 November 2026; 202611 is due. If your header is missing you get 400 VERSION_MISSING, and if it names a sunset version you get 426 NONEXISTENT_VERSION. If you report memberProfileClicks, new in 202610, note that the recent-changes page names Sponsored Updates and member follower display ads, while the schema page lists Sponsored Updates only.

Should LinkedIn Ads metrics be stored as JSONB or as columns in PostgreSQL?

Store your metrics as JSONB in a fact table keyed by pivot URN, date, grain and API version, and give your analysts typed columns through views. LinkedIn's metrics table runs past 90 fields and grows by version: 202601 added video watch time, 202605 appointmentsScheduled, 202610 memberFollows. With JSONB plus a view, you change one line per release instead of migrating your table. Load with INSERT ... ON CONFLICT DO UPDATE so your re-pulled window corrects rows rather than duplicating them.

Related

Work with Leapbuzz

Is your LinkedIn Ads warehouse quietly missing rows?

Leapbuzz builds marketing analytics and insights pipelines from Singapore for businesses across Singapore, Malaysia, Australia, the US and Canada. We audit the extraction, the reconciliation and the version calendar, then hand you a warehouse finance can trust.

Talk to us

Or send it now

Tell us the challenge this post left you with.

A senior practitioner reads it and replies within one business day with what we would check first. No deck, no templates.

No deck, no templates. We reply within one business day.