RORK LABJP
FUNDING — Rork's $15M seed was led by Left Lane Capital with Peak XV, True Ventures, Goodwater, and a16z SpeedrunGROWTH — Rork keeps growing with 743K monthly visits and an 85% growth rateMAX — Rork Max generates native Swift apps for iPhone, iPad, Watch, TV, Vision Pro, and iMessageMAX — It reaches HealthKit, Core ML, and Dynamic Island — territory React Native struggles withMARKET — Apple pushes agentic coding in Xcode 27, accelerating AI-driven native developmentMARKET — Gartner projects 75% of new apps will be low-code or no-code by the end of 2026FUNDING — Rork's $15M seed was led by Left Lane Capital with Peak XV, True Ventures, Goodwater, and a16z SpeedrunGROWTH — Rork keeps growing with 743K monthly visits and an 85% growth rateMAX — Rork Max generates native Swift apps for iPhone, iPad, Watch, TV, Vision Pro, and iMessageMAX — It reaches HealthKit, Core ML, and Dynamic Island — territory React Native struggles withMARKET — Apple pushes agentic coding in Xcode 27, accelerating AI-driven native developmentMARKET — Gartner projects 75% of new apps will be low-code or no-code by the end of 2026
Articles/Dev Tools
Dev Tools/2026-07-03Advanced

When Your App Store Connect API Pipeline Quietly Drops Days — Field Notes on JWT Expiry, Report Lag, and Reconciliation

Automated App Store Connect API pipelines rarely stop — they leak. This piece breaks down the three silent failure modes behind 401, 404, and 429, and shows how a fetch ledger, a 72-hour backfill window, and a weekly reconciliation query keep daily sales and review data complete.

App Store Connect API3JWT3Sales ReportAutomation8Supabase33Rate LimitsBackfillRork482

Premium Article

At the start of the month I opened App Store Connect, compared the dashboard totals against the daily sales rows I had been accumulating in Supabase, and the numbers were off by about 3% — three days out of thirty were missing their sales rows entirely.

The daily Slack report had arrived every single morning. That was exactly why I trusted the pipeline. In reality, the fetch for a handful of days had failed quietly, and those rows simply never existed when the month closed.

An automated pipeline that stops is easy to notice. One that leaks is not — as long as notifications keep arriving, the holes stay invisible. These field notes document the gap patterns I actually hit while automating sales and review collection with the App Store Connect API (ASC API), along with the detection and repair mechanisms that fixed them, code included.

Gaps Arrive Wearing Three Different Faces

Auditing a month of execution logs, every missing row traced back to one of three causes.

SymptomHTTP statusRoot causeWhat goes missing
Auth failure401JWT expiry, clock skew, lost newlines in the .p8 keyEverything in that run
Report not ready404Apple's daily report generation laggingSales rows for one specific date
Dropped pages429Rate limiting mid-paginationThe later pages of reviews

What makes these three nasty is that they share one property: swallow the exception and everything works fine again tomorrow. Without retries, only the hole for the failed day remains.

Some groundwork first. The ASC API requires an ES256-signed JWT on every request, with a maximum token lifetime of 20 minutes. The Sales and Trends API returns daily reports as .tsv.gz, but the previous day's report is finalized in the early hours Pacific time — which can slide into the evening or even the next day in other timezones. And the rate limits are not publicly documented. Those three facts map directly onto the 401, 404, and 429 failure modes.

Measure Token Age Instead of Minting Blindly

My first implementation logged nothing more than "got a 401" on failure. That gives you no way to tell whether the token was stale or the key material was broken.

Caching the token together with its issue time, and logging the token's age whenever a request fails, made the diagnosis almost mechanical.

// supabase/functions/_shared/asc-token.ts
import { create } from "https://deno.land/x/djwt@v3.0.2/mod.ts";
 
const ISSUER_ID = Deno.env.get("ASC_ISSUER_ID")!;
const KEY_ID = Deno.env.get("ASC_KEY_ID")!;
const PRIVATE_KEY_PEM = Deno.env.get("ASC_PRIVATE_KEY")!;
 
let cached: { token: string; issuedAt: number } | null = null;
 
// Refresh at 15 minutes — a 5-minute buffer against the 20-minute cap
const REFRESH_AFTER_MS = 15 * 60 * 1000;
 
export async function getAscToken(): Promise<string> {
  const now = Date.now();
  if (cached && now - cached.issuedAt < REFRESH_AFTER_MS) {
    return cached.token;
  }
 
  const pemBody = PRIVATE_KEY_PEM
    .replace("-----BEGIN PRIVATE KEY-----", "")
    .replace("-----END PRIVATE KEY-----", "")
    .replace(/\s/g, "");
  const binaryKey = Uint8Array.from(atob(pemBody), (c) => c.charCodeAt(0));
 
  const privateKey = await crypto.subtle.importKey(
    "pkcs8",
    binaryKey,
    { name: "ECDSA", namedCurve: "P-256" },
    false,
    ["sign"],
  );
 
  const nowSec = Math.floor(now / 1000);
  const token = await create(
    { alg: "ES256", kid: KEY_ID, typ: "JWT" },
    {
      iss: ISSUER_ID,
      iat: nowSec,
      exp: nowSec + 1140, // 19 minutes — one under the cap to absorb clock skew
      aud: "appstoreconnect-v1",
    },
    privateKey,
  );
 
  cached = { token, issuedAt: now };
  return token;
}
 
/** Evidence for deciding whether a 401 was caused by a stale token */
export function tokenAgeMs(): number | null {
  return cached ? Date.now() - cached.issuedAt : null;
}

Setting exp to 1140 seconds rather than the full 1200 protects against the "freshly issued yet already expired" accident that clock skew between your runtime and Apple's servers can produce. In cold-start environments like Edge Functions, key import time widens the gap between iat and the actual request, so that one-minute margin earns its keep.

The other classic 401 cause is the .p8 key losing its newlines. Paste it into a secret store carelessly and you get a key that still parses as PEM but produces broken signatures — a genuinely confusing failure. The reliable fix was procedural: a smoke test that calls /v1/apps once right after every deploy and asserts a 200.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
How to classify the three silent gap patterns caused by 401, 404, and 429, and detect missing days mechanically with a fetch ledger table
Working code for a 72-hour backfill window that treats Sales API 404s as pending rather than empty, absorbing Apple's report-generation lag
A generate_series reconciliation query for weekly completeness proofs, plus an upsert pattern that keeps review collection idempotent and analysis costs flat
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-06-23
DAU Went Up but Retention Didn't — Rebuilding Gamification That Actually Sticks in Rork Apps
Points, badges, and leaderboards lift DAU, but retention is a different story. Field notes on a server-authoritative point ledger, streaks that forgive, and leaderboards that don't crush newcomers — with working code for Rork apps.
Dev Tools2026-05-26
Aggregating App Store Connect, RevenueCat, and AdMob into One Morning Digest with Cloudflare Workers — A Six-App Indie Architecture
A practical architecture for indie developers running multiple Rork apps: aggregate App Store Connect API, RevenueCat REST, and AdMob Reporting API in a Cloudflare Workers Cron job and ship a single daily Slack digest each morning.
Dev Tools2026-04-19
Offline-First Architecture in Rork Apps: WatermelonDB + Supabase Sync
A complete guide to implementing offline-first architecture in Rork apps using WatermelonDB and Supabase Realtime. Covers local caching, optimistic updates, conflict resolution, and cross-device sync.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →