{
 "name": "The Ingestion Index",
 "tagline": "60 SaaS APIs graded on data engineering friendliness: can you actually get your own data out?",
 "edition": "2026-08",
 "last_verified": "2026-08-10",
 "homepage": "https://github.com/bruin-data/ingestion-index",
 "license": "CC BY 4.0",
 "grade_counts": {
  "B": 15,
  "C": 19,
  "D": 16,
  "F": 10
 },
 "dimension_names": [
  "incremental",
  "timestamps",
  "deletion",
  "pk",
  "pagination",
  "bulk_backfill",
  "rate_limits",
  "schema_docs",
  "auth"
 ],
 "weights_by_volume_class": {
  "SMALL": {
   "incremental": 10,
   "timestamps": 8,
   "deletion": 10,
   "pk": 14,
   "pagination": 12,
   "bulk_backfill": 8,
   "rate_limits": 12,
   "schema_docs": 16,
   "auth": 10
  },
  "MEDIUM": {
   "incremental": 20,
   "timestamps": 10,
   "deletion": 10,
   "pk": 10,
   "pagination": 10,
   "bulk_backfill": 15,
   "rate_limits": 10,
   "schema_docs": 10,
   "auth": 5
  },
  "LARGE": {
   "incremental": 25,
   "timestamps": 10,
   "deletion": 10,
   "pk": 10,
   "pagination": 10,
   "bulk_backfill": 18,
   "rate_limits": 10,
   "schema_docs": 5,
   "auth": 2
  }
 },
 "scoring": "Each dimension scored 0-5. total = sum(dimension_score * weight) / 5 using the weight column of the source volume class. Grades: A 90+, B 75-89, C 60-74, D 40-59, F <40.",
 "sources": [
  {
   "rank": 1,
   "id": "chargebee",
   "name": "Chargebee",
   "volume_class": "MEDIUM",
   "volume_class_reason": "invoices and transactions grow one row per billing cycle per subscription, so a mid-size SaaS site lands in the hundreds of thousands to low millions; the rubric names Chargebee invoices as MEDIUM",
   "score": 89.0,
   "grade": "B",
   "dimensions": {
    "incremental": 5,
    "timestamps": 4,
    "deletion": 5,
    "pk": 5,
    "pagination": 4,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://raw.githubusercontent.com/chargebee/openapi/main/spec/chargebee_api_v2_pc_v2_spec.json). Verified 2026-08-10: HTTP 200, 14,552,466 bytes, `openapi: 3.1.0`, `info.version: 2026-07-30.26c7bd26c97b795...",
   "docs_url": "https://apidocs.chargebee.com/docs/api",
   "one_liner": "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.",
   "scorecard": "results/chargebee.md"
  },
  {
   "rank": 2,
   "id": "recurly",
   "name": "Recurly",
   "volume_class": "MEDIUM",
   "volume_class_reason": "invoices, transactions and line items grow with billing activity: hundreds of thousands to low millions of rows for a real subscription business, while accounts and subscriptions stay bounded by customer count. Recurly is named as a MEDIUM example in the rubric itself.",
   "score": 89.0,
   "grade": "B",
   "dimensions": {
    "incremental": 5,
    "timestamps": 4,
    "deletion": 4,
    "pk": 5,
    "pagination": 4,
    "bulk_backfill": 4,
    "rate_limits": 5,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://recurly.com/developers/api/spec/v2021-02-25.yaml). Verified HTTP 200, `openapi: 3.0.0`, 1,130,505 bytes, 28,947 lines, 133 paths, 235 component schemas. Per-version specs also resolve (`v2019-10-10...",
   "docs_url": "https://recurly.com/developers/api/v2021-02-25/",
   "one_liner": "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.",
   "scorecard": "results/recurly.md"
  },
  {
   "rank": 3,
   "id": "salesforce",
   "name": "Salesforce",
   "volume_class": "MEDIUM",
   "volume_class_reason": "The rubric's own example puts CRM object stores \"bounded by the sales process\" in SMALL, and Account / Contact / Opportunity genuinely are bounded. But the table that actually sets the ceiling on a Salesforce sync is `Task` (plus `Event`, `EmailMessage`, and the `*History` field-history tables): one row per logged call, email, meeting and field change, growing with business activity into the hundreds of thousands to low millions for a typical customer. Salesforce itself auto-archives Tasks and Events at 365 days precisely because those tables grow unbounded, which no config-like object ever needs. Full refresh of Task is painful but survivable, so MEDIUM, not SMALL and not LARGE.",
   "score": 84.0,
   "grade": "B",
   "dimensions": {
    "incremental": 5,
    "timestamps": 4,
    "deletion": 4,
    "pk": 5,
    "pagination": 5,
    "bulk_backfill": 4,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 2
   },
   "spec": "partial",
   "spec_detail": "partial.** No static published machine-readable spec for the core data API. What exists is an opt-in, per-org OpenAPI 3.0 generator for the sObjects REST API, still beta: https://developer.salesforce.com/docs/atlas.en...",
   "docs_url": "https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_what_is_rest_api.htm",
   "one_liner": "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.",
   "scorecard": "results/salesforce.md"
  },
  {
   "rank": 4,
   "id": "paddle",
   "name": "Paddle",
   "volume_class": "MEDIUM",
   "volume_class_reason": "the largest table is `transactions`, one row per invoice/payment cycle for a SaaS merchant, the same grain as Chargebee/Recurly invoices; hundreds of thousands to low millions of rows, not row-per-event",
   "score": 84.0,
   "grade": "B",
   "dimensions": {
    "incremental": 3,
    "timestamps": 5,
    "deletion": 5,
    "pk": 5,
    "pagination": 4,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 5,
    "auth": 4
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://github.com/PaddleHQ/paddle-openapi/blob/main/v1/openapi.yaml). Verified: raw file returns HTTP 200, 7.4 MB, `openapi: 3.1.0`, `info.title: \"Paddle API\"`, 70 paths. Landing page https://developer.pa...",
   "docs_url": "https://developer.paddle.com/api-reference/overview",
   "one_liner": "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.",
   "scorecard": "results/paddle.md"
  },
  {
   "rank": 5,
   "id": "shopify",
   "name": "Shopify",
   "volume_class": "MEDIUM",
   "volume_class_reason": "orders grow with sales volume into the hundreds of thousands to low millions for most merchants",
   "score": 83.0,
   "grade": "B",
   "dimensions": {
    "incremental": 5,
    "timestamps": 4,
    "deletion": 3,
    "pk": 4,
    "pagination": 4,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "graphql",
   "spec_detail": "GraphQL introspection (https://shopify.dev/admin-graphql-direct-proxy/2026-07, verified HTTP 200 against a live `__schema` introspection query, no auth required; the legacy REST Admin API has no equivalent official Op...",
   "docs_url": "https://shopify.dev/docs/api/admin-graphql",
   "one_liner": "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.",
   "scorecard": "results/shopify.md"
  },
  {
   "rank": 6,
   "id": "pipedrive",
   "name": "Pipedrive",
   "volume_class": "SMALL",
   "volume_class_reason": "per the rubric's own example list: a CRM object store bounded by the sales process. The largest table a typical customer produces is activities or deals, tens of thousands of rows, so a full refresh is always affordable and missing incrementality would only be a tax, not a blocker",
   "score": 82.8,
   "grade": "B",
   "dimensions": {
    "incremental": 4,
    "timestamps": 4,
    "deletion": 3,
    "pk": 5,
    "pagination": 5,
    "bulk_backfill": 4,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "OpenAPI (https://developers.pipedrive.com/docs/api/v1/openapi-v2.yaml)**. Verified by fetch: HTTP 200, `text/yaml`, 1,015,045 bytes, header `openapi: 3.0.1` / `title: Pipedrive API v2` / `version: 2.0.0` / `servers: h...",
   "docs_url": "https://developers.pipedrive.com/docs/api/v1",
   "one_liner": "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.",
   "scorecard": "results/pipedrive.md"
  },
  {
   "rank": 7,
   "id": "linear",
   "name": "Linear",
   "volume_class": "SMALL",
   "volume_class_reason": "the rubric names project management explicitly; the flagship table is `issues`, headcount-bounded at a few thousand to a few tens of thousands of rows for a typical workspace, and the API will hand back roughly 65,000 issues per hour at full field fidelity within the complexity budget, so a full refresh of the entire workspace is an hours-at-worst operation and missing incrementality would be a tax rather than a blocker",
   "score": 82.4,
   "grade": "B",
   "dimensions": {
    "incremental": 4,
    "timestamps": 4,
    "deletion": 4,
    "pk": 5,
    "pagination": 4,
    "bulk_backfill": 4,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "graphql",
   "spec_detail": "GraphQL introspection** (verified). `POST https://api.linear.app/graphql` with `{\"query\":\"{__schema{queryType{name}}}\"}` and no credentials at all returns HTTP 200 and `{\"data\":{\"__schema\":{\"queryType\":{\"name\":\"Query\"...",
   "docs_url": "https://linear.app/developers/graphql",
   "one_liner": "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.",
   "scorecard": "results/linear.md"
  },
  {
   "rank": 8,
   "id": "asana",
   "name": "Asana",
   "volume_class": "SMALL",
   "volume_class_reason": "named explicitly in the rubric; the largest table for a typical workspace is `tasks`, bounded by how many things humans type into a work tracker, with `stories` a few multiples larger. Tens of thousands of rows, so a full refresh is a tax rather than an impossibility.",
   "score": 80.8,
   "grade": "B",
   "dimensions": {
    "incremental": 3,
    "timestamps": 3,
    "deletion": 3,
    "pk": 5,
    "pagination": 4,
    "bulk_backfill": 3,
    "rate_limits": 4,
    "schema_docs": 5,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "OpenAPI (https://raw.githubusercontent.com/Asana/openapi/master/defs/asana_oas.yaml)**",
   "docs_url": "https://developers.asana.com/reference/rest-api-reference",
   "one_liner": "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.",
   "scorecard": "results/asana.md"
  },
  {
   "rank": 9,
   "id": "zendesk",
   "name": "Zendesk",
   "volume_class": "MEDIUM",
   "volume_class_reason": "tickets grow with support volume; large orgs reach low millions, and the archive behavior makes full refresh incomplete anyway",
   "score": 80.0,
   "grade": "B",
   "dimensions": {
    "incremental": 5,
    "timestamps": 3,
    "deletion": 4,
    "pk": 5,
    "pagination": 4,
    "bulk_backfill": 3,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "OpenAPI (https://developer.zendesk.com/zendesk/oas.yaml, verified HTTP 200 on 2026-08-10, `content-length: 1,701,930`, still resolves as stated in the header above)",
   "docs_url": "https://developer.zendesk.com/api-reference/introduction/introduction/",
   "one_liner": "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.",
   "scorecard": "results/zendesk.md"
  },
  {
   "rank": 10,
   "id": "github",
   "name": "GitHub",
   "volume_class": "MEDIUM",
   "volume_class_reason": "the flagship tables are commits, issues + issue comments, pull requests + review comments; for a multi-repo engineering org they land in the hundreds of thousands to low millions of rows, so full refresh hurts but survives. Only the events feed is row-per-event, and GitHub caps it at 300 rows.",
   "score": 80.0,
   "grade": "B",
   "dimensions": {
    "incremental": 4,
    "timestamps": 4,
    "deletion": 2,
    "pk": 5,
    "pagination": 4,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 5,
    "auth": 4
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json, verified 200, 12,920,264 bytes, 808 paths) plus GraphQL introspection (SDL at https://doc...",
   "docs_url": "https://docs.github.com/en/rest/about-the-rest-api/about-the-rest-api",
   "one_liner": "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.",
   "scorecard": "results/github.md"
  },
  {
   "rank": 11,
   "id": "stripe",
   "name": "Stripe",
   "volume_class": "LARGE",
   "volume_class_reason": "payment transactions and balance transactions are row-per-event data; busy accounts produce millions of rows",
   "score": 79.4,
   "grade": "B",
   "dimensions": {
    "incremental": 4,
    "timestamps": 1,
    "deletion": 4,
    "pk": 5,
    "pagination": 5,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 5,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "OpenAPI (https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json, verified HTTP 200, official repo github.com/stripe/openapi)",
   "docs_url": "https://docs.stripe.com/api",
   "one_liner": "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.",
   "scorecard": "results/stripe.md"
  },
  {
   "rank": 12,
   "id": "klaviyo",
   "name": "Klaviyo",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship table is `events`: one row per email/SMS send, open, click, order and custom action per profile, so a mid-size ecommerce account produces tens of millions of rows; the rubric names Klaviyo email sends/opens as LARGE",
   "score": 78.4,
   "grade": "B",
   "dimensions": {
    "incremental": 4,
    "timestamps": 4,
    "deletion": 1,
    "pk": 5,
    "pagination": 4,
    "bulk_backfill": 4,
    "rate_limits": 5,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://raw.githubusercontent.com/klaviyo/openapi/main/openapi/stable.json). Verified: HTTP 200, 3,062,040 bytes, `openapi: 3.0.2`, `info.version: 2026-07-15`, 238 paths. Human-browsable at https://github....",
   "docs_url": "https://developers.klaviyo.com/en/reference/api_overview",
   "one_liner": "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.",
   "scorecard": "results/klaviyo.md"
  },
  {
   "rank": 13,
   "id": "square",
   "name": "Square",
   "volume_class": "MEDIUM",
   "volume_class_reason": "the flagship tables are `payments` and `orders`, one row per in-person transaction for a POS seller, which lands in the hundreds of thousands to low millions across a multi-year history; the physical-world throughput ceiling of a store keeps it out of the Stripe-style LARGE bracket, but full refresh is painful and incrementality matters a lot",
   "score": 77.0,
   "grade": "B",
   "dimensions": {
    "incremental": 4,
    "timestamps": 4,
    "deletion": 4,
    "pk": 4,
    "pagination": 4,
    "bulk_backfill": 4,
    "rate_limits": 2,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://github.com/square/connect-api-specification, spec at https://raw.githubusercontent.com/square/connect-api-specification/master/api.json). Verified: HTTP 200, 3,269,568 bytes, `\"openapi\": \"3.0.0\"`, ...",
   "docs_url": "https://developer.squareup.com/reference/square",
   "one_liner": "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.",
   "scorecard": "results/square.md"
  },
  {
   "rank": 14,
   "id": "quickbooks",
   "name": "QuickBooks",
   "volume_class": "MEDIUM",
   "volume_class_reason": "transaction entities such as Invoice, Payment and JournalEntry grow with business activity into the hundreds of thousands to low millions of rows for an active SMB, while the list entities behind them, Customer, Vendor and Account, stay small; full refresh is painful but survivable, so incrementality matters a lot without being existential",
   "score": 77.0,
   "grade": "B",
   "dimensions": {
    "incremental": 5,
    "timestamps": 4,
    "deletion": 4,
    "pk": 3,
    "pagination": 3,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 3,
    "auth": 3
   },
   "spec": "partial",
   "spec_detail": "partial (XSD zips + official Postman collection; no OpenAPI for the Accounting REST API)",
   "docs_url": "https://developer.intuit.com/app/developer/qbo/docs/learn/explore-the-quickbooks-online-api",
   "one_liner": "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.",
   "scorecard": "results/quickbooks.md"
  },
  {
   "rank": 15,
   "id": "gitlab",
   "name": "GitLab",
   "volume_class": "MEDIUM",
   "volume_class_reason": "the flagship tables are pipelines, merge requests and issues. Pipelines are row-per-CI-run and land in the hundreds of thousands to low millions for an active multi-project org over a few years; issues and MRs are an order of magnitude smaller. Full refresh hurts but survives, and the one genuinely row-per-event table, `/events`, is a 3-year-capped activity feed nobody syncs as a fact table.",
   "score": 76.0,
   "grade": "B",
   "dimensions": {
    "incremental": 4,
    "timestamps": 4,
    "deletion": 2,
    "pk": 5,
    "pagination": 3,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 4,
    "auth": 4
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://gitlab.com/gitlab-org/gitlab/-/raw/master/doc/api/openapi/openapi_v3.yaml, verified 200, 3,490,178 bytes, `openapi: 3.0.0`, `info.version: 19.3.0-pre`, 1,284 paths, 744 GET operations, 865 componen...",
   "docs_url": "https://docs.gitlab.com/api/rest/",
   "one_liner": "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.",
   "scorecard": "results/gitlab.md"
  },
  {
   "rank": 16,
   "id": "trello",
   "name": "Trello",
   "volume_class": "SMALL",
   "volume_class_reason": "the rubric names Trello explicitly. The flagship table is `cards`, bounded by how many cards humans drag around a board: tens of thousands of rows for a typical workspace. `actions` is the one table that can push toward MEDIUM on a large multi-year workspace, but it is also the one table with a real server-side incremental filter, so it is not the constraint. Full refresh is a tax, not an impossibility.",
   "score": 74.4,
   "grade": "C",
   "dimensions": {
    "incremental": 3,
    "timestamps": 2,
    "deletion": 4,
    "pk": 5,
    "pagination": 4,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 3,
    "auth": 4
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://developer.atlassian.com/cloud/trello/swagger.v3.json), verified HTTP 200, `\"openapi\":\"3.0.0\"`, 261,671 bytes, 191 paths. Coverage is partial: 125/261 operations carry a `200` response schema and th...",
   "docs_url": "https://developer.atlassian.com/cloud/trello/rest/",
   "one_liner": "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.",
   "scorecard": "results/trello.md"
  },
  {
   "rank": 17,
   "id": "jira",
   "name": "Jira",
   "volume_class": "SMALL",
   "volume_class_reason": "per the rubric's own example list: project management. The largest table for a typical customer is `issues` at thousands to tens of thousands of rows, with `issue_changelogs` fanning out maybe 10-20x on top. Full refresh stays affordable, so missing incrementality is a tax rather than a blocker. Note the ceiling table for scoring purposes is `issues`, which is what every Jira pipeline is actually built for.",
   "score": 72.0,
   "grade": "C",
   "dimensions": {
    "incremental": 4,
    "timestamps": 3,
    "deletion": 2,
    "pk": 4,
    "pagination": 3,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 4,
    "auth": 4
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json) — verified, HTTP 200, OpenAPI 3.0.1, 2,449,760 bytes, 421 paths, per-operation `deprecated` flags. A Swagger 2.0 variant also resolve...",
   "docs_url": "https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/",
   "one_liner": "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.",
   "scorecard": "results/jira.md"
  },
  {
   "rank": 18,
   "id": "clickup",
   "name": "ClickUp",
   "volume_class": "SMALL",
   "volume_class_reason": "the rubric's own example list puts project management here. The flagship table is `tasks`, headcount-bounded: a typical workspace has thousands to low tens of thousands of tasks. Time entries are the second-largest and are bounded by seats x days. Full refresh is always affordable, so missing incrementality is a tax rather than a blocker",
   "score": 70.0,
   "grade": "C",
   "dimensions": {
    "incremental": 4,
    "timestamps": 3,
    "deletion": 1,
    "pk": 5,
    "pagination": 3,
    "bulk_backfill": 3,
    "rate_limits": 4,
    "schema_docs": 3,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "OpenAPI (https://developer.clickup.com/openapi/clickup-api-v2-reference.json)**. Verified by fetch: HTTP 200, `content-type: application/json; charset=utf-8`, 517,903 bytes, `\"openapi\":\"3.1.0\"`, `info.title: \"ClickUp ...",
   "docs_url": "https://developer.clickup.com/docs/Getting%20Started",
   "one_liner": "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.",
   "scorecard": "results/clickup.md"
  },
  {
   "rank": 19,
   "id": "monday",
   "name": "monday.com",
   "volume_class": "SMALL",
   "volume_class_reason": "per the rubric's own example list: project management, same bucket as Asana / Linear / Jira / Trello. The flagship table is `items`, board rows, and monday itself caps items per board (`Board.items_limit`), so a typical account produces tens of thousands of item rows. ingestr confirms the read: it ships `items` as `replace`, a full refresh of every board on every run, and that is survivable. The one table that would be LARGE, `activity_logs`, is board-scoped, capped at 90 days, and not implemented by ingestr at all.",
   "score": 70.0,
   "grade": "C",
   "dimensions": {
    "incremental": 3,
    "timestamps": 3,
    "deletion": 3,
    "pk": 4,
    "pagination": 3,
    "bulk_backfill": 3,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "graphql",
   "spec_detail": "** GraphQL introspection, and better than that, a **public unauthenticated schema download**. `https://api.monday.com/v2/get_schema?format=sdl` (HTTP 200, 544,522 bytes SDL) and `https://api.monday.com/v2/get_schema` ...",
   "docs_url": "https://developer.monday.com/api-reference/",
   "one_liner": "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.",
   "scorecard": "results/monday.md"
  },
  {
   "rank": 20,
   "id": "notion",
   "name": "Notion",
   "volume_class": "SMALL",
   "volume_class_reason": "The rubric places Notion here and the API agrees: the flagship extraction, a data source query, is itself hard-capped at \"up to 10,000 results per query\" (https://developers.notion.com/reference/query-a-data-source), so no single table can be pulled past five figures without partitioning, and a full re-extract of a 10,000-row data source is 100 requests, about 40 seconds at Notion's 3 rps. Full refresh is always viable, which is exactly what ingestr does.",
   "score": 69.6,
   "grade": "C",
   "dimensions": {
    "incremental": 3,
    "timestamps": 3,
    "deletion": 4,
    "pk": 5,
    "pagination": 3,
    "bulk_backfill": 3,
    "rate_limits": 3,
    "schema_docs": 3,
    "auth": 4
   },
   "spec": "partial",
   "spec_detail": "** partial. `https://raw.githubusercontent.com/makenotion/notion-mcp-server/main/scripts/notion-openapi.json` (HTTP 200) is vendor-hosted OpenAPI 3.1.0 covering 17 paths for `2025-09-03`, but it is a byproduct of Noti...",
   "docs_url": "https://developers.notion.com/reference/intro",
   "one_liner": "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.",
   "scorecard": "results/notion.md"
  },
  {
   "rank": 21,
   "id": "smartsheet",
   "name": "Smartsheet",
   "volume_class": "SMALL",
   "volume_class_reason": "a sheet is hard-capped by the product at 500,000 cells, so the largest possible flagship table is ~20,000 rows at 25 columns, and the whole thing comes back in one HTTP call. Full refresh is not just viable, it is the default and it finishes in one request.",
   "score": 69.6,
   "grade": "C",
   "dimensions": {
    "incremental": 4,
    "timestamps": 4,
    "deletion": 1,
    "pk": 4,
    "pagination": 3,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 4,
    "auth": 3
   },
   "spec": "yes",
   "spec_detail": "OpenAPI 3.0.3 (https://developers.smartsheet.com/_spec/api/smartsheet/openapi.yaml, unadvertised URL)",
   "docs_url": "https://developers.smartsheet.com/api/smartsheet",
   "one_liner": "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.",
   "scorecard": "results/smartsheet.md"
  },
  {
   "rank": 22,
   "id": "amplitude",
   "name": "Amplitude",
   "volume_class": "LARGE",
   "volume_class_reason": "the `events` table is row-per-event product analytics data; a single hour of one project can legitimately exceed 4GB, which is why the Export API has a 4GB cap",
   "score": 69.0,
   "grade": "C",
   "dimensions": {
    "incremental": 4,
    "timestamps": 4,
    "deletion": 3,
    "pk": 4,
    "pagination": 2,
    "bulk_backfill": 4,
    "rate_limits": 2,
    "schema_docs": 3,
    "auth": 4
   },
   "spec": "none",
   "spec_detail": "none.** No OpenAPI, Swagger, or GraphQL schema is published. Verified by probe:",
   "docs_url": "https://amplitude.com/docs/apis",
   "one_liner": "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.",
   "scorecard": "results/amplitude.md"
  },
  {
   "rank": 23,
   "id": "airtable",
   "name": "Airtable",
   "volume_class": "SMALL",
   "volume_class_reason": "Airtable caps records *per base* by plan, not by usage: \"1000 records per base\" (Free), \"50000 records per base\" (Team), \"125000 records per base\" (Business) (https://support.airtable.com/docs/airtable-plans). The flagship table therefore cannot exceed a low six figures on any self-serve plan, and at 100 records per request against 5 rps a full 50,000-row base re-extracts in about 100 seconds, so full refresh is always viable and missing incrementality is a tax rather than a blocker.",
   "score": 68.0,
   "grade": "C",
   "dimensions": {
    "incremental": 2,
    "timestamps": 2,
    "deletion": 3,
    "pk": 4,
    "pagination": 3,
    "bulk_backfill": 4,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "none",
   "spec_detail": "** none. Verified: `https://airtable.com/developers/web/api/openapi.json` HTTP 404, `https://api.airtable.com/openapi.json` HTTP 404, `airtable.com` absent from the apis.guru index. Third-party \"airtable openapi\" proj...",
   "docs_url": "https://airtable.com/developers/web/api/introduction",
   "one_liner": "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.",
   "scorecard": "results/airtable.md"
  },
  {
   "rank": 24,
   "id": "freshdesk",
   "name": "Freshdesk",
   "volume_class": "MEDIUM",
   "volume_class_reason": "tickets grow with support volume into the hundreds of thousands to low millions for a mid-size helpdesk; the rubric lists \"tickets (Zendesk)\" as the canonical MEDIUM case and Freshdesk is the same shape. Contacts and companies are bounded by the customer base.",
   "score": 67.0,
   "grade": "C",
   "dimensions": {
    "incremental": 4,
    "timestamps": 4,
    "deletion": 3,
    "pk": 5,
    "pagination": 2,
    "bulk_backfill": 2,
    "rate_limits": 3,
    "schema_docs": 3,
    "auth": 5
   },
   "spec": "none",
   "spec_detail": "** none. No official OpenAPI or GraphQL surface. Verified 404 on `https://developers.freshdesk.com/api/openapi.json`, `https://developers.freshdesk.com/openapi.json` and `https://developers.freshdesk.com/api/swagger.j...",
   "docs_url": "https://developers.freshdesk.com/api/",
   "one_liner": "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.",
   "scorecard": "results/freshdesk.md"
  },
  {
   "rank": 25,
   "id": "attio",
   "name": "Attio",
   "volume_class": "SMALL",
   "volume_class_reason": "bounded by the sales process; on the live Bruin workspace `companies` sits between 20,000 and 40,000 rows and `people` between 10,000 and 20,000, measured by probing `offset` on `POST /v2/objects/{object}/records/query`, and the whole workspace re-extracts in minutes because page size is effectively uncapped, so missing incrementality is a tax rather than a blocker",
   "score": 66.4,
   "grade": "C",
   "dimensions": {
    "incremental": 2,
    "timestamps": 2,
    "deletion": 1,
    "pk": 4,
    "pagination": 3,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "OpenAPI (https://api.attio.com/openapi/api)**",
   "docs_url": "https://docs.attio.com",
   "one_liner": "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.",
   "scorecard": "results/attio.md"
  },
  {
   "rank": 26,
   "id": "typeform",
   "name": "Typeform",
   "volume_class": "MEDIUM",
   "volume_class_reason": "The flagship table is `responses`, one row per form submission, unbounded over time and growing with business activity. Response volume is rate-capped by plan (monthly response allowances), so it does not reach LARGE event-stream scale, but a lead-capture or NPS deployment lands in the hundreds of thousands to low millions of rows, and full refresh is genuinely painful here because the API caps you at 2 requests/second per account and forces per-form fan-out. Same shape as the rubric's canonical MEDIUM cases (tickets, conversations).",
   "score": 65.0,
   "grade": "C",
   "dimensions": {
    "incremental": 4,
    "timestamps": 2,
    "deletion": 0,
    "pk": 4,
    "pagination": 4,
    "bulk_backfill": 4,
    "rate_limits": 3,
    "schema_docs": 3,
    "auth": 5
   },
   "spec": "partial",
   "spec_detail": "** partial. No OpenAPI document and no GraphQL surface. Verified 404 on `https://api.typeform.com/openapi.json`, `https://api.typeform.com/swagger.json` and `https://www.typeform.com/developers/openapi.json`; `apis.gu...",
   "docs_url": "https://www.typeform.com/developers/get-started/",
   "one_liner": "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.",
   "scorecard": "results/typeform.md"
  },
  {
   "rank": 27,
   "id": "hubspot",
   "name": "HubSpot",
   "volume_class": "MEDIUM",
   "volume_class_reason": "contacts and engagements grow with business activity into the hundreds of thousands; email events would be LARGE but sit in a separate API",
   "score": 64.0,
   "grade": "C",
   "dimensions": {
    "incremental": 2,
    "timestamps": 3,
    "deletion": 4,
    "pk": 4,
    "pagination": 2,
    "bulk_backfill": 4,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 4
   },
   "spec": "yes",
   "spec_detail": "OpenAPI (catalog https://api.hubspot.com/public/api/spec/v1/specs, verified HTTP 200; per-API specs such as https://api.hubspot.com/public/api/spec/v2/specs/release/75223/version/2025-09 return real OpenAPI 3.0.1 JSON...",
   "docs_url": "https://developers.hubspot.com/docs/api-reference/latest/overview",
   "one_liner": "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.",
   "scorecard": "results/hubspot.md"
  },
  {
   "rank": 28,
   "id": "intercom",
   "name": "Intercom",
   "volume_class": "MEDIUM",
   "volume_class_reason": "conversations grow with support volume: hundreds of thousands to low millions for an established workspace, and conversation_parts inside them is a multiple of that; full refresh is survivable but expensive",
   "score": 64.0,
   "grade": "C",
   "dimensions": {
    "incremental": 3,
    "timestamps": 3,
    "deletion": 1,
    "pk": 4,
    "pagination": 3,
    "bulk_backfill": 3,
    "rate_limits": 4,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://github.com/intercom/Intercom-OpenAPI). OpenAPI 3.0.1, per-version YAML under `descriptions/`, versions 2.7 to 2.16 plus unstable. Verified resolvable: `https://raw.githubusercontent.com/intercom/In...",
   "docs_url": "https://developers.intercom.com/docs/references/rest-api/api.intercom.io/",
   "one_liner": "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\".",
   "scorecard": "results/intercom.md"
  },
  {
   "rank": 29,
   "id": "personio",
   "name": "Personio",
   "volume_class": "SMALL",
   "volume_class_reason": "per the rubric's own example list, HR. The three tables this task targets are headcount-bounded: employees is literally headcount, and the flagship high-volume table, attendances, is headcount x workdays x periods. A 500-person customer produces roughly 300k attendance periods over several years, which at the v1 page size of 200 and a 2000 req/min ceiling is a sub-hour full refresh. Missing incrementality would be a tax here, not a blocker.",
   "score": 64.0,
   "grade": "C",
   "dimensions": {
    "incremental": 4,
    "timestamps": 3,
    "deletion": 1,
    "pk": 3,
    "pagination": 3,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 3,
    "auth": 4
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI. Both generations publish OpenAPI 3.0 and both resolve (verified HTTP 200 on 2026-08-10):",
   "docs_url": "https://developer.personio.de/",
   "one_liner": "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.",
   "scorecard": "results/personio.md"
  },
  {
   "rank": 30,
   "id": "twilio",
   "name": "Twilio",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship table is `messages`, one row per SMS/MMS/WhatsApp message; this is the same shape as the email-send tables the rubric puts in LARGE, and Twilio's own retention design confirms it: message records age out of the API after 13 months and the recommended long-term path is gzipped daily file dumps, which is not something you build for a table with tens of thousands of rows",
   "score": 62.4,
   "grade": "C",
   "dimensions": {
    "incremental": 2,
    "timestamps": 3,
    "deletion": 1,
    "pk": 4,
    "pagination": 5,
    "bulk_backfill": 4,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://github.com/twilio/twilio-oai). Verified resolving: repo page 200; `https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json` returns 200 at 1,869,905 bytes, `openapi...",
   "docs_url": "https://www.twilio.com/docs/messaging/api/message-resource",
   "one_liner": "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.",
   "scorecard": "results/twilio.md"
  },
  {
   "rank": 31,
   "id": "mailchimp",
   "name": "Mailchimp",
   "volume_class": "LARGE",
   "volume_class_reason": "email activity is row-per-open and row-per-click. The rubric puts \"email sends/opens (Klaviyo, Braze, SendGrid)\" in LARGE, and Mailchimp's `/reports/{campaign_id}/email-activity` is the same grain: one row per interaction, multiplied by every campaign ever sent. Audiences and campaigns alone would be SMALL/MEDIUM, but the highest-volume table users actually sync decides the class.",
   "score": 61.8,
   "grade": "C",
   "dimensions": {
    "incremental": 3,
    "timestamps": 3,
    "deletion": 4,
    "pk": 2,
    "pagination": 3,
    "bulk_backfill": 3,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (Swagger 2.0) — `https://api.mailchimp.com/schema/3.0/Swagger.json?expand=true`. Verified resolving: HTTP 200, `application/json`, 10,773,155 bytes, `swagger: \"2.0\"`, `info.version: \"3.0.91\"`, 181 paths, `x...",
   "docs_url": "https://mailchimp.com/developer/marketing/api/",
   "one_liner": "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.",
   "scorecard": "results/mailchimp.md"
  },
  {
   "rank": 32,
   "id": "trustpilot",
   "name": "Trustpilot",
   "volume_class": "SMALL",
   "volume_class_reason": "one business unit's reviews are thousands to tens of thousands of rows for a typical API-module subscriber, and Trustpilot itself caps the filtered reviews endpoint at 100,000 records, so a full refresh is at most ~1,000 calls at 100 rows per page, which is minutes of work; the real tax on non-incrementality here is the annual API call allowance, not runtime",
   "score": 61.2,
   "grade": "C",
   "dimensions": {
    "incremental": 2,
    "timestamps": 3,
    "deletion": 4,
    "pk": 5,
    "pagination": 3,
    "bulk_backfill": 3,
    "rate_limits": 2,
    "schema_docs": 3,
    "auth": 2
   },
   "spec": "partial",
   "spec_detail": "Trustpilot</title>` shell, and `htt...",
   "docs_url": "https://developers.trustpilot.com/business-units-api",
   "one_liner": "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.",
   "scorecard": "results/trustpilot.md"
  },
  {
   "rank": 33,
   "id": "g2",
   "name": "G2",
   "volume_class": "MEDIUM",
   "volume_class_reason": "the largest table an actual subscriber syncs is `buyer_intent`, one row per company/day/signal-type, which accumulates to hundreds of thousands to low millions of rows for an active Marketing Solutions subscriber; `reviews` is bounded in the low thousands per product, so it alone would be SMALL, but the rubric classifies by the highest-volume table users actually sync",
   "score": 61.0,
   "grade": "C",
   "dimensions": {
    "incremental": 3,
    "timestamps": 2,
    "deletion": 3,
    "pk": 3,
    "pagination": 4,
    "bulk_backfill": 3,
    "rate_limits": 4,
    "schema_docs": 3,
    "auth": 2
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://data.g2.com/openapi/v2.yaml) - verified HTTP 200, `text/yaml`, 460,562 bytes, `openapi: 3.0.1`, 54 paths; plus https://data.g2.com/openapi/data_solutions.yaml (HTTP 200, 29,933 bytes) for the beta ...",
   "docs_url": "https://data.g2.com/api/v2/docs/index.html",
   "one_liner": "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.",
   "scorecard": "results/g2.md"
  },
  {
   "rank": 34,
   "id": "gorgias",
   "name": "Gorgias",
   "volume_class": "MEDIUM",
   "volume_class_reason": "helpdesk tickets grow with support volume; messages run several rows per ticket, so a busy merchant reaches low millions of message rows, and full refresh is survivable but painful. Same class as Zendesk per the rubric.",
   "score": 61.0,
   "grade": "C",
   "dimensions": {
    "incremental": 2,
    "timestamps": 3,
    "deletion": 4,
    "pk": 4,
    "pagination": 4,
    "bulk_backfill": 2,
    "rate_limits": 3,
    "schema_docs": 3,
    "auth": 5
   },
   "spec": "partial",
   "spec_detail": "** partial. No OpenAPI or GraphQL schema published. `https://developers.gorgias.com/llms.txt` resolves (HTTP 200) but is a markdown page index with no OpenAPI entries; `/openapi.json`, `/openapi.yaml`, `/docs.json`, `...",
   "docs_url": "https://developers.gorgias.com/reference/introduction",
   "one_liner": "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.",
   "scorecard": "results/gorgias.md"
  },
  {
   "rank": 35,
   "id": "google_ads",
   "name": "Google Ads",
   "volume_class": "LARGE",
   "volume_class_reason": "the tables people actually sync are performance reports at ad or keyword or search-term by day by network by device grain, plus `click_view` at one row per gclid per day, which is millions of rows for a single mid-size advertiser",
   "score": 58.0,
   "grade": "D",
   "dimensions": {
    "incremental": 2,
    "timestamps": 1,
    "deletion": 5,
    "pk": 3,
    "pagination": 4,
    "bulk_backfill": 3,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 3
   },
   "spec": "none",
   "spec_detail": "none (Google Ads API is gRPC/protobuf only, absent from the Google API Discovery directory at https://www.googleapis.com/discovery/v1/apis, which lists neighboring products such as adsense and searchads360 but no goog...",
   "docs_url": "https://developers.google.com/google-ads/api/docs/start",
   "one_liner": "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.",
   "scorecard": "results/google_ads.md"
  },
  {
   "rank": 36,
   "id": "snapchat_ads",
   "name": "Snapchat Ads",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship table is ad-level stats at DAY or HOUR granularity, optionally exploded by `report_dimension` country/age/gender/os, which is row-per-interaction reporting data in the millions per account",
   "score": 58.0,
   "grade": "D",
   "dimensions": {
    "incremental": 1,
    "timestamps": 3,
    "deletion": 4,
    "pk": 3,
    "pagination": 4,
    "bulk_backfill": 4,
    "rate_limits": 3,
    "schema_docs": 3,
    "auth": 4
   },
   "spec": "none",
   "spec_detail": "none (no official OpenAPI/Swagger or GraphQL introspection; the specs found in the wild, e.g. Jentic's, are unofficial third-party reconstructions)",
   "docs_url": "https://developers.snap.com/api/marketing-api/Ads-API/introduction",
   "one_liner": "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.",
   "scorecard": "results/snapchat_ads.md"
  },
  {
   "rank": 37,
   "id": "pinterest",
   "name": "Pinterest",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship table is ads analytics at ad/day grain, and with `targeting_types` breakdowns like KEYWORD, LOCATION metro code, AGE_BUCKET and GENDER it is row-per-interaction reporting in the millions",
   "score": 57.0,
   "grade": "D",
   "dimensions": {
    "incremental": 1,
    "timestamps": 3,
    "deletion": 4,
    "pk": 3,
    "pagination": 4,
    "bulk_backfill": 3,
    "rate_limits": 4,
    "schema_docs": 4,
    "auth": 3
   },
   "spec": "yes",
   "spec_detail": "OpenAPI (https://github.com/pinterest/api-description, v5 spec)",
   "docs_url": "https://developers.pinterest.com/docs/api/v5/",
   "one_liner": "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.",
   "scorecard": "results/pinterest.md"
  },
  {
   "rank": 38,
   "id": "wise",
   "name": "Wise",
   "volume_class": "MEDIUM",
   "volume_class_reason": "balance-statement transaction lines and transfers grow with payout activity: tens of thousands to low millions of rows for an active business, painful to full-refresh but not row-per-event telemetry",
   "score": 57.0,
   "grade": "D",
   "dimensions": {
    "incremental": 3,
    "timestamps": 2,
    "deletion": 3,
    "pk": 3,
    "pagination": 2,
    "bulk_backfill": 3,
    "rate_limits": 4,
    "schema_docs": 3,
    "auth": 2
   },
   "spec": "partial",
   "spec_detail": "** partial. The reference is generated from an OpenAPI definition (Redocly Realm, definition id `api-reference/@latest/index.yaml`, exposed in `https://docs.wise.com/page-data/api-reference/balance/data.json`), but no...",
   "docs_url": "https://docs.wise.com/api-reference",
   "one_liner": "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.",
   "scorecard": "results/wise.md"
  },
  {
   "rank": 39,
   "id": "surveymonkey",
   "name": "SurveyMonkey",
   "volume_class": "MEDIUM",
   "volume_class_reason": "The flagship table is `survey_responses`, one row per submitted response with the full answer payload attached, unbounded over time and growing with business activity. It never reaches event-stream scale because response volume is itself capped by the customer's SurveyMonkey plan allowance, but an NPS/CSAT/market-research deployment lands in the hundreds of thousands to low millions of rows, and full refresh is genuinely painful because the API rations you to 500 requests per day at 100 responses per page. Same shape as the rubric's canonical MEDIUM cases (tickets, conversations) and identical in shape to Typeform.",
   "score": 56.0,
   "grade": "D",
   "dimensions": {
    "incremental": 4,
    "timestamps": 3,
    "deletion": 1,
    "pk": 4,
    "pagination": 3,
    "bulk_backfill": 2,
    "rate_limits": 2,
    "schema_docs": 3,
    "auth": 2
   },
   "spec": "none",
   "spec_detail": "none.** Verified:",
   "docs_url": "https://api.surveymonkey.com/v3/docs",
   "one_liner": "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.",
   "scorecard": "results/surveymonkey.md"
  },
  {
   "rank": 40,
   "id": "customerio",
   "name": "Customer.io",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship tables are `messages` / deliveries, one row per message or webhook sent to one person with its per-metric timestamps, and `activities`, the workspace event feed covering sends, opens, clicks, attribute changes and profile deletes; the rubric puts email sends/opens in LARGE and Customer.io's own `/v1/workspaces` response counts billable messages sent for exactly this reason",
   "score": 55.2,
   "grade": "D",
   "dimensions": {
    "incremental": 2,
    "timestamps": 2,
    "deletion": 4,
    "pk": 3,
    "pagination": 4,
    "bulk_backfill": 2,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://docs.customer.io/files/journeys-app.json). Verified: HTTP 200, OpenAPI 3.1.0, title \"Customer.io App API\", 122 paths, both US and EU servers declared. The published reference at https://docs.custom...",
   "docs_url": "https://docs.customer.io/integrations/api/app/",
   "one_liner": "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.",
   "scorecard": "results/customerio.md"
  },
  {
   "rank": 41,
   "id": "posthog",
   "name": "PostHog",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship table is `events`, one row per product-analytics event; a typical customer produces millions to billions of rows, and PostHog itself meters batch exports in units of a million rows",
   "score": 54.8,
   "grade": "D",
   "dimensions": {
    "incremental": 2,
    "timestamps": 2,
    "deletion": 3,
    "pk": 4,
    "pagination": 2,
    "bulk_backfill": 3,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://us.posthog.com/api/schema/). Verified resolving: HTTP 200, OpenAPI 3.1.0, 6.8 MB YAML, no authentication required. Mirrored at `https://app.posthog.com/api/schema/` and `https://eu.posthog.com/api/...",
   "docs_url": "https://posthog.com/docs/api",
   "one_liner": "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.",
   "scorecard": "results/posthog.md"
  },
  {
   "rank": 42,
   "id": "facebook_ads",
   "name": "Facebook Ads",
   "volume_class": "LARGE",
   "volume_class_reason": "insights at ad/day grain across accounts and breakdowns is row-per-interaction reporting data in the millions",
   "score": 53.4,
   "grade": "D",
   "dimensions": {
    "incremental": 2,
    "timestamps": 3,
    "deletion": 4,
    "pk": 3,
    "pagination": 4,
    "bulk_backfill": 2,
    "rate_limits": 2,
    "schema_docs": 3,
    "auth": 3
   },
   "spec": "partial",
   "spec_detail": "partial (Meta publishes a proprietary, non-OpenAPI JSON schema per node type, used for its own SDK codegen, at https://github.com/facebook/facebook-business-sdk-codegen/tree/main/api_specs/specs, e.g. https://raw.gith...",
   "docs_url": "https://developers.facebook.com/docs/marketing-api/",
   "one_liner": "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.",
   "scorecard": "results/facebook_ads.md"
  },
  {
   "rank": 43,
   "id": "reddit_ads",
   "name": "Reddit Ads",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship table is `POST /ad_accounts/{id}/reports` at ad x date x breakdown grain over 24 months of history, which is row-per-interaction reporting data; a `COMMUNITY` or `KEYWORD` breakdown multiplies every ad-day by thousands of subreddits or keywords",
   "score": 52.8,
   "grade": "D",
   "dimensions": {
    "incremental": 1,
    "timestamps": 3,
    "deletion": 4,
    "pk": 3,
    "pagination": 4,
    "bulk_backfill": 2,
    "rate_limits": 4,
    "schema_docs": 3,
    "auth": 4
   },
   "spec": "yes",
   "spec_detail": "OpenAPI 3.1 (https://ads-api.reddit.com/api/v3/openapi.json)",
   "docs_url": "https://ads-api.reddit.com/docs/v3/api/reddit-advertising-api",
   "one_liner": "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.",
   "scorecard": "results/reddit_ads.md"
  },
  {
   "rank": 44,
   "id": "revenuecat",
   "name": "RevenueCat",
   "volume_class": "LARGE",
   "volume_class_reason": "a customer row is created for every SDK install, including `$RCAnonymousID:...` users, and the flagship warehouse table is per-transaction subscription revenue; a mid-size consumer app produces millions of customers and tens of millions of transaction rows",
   "score": 49.4,
   "grade": "D",
   "dimensions": {
    "incremental": 1,
    "timestamps": 2,
    "deletion": 0,
    "pk": 3,
    "pagination": 3,
    "bulk_backfill": 4,
    "rate_limits": 4,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://www.revenuecat.com/docs/redocusaurus/openapi-v2.yaml). Verified 2026-08-10: HTTP 200, `application/yaml`, 544,559 bytes, OpenAPI document titled \"Developer API\" version 2.0.0, covering all v2 tags....",
   "docs_url": "https://www.revenuecat.com/docs/api-v2",
   "one_liner": "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.",
   "scorecard": "results/revenuecat.md"
  },
  {
   "rank": 45,
   "id": "slack",
   "name": "Slack",
   "volume_class": "MEDIUM",
   "volume_class_reason": "messages grow with headcount times tenure; an established workspace is low millions of rows, and thread replies are a multiple on top. The rubric names Slack messages as a MEDIUM example. Full refresh is survivable in principle but see dimension 7 for why it is not.",
   "score": 49.0,
   "grade": "D",
   "dimensions": {
    "incremental": 3,
    "timestamps": 2,
    "deletion": 1,
    "pk": 3,
    "pagination": 4,
    "bulk_backfill": 2,
    "rate_limits": 1,
    "schema_docs": 3,
    "auth": 3
   },
   "spec": "partial",
   "spec_detail": "** partial. `https://api.slack.com/specs/openapi/v2/slack_web.json` is verified resolvable (HTTP 200, 1,022,938 bytes), Swagger 2.0, `info.version` 1.7.0, 170 paths, 48 definitions. Mirror at `https://raw.githubuserco...",
   "docs_url": "https://docs.slack.dev/reference/methods/",
   "one_liner": "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.",
   "scorecard": "results/slack.md"
  },
  {
   "rank": 46,
   "id": "tiktok_ads",
   "name": "TikTok Ads",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship table is the basic report at ad/day grain, optionally split by country, which is row-per-interaction data in the millions for any real advertiser",
   "score": 48.0,
   "grade": "D",
   "dimensions": {
    "incremental": 1,
    "timestamps": 3,
    "deletion": 4,
    "pk": 3,
    "pagination": 2,
    "bulk_backfill": 3,
    "rate_limits": 2,
    "schema_docs": 3,
    "auth": 3
   },
   "spec": "partial",
   "spec_detail": "partial (no OpenAPI/Swagger or GraphQL; TikTok publishes an official Postman collection for the Business API, verified 200 at https://www.postman.com/tiktok/tiktok-api-for-business/documentation/efqhadc/tiktok-busines...",
   "docs_url": "https://business-api.tiktok.com/portal/docs",
   "one_liner": "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.",
   "scorecard": "results/tiktok_ads.md"
  },
  {
   "rank": 47,
   "id": "mixpanel",
   "name": "Mixpanel",
   "volume_class": "LARGE",
   "volume_class_reason": "row-per-event product analytics; the `events` table is millions to billions of rows for a typical customer, full refresh is not an option",
   "score": 44.4,
   "grade": "D",
   "dimensions": {
    "incremental": 2,
    "timestamps": 3,
    "deletion": 0,
    "pk": 2,
    "pagination": 1,
    "bulk_backfill": 3,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 4
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://docs.mixpanel.com/openapi/export.openapi.yaml). Verified HTTP 200, 5,185 bytes, OpenAPI 3.0.2. Twelve sibling specs also verified resolving, notably https://docs.mixpanel.com/openapi/query.openapi....",
   "docs_url": "https://docs.mixpanel.com/reference/overview",
   "one_liner": "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.",
   "scorecard": "results/mixpanel.md"
  },
  {
   "rank": 48,
   "id": "sendgrid",
   "name": "SendGrid",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship table is Email Activity, one row per message per recipient plus its engagement events; a sender doing a million emails a month produces a million-plus rows a month, and SendGrid's own CSV export is capped at \"the last 1 million events\", which tells you what grain they expect",
   "score": 40.8,
   "grade": "D",
   "dimensions": {
    "incremental": 3,
    "timestamps": 3,
    "deletion": 0,
    "pk": 3,
    "pagination": 1,
    "bulk_backfill": 1,
    "rate_limits": 2,
    "schema_docs": 3,
    "auth": 3
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://github.com/twilio/sendgrid-oai). Verified resolving: the repo page returns 200, and individual specs fetch 200 as raw files, e.g. https://raw.githubusercontent.com/twilio/sendgrid-oai/main/spec/yam...",
   "docs_url": "https://www.twilio.com/docs/sendgrid/api-reference",
   "one_liner": "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.",
   "scorecard": "results/sendgrid.md"
  },
  {
   "rank": 49,
   "id": "zoom",
   "name": "Zoom",
   "volume_class": "MEDIUM",
   "volume_class_reason": "the flagship table is `participants`: one row per participant per meeting session, with rejoins emitting extra rows. A few hundred hosts produce roughly hundreds of thousands to low millions of rows a year, and Zoom's own 6-month report retention caps it below event-scale.",
   "score": 40.0,
   "grade": "D",
   "dimensions": {
    "incremental": 2,
    "timestamps": 1,
    "deletion": 1,
    "pk": 2,
    "pagination": 4,
    "bulk_backfill": 1,
    "rate_limits": 2,
    "schema_docs": 3,
    "auth": 3
   },
   "spec": "yes",
   "spec_detail": "** OpenAPI (https://developers.zoom.us/api-hub/meetings/methods/endpoints.json) - verified 200, `\"openapi\": \"3.0.0\"`, per-product documents (also `.../accounts/...`, `.../users/...`, `.../phone/...`; `.../team-chat/.....",
   "docs_url": "https://developers.zoom.us/docs/api/",
   "one_liner": "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.",
   "scorecard": "results/zoom.md"
  },
  {
   "rank": 50,
   "id": "adapty",
   "name": "Adapty",
   "volume_class": "MEDIUM",
   "volume_class_reason": "the flagship tables are the six day-grain analytics charts, which grow with business activity once `segmentation` and `country` filters are applied and land in the hundreds of thousands to low millions of rows across a multi-year, multi-chart sync; the row-per-transaction event grain that would make this LARGE is not exposed by any API endpoint at all, so the biggest table you can actually build from the API is an aggregate one, and a full re-backfill is painful but survivable",
   "score": 40.0,
   "grade": "D",
   "dimensions": {
    "incremental": 1,
    "timestamps": 1,
    "deletion": 1,
    "pk": 1,
    "pagination": 2,
    "bulk_backfill": 3,
    "rate_limits": 3,
    "schema_docs": 3,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "OpenAPI (https://adapty.io/docs/api-specs/export-analytics-api.yaml and https://adapty.io/docs/api-specs/adapty-api.yaml)**",
   "docs_url": "https://adapty.io/docs/api-export-analytics",
   "one_liner": "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.",
   "scorecard": "results/adapty.md"
  },
  {
   "rank": 51,
   "id": "apple_ads",
   "name": "Apple Search Ads",
   "volume_class": "LARGE",
   "volume_class_reason": "the tables people actually sync are `ad_reports` / `ad_group_reports` at daily granularity with `groupBy` on country, admin area, locality, device class, age and gender, which is row-per-ad-per-day-per-geo reporting data in the millions",
   "score": 38.8,
   "grade": "F",
   "dimensions": {
    "incremental": 1,
    "timestamps": 2,
    "deletion": 4,
    "pk": 2,
    "pagination": 3,
    "bulk_backfill": 1,
    "rate_limits": 2,
    "schema_docs": 3,
    "auth": 3
   },
   "spec": "none",
   "spec_detail": "none (no OpenAPI/Swagger or GraphQL introspection published for the Campaign Management API; contrast with App Store Connect, which does ship one)",
   "docs_url": "https://developer.apple.com/documentation/apple_ads/apple-search-ads-campaign-management-api-5",
   "one_liner": "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.",
   "scorecard": "results/apple_ads.md"
  },
  {
   "rank": 52,
   "id": "braze",
   "name": "Braze",
   "volume_class": "LARGE",
   "volume_class_reason": "message engagement is row-per-send/open/click data in the millions to billions, and the biggest thing the REST API will actually hand you, `/users/export/segment`, is one row per user per segment across the whole profile base; full refresh of either is not an option",
   "score": 37.6,
   "grade": "F",
   "dimensions": {
    "incremental": 1,
    "timestamps": 1,
    "deletion": 1,
    "pk": 3,
    "pagination": 3,
    "bulk_backfill": 1,
    "rate_limits": 4,
    "schema_docs": 3,
    "auth": 5
   },
   "spec": "partial",
   "spec_detail": "** partial (unofficial). Braze publishes no OpenAPI document; the official machine-readable artifact is a Postman collection (https://www.braze.com/docs/api/postman_collection/). A community-maintained OpenAPI 3.0.0 s...",
   "docs_url": "https://www.braze.com/docs/api/basics/",
   "one_liner": "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.",
   "scorecard": "results/braze.md"
  },
  {
   "rank": 53,
   "id": "linkedin_ads",
   "name": "LinkedIn Ads",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship table is `adAnalytics` at pivot-by-day grain, and with a demographic pivot LinkedIn returns the top 100 professional-demographic values per creative per day, so one account with one pivot is millions of rows per year",
   "score": 36.4,
   "grade": "F",
   "dimensions": {
    "incremental": 1,
    "timestamps": 2,
    "deletion": 4,
    "pk": 3,
    "pagination": 1,
    "bulk_backfill": 1,
    "rate_limits": 2,
    "schema_docs": 3,
    "auth": 2
   },
   "spec": "partial",
   "spec_detail": "partial (no OpenAPI/Swagger or GraphQL; LinkedIn publishes an official Postman collection for the Marketing API, verified 200 at https://www.postman.com/linkedin-developer-apis/linkedin-marketing-solutions-versioned-a...",
   "docs_url": "https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/ads-reporting",
   "one_liner": "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.",
   "scorecard": "results/linkedin_ads.md"
  },
  {
   "rank": 54,
   "id": "adjust",
   "name": "Adjust",
   "volume_class": "LARGE",
   "volume_class_reason": "an MMP report at campaign or creative x day x country x channel grain across an app portfolio is row-per-interaction reporting data in the millions, and the `hour` dimension makes it tens of millions; re-pulling years of it on every run is not an option",
   "score": 36.4,
   "grade": "F",
   "dimensions": {
    "incremental": 1,
    "timestamps": 1,
    "deletion": 0,
    "pk": 2,
    "pagination": 1,
    "bulk_backfill": 3,
    "rate_limits": 4,
    "schema_docs": 3,
    "auth": 4
   },
   "spec": "none",
   "spec_detail": "none. No OpenAPI, Swagger, or GraphQL surface is published, consistent with the \"Schema, docs, versioning\" evidence above (no version segment in the base URL either). Verified 2026-08-10: `https://dev.adjust.com/opena...",
   "docs_url": "https://dev.adjust.com/en/api/rs-api/",
   "one_liner": "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.",
   "scorecard": "results/adjust.md"
  },
  {
   "rank": 55,
   "id": "google_search_console",
   "name": "Google Search Console",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship table is the Search Analytics report at `date x query x page x country x device` grain, the direct analogue of ad performance at ad/day/geo grain; at the documented 50,000 rows per day per site per search type that is up to ~1.5M rows a month and ~24M rows across the retention window, and full refresh is not just painful, it is impossible once data ages out",
   "score": 36.2,
   "grade": "F",
   "dimensions": {
    "incremental": 1,
    "timestamps": 1,
    "deletion": 0,
    "pk": 3,
    "pagination": 2,
    "bulk_backfill": 2,
    "rate_limits": 3,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "Google Discovery (https://searchconsole.googleapis.com/$discovery/rest?version=v1, verified HTTP 200 on 2026-08-10, GET returns a valid discovery document with typed schemas)",
   "docs_url": "https://developers.google.com/webmaster-tools/v1/api_reference_index",
   "one_liner": "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.",
   "scorecard": "results/google_search_console.md"
  },
  {
   "rank": 56,
   "id": "appstore",
   "name": "App Store Connect",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship tables are the \"Detailed\" daily analytics reports at date x territory x device x platform version x app version x source type x source info x page type x page title x campaign grain, plus the daily Subscriber detailed report; one mid-size app emits tens to hundreds of thousands of rows per day, tens of millions per year, and full refresh is not merely painful but impossible because Apple deletes report instances after 35 days",
   "score": 35.2,
   "grade": "F",
   "dimensions": {
    "incremental": 1,
    "timestamps": 1,
    "deletion": 2,
    "pk": 1,
    "pagination": 4,
    "bulk_backfill": 1,
    "rate_limits": 3,
    "schema_docs": 3,
    "auth": 4
   },
   "spec": "yes",
   "spec_detail": "OpenAPI (https://developer.apple.com/sample-code/app-store-connect/app-store-connect-openapi-specification.zip, verified HTTP 200, Content-Type: application/zip)",
   "docs_url": "https://developer.apple.com/documentation/appstoreconnectapi",
   "one_liner": "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.",
   "scorecard": "results/appstore.md"
  },
  {
   "rank": 57,
   "id": "google_analytics",
   "name": "Google Analytics 4",
   "volume_class": "LARGE",
   "volume_class_reason": "the table people actually sync is a fine-grained custom report, date x channel x source x medium x campaign x geo x landing page, which is the same row-per-interaction reporting shape the rubric puts in LARGE for ad platforms; ingestr's own test case builds exactly that 9-dimension report and its fetch loop pages 100,000 rows at a time",
   "score": 34.2,
   "grade": "F",
   "dimensions": {
    "incremental": 1,
    "timestamps": 1,
    "deletion": 0,
    "pk": 2,
    "pagination": 3,
    "bulk_backfill": 2,
    "rate_limits": 2,
    "schema_docs": 4,
    "auth": 5
   },
   "spec": "yes",
   "spec_detail": "Google Discovery (https://analyticsdata.googleapis.com/$discovery/rest?version=v1beta, verified HTTP 200 on 2026-08-10, GET returns a valid discovery document with `\"canonicalName\": \"AnalyticsData\"`)",
   "docs_url": "https://developers.google.com/analytics/devguides/reporting/data/v1",
   "one_liner": "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.",
   "scorecard": "results/google_analytics.md"
  },
  {
   "rank": 58,
   "id": "clevertap",
   "name": "CleverTap",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship table is `events`, one row per event occurrence per user for a mobile engagement and analytics platform; App Launched alone is millions of rows a month for any real app, and CleverTap's own retention policy is measured in years of that, so full refresh is not an option",
   "score": 34.2,
   "grade": "F",
   "dimensions": {
    "incremental": 2,
    "timestamps": 1,
    "deletion": 0,
    "pk": 1,
    "pagination": 3,
    "bulk_backfill": 1,
    "rate_limits": 3,
    "schema_docs": 3,
    "auth": 4
   },
   "spec": "partial",
   "spec_detail": "partial.** No OpenAPI and no GraphQL. `https://developer.clevertap.com/openapi.json` and `/openapi.yaml` both 404, and `https://developer.clevertap.com/reference` 404s, so there are no spec-backed reference pages at a...",
   "docs_url": "https://developer.clevertap.com/docs/api-overview",
   "one_liner": "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.",
   "scorecard": "results/clevertap.md"
  },
  {
   "rank": 59,
   "id": "applovin",
   "name": "AppLovin",
   "volume_class": "LARGE",
   "volume_class_reason": "the MAX user-level endpoint with `aggregated=false` is one row per impression per app per day, and the advertiser report at ad/creative/country/day grain is the same shape as Meta or TikTok ad reporting",
   "score": 26.6,
   "grade": "F",
   "dimensions": {
    "incremental": 1,
    "timestamps": 1,
    "deletion": 0,
    "pk": 1,
    "pagination": 3,
    "bulk_backfill": 1,
    "rate_limits": 2,
    "schema_docs": 2,
    "auth": 5
   },
   "spec": "none",
   "spec_detail": "none. No official OpenAPI, Swagger, or GraphQL schema, consistent with the \"no version segment, no changelog\" finding above; the only spec-shaped artifact cited in the docs is a third-party reconstruction. Verified 20...",
   "docs_url": "https://support.applovin.com/en/growth/promoting-your-apps/api/reporting-api",
   "one_liner": "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.",
   "scorecard": "results/applovin.md"
  },
  {
   "rank": 60,
   "id": "appsflyer",
   "name": "AppsFlyer",
   "volume_class": "LARGE",
   "volume_class_reason": "the flagship tables are the raw installs and raw in-app events reports at one row per event, which is millions to billions of rows for any real app; AppsFlyer itself caps a single response at 1M rows and expects you to split a single day into sub-day windows",
   "score": 24.4,
   "grade": "F",
   "dimensions": {
    "incremental": 1,
    "timestamps": 1,
    "deletion": 3,
    "pk": 1,
    "pagination": 0,
    "bulk_backfill": 1,
    "rate_limits": 1,
    "schema_docs": 3,
    "auth": 2
   },
   "spec": "none",
   "spec_detail": "none. No OpenAPI, Swagger, or GraphQL introspection is published. Verified 2026-08-10: `https://dev.appsflyer.com/openapi.json` redirects (302) to the ReadMe docs shell rather than a spec file, `https://api.appsflyer....",
   "docs_url": "https://dev.appsflyer.com/hc/reference/raw_data_pull_api_tokenv2-overview",
   "one_liner": "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.",
   "scorecard": "results/appsflyer.md"
  }
 ]
}