60 SaaS APIs graded on data engineering friendliness: can you actually get your own data out?
Interactive leaderboard: https://bruin-data.github.io/ingestion-index/
Every company's data lives in other companies' products. Getting it back out, into your own warehouse, is the first mile of every data platform. Some vendors make that easy. Most do not. This index grades them.
The headline: zero out of 60 APIs earned an A. 15 B, 19 C, 16 D, 10 F.
Each API was scored on 9 dimensions (0-5), weighted by the volume class of the data it serves, against two evidence sources: the vendor's official, current documentation (cited by URL in every scorecard) and the production connector code of ingestr, which has to survive these APIs in the real world. Methodology: RUBRIC.md.
| Rank | Source | Class | Score | Grade | Spec | Why |
|---|---|---|---|---|---|---|
| 1 | Chargebee | MEDIUM | 89 | B | yes | Chargebee is the control group for this whole index: updated_at[after] plus sort_by[asc]=updated_at plus include_deleted=true on every table that matters, which is why the battle-tested connector is 235 lines with no window sharding, no fallback modes and no synthetic keys, and why the only real scar is that per-site rate limits depend on the customer's plan so the client has to assume the cheapest one. |
| 2 | Recurly | MEDIUM | 89 | B | yes | Recurly is the control group: sort=updated_at&order=asc plus begin_time/end_time on 44 endpoints, cursor pagination, soft deletes that stay in the feed, 1,000 GETs a minute with writes free, and the ingestr connector comes out at 235 lines with no workarounds, which is what the other 29 sources in this index cost you for lacking. |
| 3 | Salesforce | MEDIUM | 84 | B | partial | Salesforce is the API that proves incrementality is not enough: SystemModstamp plus queryAll plus getDeleted plus Bulk is textbook replication design, and you still have to buy your way past a Professional-edition paywall, share a daily call quota with the marketing team, and know that query() quietly hides every Task older than a year. |
| 4 | Paddle | MEDIUM | 84 | B | yes | Paddle gives every record a trustworthy updated_at and then lets you filter on it in exactly one place, so the flagship transactions table syncs beautifully while subscriptions and customers get full-crawled forever. |
| 5 | Shopify | MEDIUM | 83 | B | graphql | Shopify is what a data-friendly API looks like when the vendor actually ships a bulk JSONL export and updated_at filters on everything, then charges you for it in migration tax: your extractor gets rewritten from REST to GraphQL, your product ids change from 123 to gid://shopify/Product/123, and your order history stops 60 days back until someone approves a scope. |
| Rank | Source | Class | Score | Grade | Spec | Why |
|---|---|---|---|---|---|---|
| 56 | App Store Connect | LARGE | 35.2 | F | yes | Apple will not keep your analytics data unless you ask first, deletes every report instance after 35 days, and rations the historical backfill to one snapshot request per calendar month, so the App Store Connect API is the only source in this index where forgetting to run your pipeline destroys data that no amount of re-reading can recover. |
| 57 | Google Analytics 4 | LARGE | 34.2 | F | yes | Google Analytics 4 has no ingestion API: the Data API hands you a recomputed pivot table with no ids, no change timestamps, no deletions and a daily token budget, and Google's actual answer for getting your events into a warehouse is a different product entirely. |
| 58 | CleverTap | LARGE | 34.2 | F | partial | CleverTap will hand you raw event rows, one event name and one whole day at a time, on a cursor that self-destructs in four hours, and every row arrives with no id, a local-time integer for a timestamp that the payload declines to name a timezone for, and a user profile snapshot from today rather than from when the event happened. |
| 59 | AppLovin | LARGE | 26.6 | F | none | AppLovin is the friendliest possible request wrapped around the least friendly possible dataset: one unauthenticated-feeling GET with an API key hands you a whole day of impression-level revenue, but there are no ids, no updated_at, no deletions, no versioning, no published rate limit, and only 45 days of history that is still being restated when it expires, so your warehouse becomes the only surviving copy of numbers you were never allowed to verify. |
| 60 | AppsFlyer | LARGE | 24.4 | F | none | AppsFlyer sells you every event in your app and then hands you a straw: no cursor, no pagination, no updated_at, a 1M row hard stop per CSV, a 90 day lookback, and about a dozen calls a day per app to spend on all of it, which is why the battle-tested connector skips the raw data API entirely and why the real answer is to stop calling the API and pay for a bucket. |
Spec = machine-readable API contract: yes (OpenAPI / Google Discovery, URL verified), graphql (introspection), partial (Postman collection, proprietary codegen files, XSD), none. Links per source in LINKS.md; raw data in scores.csv and data.json (full structured index: scores, dimensions, weights, one-liners, spec status).
| Rank | Source | Class | Score | Grade | Spec | One-liner |
|---|---|---|---|---|---|---|
| 1 | Chargebee | MEDIUM | 89 | B | yes | Chargebee is the control group for this whole index: updated_at[after] plus sort_by[asc]=updated_at plus include_deleted=true on every table that matters, which is why the battle-tested connector is 235 lines with no window sharding, no fallback modes and no synthetic keys, and why the only real scar is that per-site rate limits depend on the customer's plan so the client has to assume the cheapest one. |
| 2 | Recurly | MEDIUM | 89 | B | yes | Recurly is the control group: sort=updated_at&order=asc plus begin_time/end_time on 44 endpoints, cursor pagination, soft deletes that stay in the feed, 1,000 GETs a minute with writes free, and the ingestr connector comes out at 235 lines with no workarounds, which is what the other 29 sources in this index cost you for lacking. |
| 3 | Salesforce | MEDIUM | 84 | B | partial | Salesforce is the API that proves incrementality is not enough: SystemModstamp plus queryAll plus getDeleted plus Bulk is textbook replication design, and you still have to buy your way past a Professional-edition paywall, share a daily call quota with the marketing team, and know that query() quietly hides every Task older than a year. |
| 4 | Paddle | MEDIUM | 84 | B | yes | Paddle gives every record a trustworthy updated_at and then lets you filter on it in exactly one place, so the flagship transactions table syncs beautifully while subscriptions and customers get full-crawled forever. |
| 5 | Shopify | MEDIUM | 83 | B | graphql | Shopify is what a data-friendly API looks like when the vendor actually ships a bulk JSONL export and updated_at filters on everything, then charges you for it in migration tax: your extractor gets rewritten from REST to GraphQL, your product ids change from 123 to gid://shopify/Product/123, and your order history stops 60 days back until someone approves a scope. |
| 6 | Pipedrive | SMALL | 82.8 | B | yes | Pipedrive is the rare vendor that actually fixed its own API: v2 hands you updated_since, keyset cursors, 500-row pages and a token cost printed in the OpenAPI spec, then charges the whole account one shared daily allowance to use them, and still won't tell you which persons were deleted. |
| 7 | Linear | SMALL | 82.4 | B | graphql | Linear is the rare API where the data-engineering basics are all present and typed (updatedAt filters on every entity that matters, Relay cursors, includeArchived on every connection, a UUID on every node) and the wall you eventually hit is not a missing feature but a complexity meter that charges you per field: rate limits arrive as HTTP 400 with no Retry-After, so ingestr's 429-based retries never fire, and 56% of its issues-sync budget goes to a nested labels(first: 250) connection re-fetching the labelIds it already asked for. |
| 8 | Asana | SMALL | 80.8 | B | yes | Asana is the best-documented API in the project-management set, with a real 3 MB OpenAPI spec and a deprecation policy other vendors should copy, and it still makes you crawl a project tree by hand: modified_since exists on exactly one of 175 paths, users and teams carry no timestamps at all, comments cost one HTTP request per task, and the change feed that would fix all of it hands you a sync token that expires in 24 hours. |
| 9 | Zendesk | MEDIUM | 80 | B | yes | Zendesk actually built the changes feed everyone begs for, deleted records and an end_of_stream flag included, then capped it at 10 requests a minute and gave tickets two different update clocks so the one in the payload is not the one the filter uses. |
| 10 | GitHub | MEDIUM | 80 | B | yes | The best-documented API in the index still cannot tell you which pull requests changed: 18 of 636 GET endpoints accept a since, and the sanctioned fix for the flagship table is to list your PRs as issues and then re-fetch each one. |
| 11 | Stripe | LARGE | 79.4 | B | yes | Stripe built the best event log in SaaS because it had to: almost none of its objects have an updated_at, so incremental sync means replaying 30 days of events, re-fetching every changed object by ID, and praying you never fall 31 days behind. |
| 12 | Klaviyo | LARGE | 78.4 | B | yes | Klaviyo's /api/events is what a LARGE-volume API should look like, filterable and sortable on event time, cursor-paginated, immutable, with no retention limit at all, and then the same API makes you pin a channel filter to list campaigns, caps segments at ten rows a page, and tells you to buy the CDP if you actually want your 30 million events out. |
| 13 | Square | MEDIUM | 77 | B | yes | Square is the only API in this index whose deletes arrive inside the incremental window on purpose (is_deleted: true with updated_at set to the deletion time), and it is also the API that will not tell you how many requests per second you are allowed to make, so ingestr picked 10, hardcoded it, and left a comment about a QPS cap that has never been written down anywhere. |
| 14 | QuickBooks | MEDIUM | 77 | B | partial | QuickBooks ships the CDC feed everyone else is missing and then caps it at 1000 rows with no way to ask for page two, so the real pipeline is still a SELECT * ... ORDERBY MetaData.LastUpdatedTime offset crawl. |
| 15 | GitLab | MEDIUM | 76 | B | yes | GitLab is the only API in this index where updated_after and order_by=updated_at both exist on every table you actually want, and it then hands you page=1,2,3… to walk them: keyset pagination reached project issues in 18.3, audit events, and jobs, but never reached merge requests or pipelines, and its 1,284-path OpenAPI spec does not declare a single one of its 322 list endpoints as returning an array. |
| 16 | Trello | SMALL | 74.4 | C | yes | Trello hands you a perfect append-only change feed and then makes it useless for the flagship table: cards can be filtered by modified time only through the one endpoint that cannot be paginated, so ingestr re-downloads every card on every board on every run and does the incremental filter in Go. |
| 17 | Jira | SMALL | 72 | C | yes | Jira publishes a machine-readable OpenAPI spec, per-operation deprecation flags and a textbook deleted-ids feed for worklogs, then makes the table everyone actually syncs depend on a hand-formatted JQL sentence with minute precision, no timezone, and no row count to tell you when your pagination loop gave up. |
| 18 | ClickUp | SMALL | 70 | C | yes | ClickUp will happily filter tasks by date_updated for you, then make you crawl workspace to space to folder to list at 100 requests a minute to find out which lists to ask, hide archived and deleted work behind no endpoint at all, and default subtasks to off so the pipeline that looks like it works is quietly missing half the tasks. |
| 19 | monday.com | SMALL | 70 | C | graphql | monday.com publishes its entire GraphQL schema unauthenticated and versions the API quarterly, then makes you discover every board by offset paging before you can cursor-walk each one separately, so the best-documented API in the index is also the one where ingestr ships the flagship table as a full refresh. |
| 20 | Notion | SMALL | 69.6 | C | partial | Notion is the rare API that gets the hard parts right, filter and sort on last_edited_time, UUIDs everywhere, a visible trash, and then rounds that timestamp down to the nearest minute, caps every query at 10,000 rows while returning has_more: false, and makes page content reachable only by recursively walking block trees at three requests per second. |
| 21 | Smartsheet | SMALL | 69.6 | C | yes | Smartsheet quietly ships the incremental primitives most APIs lack (rowsModifiedSince, modifiedSince, ifVersionAfter, a per-sheet version counter) and then makes them pointless, because a deleted row is simply absent from the response and the audit log has no concept of a row, so the only correct sync is the full refresh you were trying to avoid. |
| 22 | Amplitude | LARGE | 69 | C | none | Amplitude got the one thing right that nobody else does, windowing exports by ingestion time so late-arriving mobile events can never be missed, and then wrapped it in a synchronous multi-gigabyte zip download with no pagination, no async job, no published rate limit, and a 4GB wall whose documented workaround is to stop using the API. |
| 23 | Airtable | SMALL | 68 | C | none | Airtable's only real change feed is hidden inside its webhooks API, so the batch path is left doing full refreshes and hand-escaping spreadsheet formulas into a URL to fake an updated_at that silently ignores every rollup, lookup and linked-record edit. |
| 24 | Freshdesk | MEDIUM | 67 | C | none | Freshdesk gives you updated_since on every table that matters and then caps the answer at 30,000 rows with no end-date filter and no bulk export, so the flagship ticket table is incremental-friendly right up to the moment it becomes un-backfillable. |
| 25 | Attio | SMALL | 66.4 | C | yes | Attio is the CRM that never writes down when anything changed: updated_at returns 400 unknown_filter_attribute_slug on every object, so ingestr gave up and marked all five tables replace, partitioned by created_at for lack of anything better, and the only way to learn a record was deleted is to have been running a webhook listener at the moment it happened. |
| 26 | Typeform | MEDIUM | 65 | C | partial | Typeform gets the hard part right, immutable rows behind a real cursor with unlimited history, then hands you 2 requests per second per account, no endpoint that lists responses across forms, and a documented 30-minute blind spot at the head of the very timestamp you have to page on. |
| 27 | HubSpot | MEDIUM | 64 | C | yes | HubSpot's list endpoint cannot filter by anything and its filterable endpoint cannot page past 10,000 results or see deletions, so every incremental sync is a keyset-paginated search that restarts every 10,000 rows plus a second full crawl of the recycle bin just to learn what got deleted. |
| 28 | Intercom | MEDIUM | 64 | C | yes | Intercom documents every one of its own landmines beautifully, then hands you a support tool whose conversations you can sync incrementally but whose messages you cannot, whose companies have no updated_at filter and a 10,000-row ceiling escapable only by a one-per-app scroll that cannot be resumed after it fails, and whose contacts can be filtered no more precisely than "some time that calendar day". |
| 29 | Personio | SMALL | 64 | C | yes | Personio publishes a clean OpenAPI spec and then uses it to document, in its own words, that limit and offset are ignored the moment you pass updated_since, so on the flagship employees endpoint you can have incrementality or pagination but not both. |
| 30 | Twilio | LARGE | 62.4 | C | yes | Twilio gives you the best primary keys and the cleanest cursor pagination in this whole index, then omits a single DateUpdated filter from every list endpoint, so the connector re-downloads your entire message history on every run and throws most of it away in Go. |
| 31 | Mailchimp | LARGE | 61.8 | C | yes | Mailchimp gives contacts a proper since_last_changed filter and then hands you email activity as pages of recipients with events buried in an unkeyed nested array, so the one table with millions of rows is the one where "give me what's new" still costs you a full re-crawl of everyone you ever mailed, once per campaign. |
| 32 | Trustpilot | SMALL | 61.2 | C | partial | Trustpilot puts updatedAt on every review and then makes sure no endpoint will ever let you filter or sort by it, so the only honest sync is a full re-walk you have to pay for out of a yearly call allowance. |
| 33 | G2 | MEDIUM | 61 | C | yes | G2 publishes a real OpenAPI spec and then breaks its own contract on the endpoints that matter: reviews let you filter by an updated_at the response never returns, and buyer intent, the one table big enough to need incrementality, is an OLAP query interface whose primary key is a hash of whichever columns you happened to ask for. |
| 34 | Gorgias | MEDIUM | 61 | C | partial | Gorgias will happily sort tickets by updated_datetime and then refuse to let you filter on it, which is how you end up re-scanning the whole helpdesk at two requests a second to find yesterday's changes, and the flagship messages table does not have an updated_datetime at all, so ingestr had to invent one out of whatever timestamp field happened to be largest. |
| 35 | Google Ads | LARGE | 58 | D | none | Google Ads publishes a fully typed schema for every one of its thousands of fields and then hands you exactly one way to sync them: re-download the last ninety days of every report, forever, keyed on a composite grain you have to invent yourself. |
| 36 | Snapchat Ads | LARGE | 58 | D | none | Snapchat ships the one thing every ad API should copy, a finalized_data_end_time on every stats row telling you when the number stops moving, and then forgets to ship a modified-since filter anywhere in the entire API. |
| 37 | LARGE | 57 | D | yes | Pinterest is the only ads API that will tell you when yesterday's numbers are final, and the only one where nothing anywhere will tell you what changed: metrics_ready_state is a gift, and then you find that not one list endpoint in the entire ads surface accepts a time filter, and the single updated_since feed that exists returns active entities only. |
|
| 38 | Wise | MEDIUM | 57 | D | partial | Wise will push you a webhook the instant a transfer changes state, and will not let you ask which transfers changed: the flagship list filters on creation date only, transfers carry no updated_at, and the statement endpoint that holds the actual ledger has no row IDs, no pagination, a 469-day window cap, an SCA private-key signing dance, and is simply switched off for European businesses on a self-serve token. |
| 39 | SurveyMonkey | MEDIUM | 56 | D | none | SurveyMonkey does the hard part right and then rations it: /responses/bulk filters and sorts by date_modified like a textbook incremental endpoint, but it hands you 100 rows at a time out of a 500-request daily allowance, so a million-response backfill is a twenty-day project and the fix is a bigger invoice. |
| 40 | Customer.io | LARGE | 55.2 | D | yes | Customer.io publishes a beautiful OpenAPI spec for an event feed that guarantees you only 30 days, cannot be filtered by time, and hands you deliveries whose opens and clicks keep changing long after the only timestamp you are allowed to filter on. |
| 41 | PostHog | LARGE | 54.8 | D | yes | PostHog is the rare API polished enough to publish a 6.8 MB public OpenAPI spec and then use it to tell you, in writing, that you are not allowed to build a connector on it: the events endpoint is deprecated with a 100k offset ceiling, /query explicitly rejects third-party connectors, persons has no modified-time filter at all, and the only sanctioned road to a warehouse is a metered push product that guarantees duplicates. |
| 42 | Facebook Ads | LARGE | 53.4 | D | partial | Meta gives you an async report API and then punishes you for using it at scale: the battle-tested configuration is one async job per account per day, polled for up to four hours, re-run across a rolling 28-day window because attribution keeps rewriting the past. |
| 43 | Reddit Ads | LARGE | 52.8 | D | yes | Reddit built the modern parts (published OpenAPI spec, per-endpoint rate limit policies with IETF headers, no allowlisting, a real changelog) and skipped the boring ones, so you get a beautifully documented API with no modified-time filter on a single endpoint, no async export, 24 months of history, and a paginated POST report whose next page nobody explains how to fetch. |
| 44 | RevenueCat | LARGE | 49.4 | D | yes | RevenueCat's API cannot list your subscriptions and cannot tell you when anything changed, so the only sane pipeline is the one that bypasses the API entirely and reads gzipped CSVs out of your own bucket. |
| 45 | Slack | MEDIUM | 49 | D | partial | Slack is the only API in this index where the same endpoint returns 999 messages a minute or 15 depending on whether Slack has approved your business model, and the one thing you cannot get at any tier is the knowledge that a message was edited or deleted. |
| 46 | TikTok Ads | LARGE | 48 | D | partial | TikTok keeps your ad data forever and then hands it back 20,000 ads at a time in 30-day slices, announcing the silent truncation in a response header while a five-minute penalty box drops your QPS to zero for daring to ask for too much at once. |
| 47 | Mixpanel | LARGE | 44.4 | D | yes | Mixpanel will stream you a whole day of events as gzipped JSONL and then spend the next two years quietly editing that day behind your back: backdated imports with no age limit, compaction dedup that deletes rows it already gave you, identity merges that rewrite distinct_id retroactively, and GDPR deletions so invisible that Mixpanel's own paid warehouse connector disclaims responsibility for syncing them. |
| 48 | SendGrid | LARGE | 40.8 | D | yes | SendGrid sells you a 30-day paid add-on for the privilege of pulling your own email events 1,000 rows at a time, six times a minute, with no pagination, so ingestr binary-searches the clock and then binary-searches the message IDs and still prints a warning that some events were lost. |
| 49 | Zoom | MEDIUM | 40 | D | yes | Zoom will happily tell you about one meeting at a time, six months back, one month per request, on the heavy quota: the participants table everyone actually wants is an N+1 fan-out with no updated_at, no deletes, and an empty-string primary key. |
| 50 | Adapty | MEDIUM | 40 | D | yes | Adapty publishes a clean OpenAPI 3.1 spec, a one-line API key and an honest documented rate limit, and then declines to give you a single row of the transaction data you bought it for: the API hands you pre-aggregated chart values with no id, no timestamp and no pagination, so the flagship pipeline is one POST per calendar day per chart at two requests per second, and the real event export is a CSV dropped in your own S3 bucket once a day. |
| 51 | Apple Search Ads | LARGE | 38.8 | F | none | Apple will not sell you daily ad performance older than 90 days at any price, will not tell you its rate limit, and will not tell you when a number changed, so the only honest Apple Search Ads pipeline is one that re-downloads a rolling 90-day window forever and treats its own warehouse as the system of record. |
| 52 | Braze | LARGE | 37.6 | F | partial | Braze will happily sell you the raw engagement events its REST API refuses to expose, but only as a fire-and-forget stream into your own bucket that can never be replayed, so the pipeline you build today has no yesterday. |
| 53 | LinkedIn Ads | LARGE | 36.4 | F | partial | LinkedIn's reporting endpoint tells you in one breath that it "currently does not support pagination" and that the "response is limited to 15,000 elements", with no error code for hitting the cap, so the flagship ads table is silently truncated by an API that also documents that its daily rows will never sum to its monthly totals. |
| 54 | Adjust | LARGE | 36.4 | F | none | Adjust's Report Service is the Datascape dashboard with a URL bolted on: no row id, no update timestamp, no pagination, no deletes, and cohort revenue that keeps rewriting itself for 120 days, so the only correct pipeline is "delete the last 30 days and ask again", and event-level data is not in the API at all. |
| 55 | Google Search Console | LARGE | 36.2 | F | yes | Google Search Console is a pleasant API that quietly destroys its own data: no row identity, no updated_at, no deletions, offset pagination into a documented 50,000-row-per-day cap that returns "top ones" rather than all rows, roughly half your clicks withheld as anonymized queries, and a hard 16-month amnesia that makes warehousing it mandatory and backfilling it impossible. |
| 56 | App Store Connect | LARGE | 35.2 | F | yes | Apple will not keep your analytics data unless you ask first, deletes every report instance after 35 days, and rations the historical backfill to one snapshot request per calendar month, so the App Store Connect API is the only source in this index where forgetting to run your pipeline destroys data that no amount of re-reading can recover. |
| 57 | Google Analytics 4 | LARGE | 34.2 | F | yes | Google Analytics 4 has no ingestion API: the Data API hands you a recomputed pivot table with no ids, no change timestamps, no deletions and a daily token budget, and Google's actual answer for getting your events into a warehouse is a different product entirely. |
| 58 | CleverTap | LARGE | 34.2 | F | partial | CleverTap will hand you raw event rows, one event name and one whole day at a time, on a cursor that self-destructs in four hours, and every row arrives with no id, a local-time integer for a timestamp that the payload declines to name a timezone for, and a user profile snapshot from today rather than from when the event happened. |
| 59 | AppLovin | LARGE | 26.6 | F | none | AppLovin is the friendliest possible request wrapped around the least friendly possible dataset: one unauthenticated-feeling GET with an API key hands you a whole day of impression-level revenue, but there are no ids, no updated_at, no deletions, no versioning, no published rate limit, and only 45 days of history that is still being restated when it expires, so your warehouse becomes the only surviving copy of numbers you were never allowed to verify. |
| 60 | AppsFlyer | LARGE | 24.4 | F | none | AppsFlyer sells you every event in your app and then hands you a straw: no cursor, no pagination, no updated_at, a 1M row hard stop per CSV, a 90 day lookback, and about a dozen calls a day per app to spend on all of it, which is why the battle-tested connector skips the raw data API entirely and why the real answer is to stop calling the API and pay for a bucket. |
- Category is destiny. Subscription billing (Chargebee 89, Recurly 89, Paddle 84) and CRM/commerce own the top. Ads and measurement own the bottom: all 15 ads/measurement APIs scored D or F, with a median incremental-extraction score of 1/5. APIs are good where customers reconcile the numbers against money, and bad where the vendor grades its own homework.
- The bottom 10 are all LARGE-volume sources. The APIs carrying the most data are the worst at letting you move it.
- Connector line count is a proxy for API hostility. Chargebee and Recurly each need ~235 lines of connector code. Stripe needs 2,253. SendGrid requires recursively bisecting time ranges down to single seconds. A 3-year Facebook Ads backfill for one account is ~1,100 polled async report jobs.
- Retention cliffs turn pipelines into insurance. Apple Search Ads and AppsFlyer raw data (90 days), AppLovin (45), App Store Connect (35), Stripe events / Customer.io activities / SendGrid activity (30), QuickBooks CDC (30), Google Search Console (16 months). Miss the window and the data is gone forever.
- "Pay for a bucket." For seven vendors (Braze, AppsFlyer, GA4, Adjust, RevenueCat, Mixpanel, PostHog) the real warehouse answer is a separate paid delivery product, not the API.
- Silent truncation is epidemic. LinkedIn caps reports at 15,000 elements with no error code. TikTok announces truncation only in a response header. Google Search Console returns "top" rows, not all rows. Notion returns has_more:false at its 10,000-row cap.
- Deletion detection is the forgotten criterion. 19 of 60 sources scored 0-1: if a record is deleted upstream, your warehouse never finds out without a full re-extract.
- "Immutable" event data usually is not. Mixpanel rewrites exported days via backdated imports, dedup compaction and identity merges. Meta restates 28 days of attribution. Adjust restates cohort revenue for 120 days. Snapchat and Pinterest at least tell you when a number is final (finalized_data_end_time, metrics_ready_state); nobody else does.
- Sort-but-not-filter. A recurring anti-pattern: the object carries an updated-at timestamp, and no endpoint lets you filter on it (Gorgias, Attio, Trello, Snapchat, LinkedIn structure objects, Klaviyo catalogs).
- The good parts all exist, just never together. Stripe's event log, Zendesk's incremental export, Klaviyo's filterable immutable events, Amplitude's ingestion-time windowing, Salesforce's SystemModstamp + getDeleted, Shopify's bulk JSONL export, QuickBooks' CDC endpoint, Square's deletes-inside-the-window, Snapchat's finality timestamp. An API that shipped all nine would score the first A.
If you build APIs and want to top the next edition, the spec is simple:
- Every object has a stable unique id and a server-side
updated_atthat changes on every mutation, including child/nested changes. - Every list endpoint can filter AND sort on
updated_at. - Deletions are visible: soft-delete flags in the feed, or a changes/events endpoint that includes deletes, with retention measured in years, not days.
- Cursor pagination, stable under concurrent writes, no silent result caps. If you must cap, return an explicit error.
- A bulk export path for backfills, available on every plan that has API access, with full history.
- Documented rate limits with Retry-After, no daily quotas that make backfills impossible.
- A published OpenAPI spec (or GraphQL introspection) that matches reality.
- If your data restates (attribution windows, late events), say when a number becomes final, in the payload.
- 9 dimensions: incremental extraction, change timestamps, deletion detection, primary keys, pagination, bulk export & backfill, rate limits & quotas, schema/docs/versioning, auth & access friction.
- Volume-aware weighting: each source is classed SMALL / MEDIUM / LARGE by the highest-volume table users actually sync. Missing incrementality on a 10k-row config object is a tax; on billions of ad rows it is disqualifying, and the weights reflect that.
- Evidence over vibes: every dimension score cites current official docs by URL. Where docs and reality disagree, we trust the production connector code in ingestr that has to work around reality.
- Scores reflect the API as of the last-verified date at the bottom of each scorecard.
Full rubric with scoring anchors and weight tables: RUBRIC.md.
If you work at one of these vendors and believe a score is wrong or outdated: open an issue with links to the docs. We re-verify and re-grade with receipts, and improvements make the changelog. That is the point of this index. See CONTRIBUTING.md.
Built by Bruin, maintainers of ingestr, an open-source CLI that copies data from any of these sources into any warehouse. The index exists because we maintain production connectors for all 60 of these APIs and got tired of writing the same workarounds.
Content licensed under CC BY 4.0.