Deploy & Catch
Catch — symbol upload
POST /api/v1/catch/symbols — upload source maps, dSYM bundles, R8 / ProGuard mappings, NDK symbols, or Flutter symbols. Multipart form upload. Synchronous to S3.
To get readable stack traces in the dashboard, you upload your build's symbols at deploy time. Catch ingest stores raw stack frames; the symbolication worker patches them with source-mapped function names + line numbers as soon as a matching artifact lands.
The CLI (sankofa catch symbols upload) wraps this endpoint. For CI integrations or custom pipelines, call it directly.
Upload symbol artifact
Authentication
Authorization: Bearer <jwt> — Editor role minimum. Using a Deploy token (sk_deploy_*) or project API key (sk_live_*) does not work — symbol upload is a dashboard / management action.
For CI integrations, mint a long-lived JWT from /dashboard/<project>/settings/api-tokens with the Editor role.
Content-Type
multipart/form-data (file upload).
Form fields
filefileRequiredkindstringRequiredenvironmentstringreleasestringmatch_keystringcommit_shastringExample request
# Upload JavaScript source maps from a Vite build
curl -X POST https://api.sankofa.dev/api/v1/catch/symbols \
-H "Authorization: Bearer eyJhbGciOiJI..." \
-F "file=@./dist/app.js.map" \
-F "kind=js_sourcemap" \
-F "environment=live" \
-F "release=v1.2.0" \
-F "match_key=app.js" \
-F "commit_sha=a3b9ff1234567890..."Response (201 Created)
{
"artifact": {
"id": "sym_abc123",
"project_id": "proj_xyz",
"environment": "live",
"kind": "js_sourcemap",
"release": "v1.2.0",
"match_key": "app.js",
"size_bytes": 51200,
"sha256": "8c2f7a9b...",
"status": "ready",
"uploaded_at": "2026-05-09T14:32:01.482Z",
"storage_path": "s3://catch-symbols/proj_xyz/sym_abc123.map",
"original_name": "app.js.map"
}
}status: "ready" means the upload completed and the artifact is available for the symbolication worker. The handler doesn't return until the S3 write is durable, so a 201 guarantees usability.
Errors
| Status | Body | When |
|---|---|---|
400 | {"error": "missing_file"} | No file form field |
400 | {"error": "invalid_kind"} | kind not in the allowed enum |
400 | {"error": "file_too_large"} | File > 512 MB |
401 | {"error": "missing_authorization"} | No JWT |
403 | {"error": "permission_denied"} | JWT role isn't Editor or higher |
413 | {"error": "payload_too_large"} | Total request exceeds the 500 MB body cap |
List symbol artifacts
Authentication
Authorization: Bearer <jwt> — Viewer role minimum.
Query parameters
environmentstringkindstringreleasestringlimitintegerResponse
{
"artifacts": [
{
"id": "sym_abc123",
"kind": "js_sourcemap",
"release": "v1.2.0",
"match_key": "app.js",
"size_bytes": 51200,
"status": "ready",
"uploaded_at": "2026-05-09T14:32:01.482Z"
},
{
"id": "sym_def456",
"kind": "ios_dsym",
"release": "v1.2.0",
"match_key": "550e8400-e29b-41d4-a716-446655440000",
"size_bytes": 2048576,
"status": "ready",
"uploaded_at": "2026-05-09T14:31:42.100Z"
}
]
}Delete symbol artifact
Authentication
Authorization: Bearer <jwt> — Editor role minimum.
Response
{
"deleted": true
}Idempotent — re-deleting an already-deleted artifact returns 200 {"deleted": true} without error.
Note: deleting an artifact does not retroactively unsymbolicate events that were already symbolicated against it. The symbolicated stacks are persisted on the event row.
Per-platform upload patterns
Web (JavaScript source maps)
# Vite (sourcemap: true in vite.config.ts)
sankofa catch symbols upload \
--kind js_sourcemap --release "$GIT_SHA" --dir ./dist
# Next.js (productionBrowserSourceMaps: true)
sankofa catch symbols upload \
--kind js_sourcemap --release "$GIT_SHA" --dir ./.next/static/chunks
# Webpack / esbuild — same --dir patternThe CLI walks the directory, uploads every .map file with match_key set to the matching .js filename. The matching pair is what the symbolication worker looks up.
iOS (dSYM)
# Xcode build → ./build/dSYMs/MyApp.app.dSYM
sankofa catch symbols upload \
--kind ios_dsym --release "$GIT_SHA" --dir ./build/dSYMsThe CLI zips each .dSYM bundle (Apple's nested directory format) and uploads it. The engine reads the bundle's Contents/Resources/DWARF/MyApp Mach-O file's LC_UUID load command to extract the debug_id automatically — you don't need to pass match_key.
Android (R8 / ProGuard mapping)
# After ./gradlew assembleRelease
sankofa catch symbols upload \
--kind android_mapping --release "$GIT_SHA" \
--file ./app/build/outputs/mapping/release/mapping.txtThe mapping file is the R8 / ProGuard output that maps obfuscated names back to original ones. Single file per build.
Android NDK
# Native libraries built via NDK
sankofa catch symbols upload \
--kind android_ndk --release "$GIT_SHA" --dir ./objWalks the obj/ directory, picks up every .so file with debug info.
Flutter
# Flutter symbols artifact
sankofa catch symbols upload \
--kind flutter_symbols --release "$GIT_SHA" --dir ./build/symbolsSymbolication worker behavior
After upload, the worker:
- Verifies the artifact's
sha256against the stored row. - Indexes the artifact's
match_key(anddebug_idfor dSYM) for lookup. - Re-processes events in the last 24 hours that match
release+match_keyand have no symbolicated frames yet.
For events older than 24 hours, the engine doesn't auto-reprocess — submit a Catch support ticket if you need historical re-symbolication.
Storage
Symbols are stored in S3 (or compatible object storage) at catch/symbols/{project_id}/{symbol_id}.[bin|zip|json|txt]. There's no public access — the dashboard generates signed URLs with short TTLs to render preview / download links.
Symbols persist for the lifetime of the release (per your retention plan). Deleted releases purge their symbols on a scheduled cleanup job.
Storage and quota
Symbols don't count toward your monthly events quota — they're a separate flat-rate capacity per project tier. Hobby tier limits to ~5 GB of symbols; Pro and above are effectively uncapped (we'll reach out before we cap a customer).