Developer reference
A clean JSON feed for every release signal.
Pull the same ranked regression digest that powers Repaw's morning report. The endpoint is intentionally small: one authenticated GET, no request body, no query parameters, and a stable response contract.
01 · Endpoint
One request, ranked signal
/api/digest.jsonThe legacy /api/digest route remains available. Use /api/digest.json for integrations and documentation-driven clients.
02 · Authentication
Bring a session token
Send a Better Auth session token in the standard bearer header. Sign in through Repaw, then copy the token from the auth flow's set-auth-token response header.
Authorization: Bearer <session-token>03 · Request
No knobs to misconfigure
curl --fail-with-body \
-H "Authorization: Bearer <session-token>" \
"$REPAW_URL/api/digest.json"04 · Response
A stable digest contract
A successful response contains release metadata and an ordered list of regression records. Scores are numeric impact signals; replayUrl is nullable when a replay is not available.
{
"meta": {
"buildTag": "string",
"buildShippedAt": "string",
"projectName": "string",
"flowsMonitored": "integer",
"flowsReplayed": "integer",
"replayDuration": "string",
"criticalCount": "integer",
"majorCount": "integer",
"minorCount": "integer"
},
"regressions": [
{
"id": "string",
"rank": "integer",
"severity": "critical | major | minor",
"type": "crash | jank-spike | dropped-frames | dead-tap | deep-link | silent-error",
"flow": "string",
"issue": "string",
"impactText": "string",
"impactScore": "number",
"device": "string",
"buildDelta": "string",
"replayUrl": "string | null",
"timing": "string",
"likelyRootCause": "string",
"suggestedCodeChange": "string"
}
]
}DigestResponse JSON document.05 · Complete example
Copy the whole payload
This example is generated from the same validated fixture used by the endpoint, so every included regression shows the full field set with no omitted properties.
{
"meta": {
"buildTag": "#248",
"buildShippedAt": "2025-08-23 · 08:43",
"projectName": "Acme Mobile",
"flowsMonitored": 14,
"flowsReplayed": 11,
"replayDuration": "31m 07s",
"criticalCount": 3,
"majorCount": 3,
"minorCount": 2
},
"regressions": [
{
"id": "r1",
"rank": 1,
"severity": "critical",
"type": "crash",
"flow": "Sign-up · OAuth callback",
"issue": "NullPointerException on Android 14 — fresh-install token exchange",
"impactText": "~8.1% of new sign-ups",
"impactScore": 96,
"device": "Pixel 7 · Android 14",
"buildDelta": "regressed in #248",
"replayUrl": "https://app.repaw.ai/replays/demo/r1",
"timing": "1.8s after OAuth return",
"likelyRootCause": "The callback resumes before the fresh-install token exchange has hydrated its session object. Android 14 is exposing the race because the browser handoff returns faster than the secure storage write completes, leaving the callback dereference with no token payload.",
"suggestedCodeChange": "const session = await exchangeOAuthToken(code);\nif (!session?.accessToken) return showAuthError();\nawait secureStore.set('accessToken', session.accessToken);"
},
{
"id": "r2",
"rank": 2,
"severity": "critical",
"type": "dead-tap",
"flow": "Checkout · Apple Pay",
"issue": "\"Pay\" button dead after deeplink return; tap never resolves",
"impactText": "~6.4% of EU checkouts",
"impactScore": 88,
"device": "iPhone 15 · iOS 17.4",
"buildDelta": "regressed in #248",
"replayUrl": "https://app.repaw.ai/replays/demo/r2",
"timing": "420ms after deeplink return",
"likelyRootCause": "The checkout screen restores its visual state but not the payment sheet listener after the app returns from the bank deeplink. The button remains enabled, yet its handler still points at the unmounted payment session and exits without resolving.",
"suggestedCodeChange": "useEffect(() => {\n paymentSheetRef.current = createPaymentSheet();\n return () => paymentSheetRef.current?.destroy();\n}, [checkoutSessionId]);"
},
{
"id": "r3",
"rank": 3,
"severity": "critical",
"type": "deep-link",
"flow": "Push → product detail",
"issue": "Deep link from push notification lands on 404 shell",
"impactText": "~5.2% of push opens",
"impactScore": 82,
"device": "Pixel 6a · Android 13",
"buildDelta": "regressed in #248",
"replayUrl": "https://app.repaw.ai/replays/demo/r3",
"timing": "0.6s after notification tap",
"likelyRootCause": "The notification payload now sends a product slug while the native route resolver still expects a numeric product id. The resolver falls through to the generic not-found shell instead of normalizing the new payload shape.",
"suggestedCodeChange": "const productId = payload.productId ?? payload.productSlug;\nif (productId) router.replace(`/product/${productId}`);"
},
{
"id": "r4",
"rank": 4,
"severity": "major",
"type": "jank-spike",
"flow": "Onboarding · step 3/5",
"issue": "TTI regressed 1.2s → 2.7s; Lottie bundle not lazy-loaded",
"impactText": "−18% completion on mid-tier",
"impactScore": 61,
"device": "Pixel 6a · Android 13",
"buildDelta": "regressed in #248",
"replayUrl": "https://app.repaw.ai/replays/demo/r4",
"timing": "TTI 1.2s → 2.7s",
"likelyRootCause": "The animation package moved into the initial onboarding bundle when step three was split into its own component. Mid-tier Android devices parse the Lottie runtime before the form is interactive, turning a visual enhancement into the dominant time-to-interactive cost.",
"suggestedCodeChange": "const WelcomeAnimation = lazy(() => import('./WelcomeAnimation'));\n\n<Suspense fallback={null}>\n <WelcomeAnimation />\n</Suspense>"
},
{
"id": "r5",
"rank": 5,
"severity": "major",
"type": "dropped-frames",
"flow": "Checkout · confirm",
"issue": "Frame rate 60→18 fps on Pixel 7 during bottom sheet dismiss",
"impactText": "~3.7% of checkouts affected",
"impactScore": 54,
"device": "Pixel 7 · Android 14",
"buildDelta": "regressed in #248",
"replayUrl": "https://app.repaw.ai/replays/demo/r5",
"timing": "60 → 18 fps for 340ms",
"likelyRootCause": "Dismissal triggers a synchronous cart recalculation and a full item-list render on the same frame as the sheet animation. The work is short on flagship hardware but blocks the UI thread long enough to produce a visible hitch on the replay device.",
"suggestedCodeChange": "startTransition(() => {\n setCartSummary(recalculateCart(cart));\n});"
},
{
"id": "r6",
"rank": 6,
"severity": "major",
"type": "dead-tap",
"flow": "Profile · edit photo",
"issue": "\"Change photo\" unresponsive after camera permission denied",
"impactText": "~2.1% of profile edits",
"impactScore": 47,
"device": "Samsung S23 · Android 14",
"buildDelta": "regressed in #247",
"replayUrl": null,
"timing": "2.4s after permission denial",
"likelyRootCause": "The denied-permission branch records the analytics event but never clears the pending picker state. Subsequent taps are ignored because the UI believes the original camera request is still active.",
"suggestedCodeChange": "catch (error) {\n setPickerPending(false);\n setCameraPermission('denied');\n}"
},
{
"id": "r7",
"rank": 7,
"severity": "minor",
"type": "jank-spike",
"flow": "Home feed · scroll",
"issue": "Scroll jank on < 3 GB RAM devices; GC pressure during inflate",
"impactText": "~1.4% of sessions",
"impactScore": 28,
"device": "Moto G54 · Android 13",
"buildDelta": "regressed in #247",
"replayUrl": null,
"timing": "18 fps at item 9",
"likelyRootCause": "The feed inflates image placeholders for every card in the viewport and retains them after they scroll away. Lower-memory devices begin collecting during a fast swipe, which steals frames from the list while the next cards are being bound.",
"suggestedCodeChange": "<FeedList\n removeClippedSubviews\n windowSize={5}\n maxToRenderPerBatch={4}\n/>"
},
{
"id": "r8",
"rank": 8,
"severity": "minor",
"type": "silent-error",
"flow": "Push · badge count",
"issue": "Badge count not cleared after push open on iOS 17",
"impactText": "~0.8% of iOS push opens",
"impactScore": 18,
"device": "iPhone 14 · iOS 17.4",
"buildDelta": "regressed in #247",
"replayUrl": null,
"timing": "stale for 1 session",
"likelyRootCause": "The push-open handler clears the in-app counter but skips the native badge update when the app is resumed from a suspended state. The next notification starts from the stale native value, so the badge appears to survive a successful open.",
"suggestedCodeChange": "AppState.addEventListener('change', (state) => {\n if (state === 'active') Notifications.setBadgeCountAsync(0);\n});"
}
]
}