Developer reference

Authenticated

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

GET
/api/digest.json
JSON · authenticated

The 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.

Request header
Authorization: Bearer <session-token>

03 · Request

No knobs to misconfigure

Query parameters
None. Do not append filters, pagination, or build identifiers.
Request body
None. The endpoint is GET-only and reads the current digest for the session.
cURL request
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.

DigestResponse shape
{
  "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"
    }
  ]
}
200 · OK
Returns the complete validated DigestResponse JSON document.
401 · Unauthorized
Returned when the bearer token or browser session is missing or invalid.

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.

200 response body
{
  "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});"
    }
  ]
}