-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdata.json
More file actions
1563 lines (1563 loc) · 80 KB
/
Copy pathdata.json
File metadata and controls
1563 lines (1563 loc) · 80 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{
"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,