4 · BugWatch — Source Maps & Symbols
/api/v1/bugwatch/artifacts/presignArtifact upload · step 1 – presign
Step 1 of 2 — request a presigned URL for a text symbolication artifact.
Scope: symbols:upload · Auth: project secret key (keyId:secret)
artifactType | File | Platform |
|---|---|---|
r8 / proguard | mapping.txt | android |
sourcemap | .map | ios · react-native · android |
dart-symbols | --split-debug-info output | flutter |
dsym | Apple dSYM referenced as an artifact | ios |
The file never passes through this API. You presign, PUT the bytes straight to storage, then confirm. That is why uploads are not bound by request-size limits — the previous raw-body routes were rejected by the edge proxy for any real symbol file.
1. POST …/presign → { uploadId, uploadUrl, expiresAt }
2. PUT <uploadUrl> → the raw bytes, streamed to storage
3. POST …/uploads/{uploadId}/complete → verified, then queued
The API key goes on steps 1 and 3 only — never to the storage host, which is authorised by the presigned URL's own signature.
Uploads are idempotent per (release, platform, artifactType) — completing a new
upload replaces the previous artifact and deletes the superseded file.
The release you declare must match the release your SDK reports at runtime, or the
artifact exists but nothing matches it and stacks stay unreadable.
Most integrators should use the CLI, which performs all three steps, streams the file (flat memory regardless of size) and retries transient failures:
# Binary debug-symbol archives — Apple dSYM, Android native .so
npx @newinstance/bugwatch-cli symbols upload MyApp.xcarchive \
--release "1.4.2" --build-number "318"
# Text artifacts — R8/ProGuard mapping, source map, Dart symbols
npx @newinstance/bugwatch-cli artifacts upload mapping.txt \
--release "1.4.2" --platform android --type r8The two commands are not interchangeable: symbols upload accepts binary archives
only and validates magic bytes, so a mapping.txt or .map is rejected.
Headers
x-api-keyResponses
Successful response
nullhttp://localhost:5050/api/v1/bugwatch/artifacts/presignHeaders
x-api-keyRequest body
Code samples
curl -X POST 'http://localhost:5050/api/v1/bugwatch/artifacts/presign'/api/v1/bugwatch/artifacts/uploads/completeArtifact upload · step 2 – complete
Step 2 of 2 — confirm the artifact landed and swap it in.
Scope: symbols:upload
What is verified
completeconfirms the object actually landed in storage and that its size matches what was declared at presign.- The pending row is looked up scoped to the calling project, so one project can never finalise another's upload (a miss returns
404). - Only an
AWAITING_UPLOADrow can be completed, so a completion cannot be replayed to re-queue processing. - For debug symbols the worker re-hashes the real bytes before indexing and rejects a mismatch (
CHECKSUM_MISMATCH). A declared checksum is never trusted on its own.
Responses
201{ fileId, sha256 }— stored and now the active artifact for that(release, platform, artifactType).409— the object was never PUT touploadUrl.400— the stored size does not match what was declared at presign.404— no such upload for this project.
Headers
x-api-keyResponses
Successful response
nullhttp://localhost:5050/api/v1/bugwatch/artifacts/uploads/completeHeaders
x-api-keyRequest body
Code samples
curl -X POST 'http://localhost:5050/api/v1/bugwatch/artifacts/uploads/complete'/api/v1/bugwatch/debug-symbols/presignDebug symbols · step 1 – presign
Step 1 of 2 — request a presigned URL for a binary debug-symbol archive.
Scope: symbols:upload · Auth: project secret key (keyId:secret)
| Platform | Accepted |
|---|---|
| ios · macos · tvos · watchos · visionos · catalyst | .zip of .dSYM bundles, .xcarchive dSYMs, or a raw Mach-O |
| android | Raw ELF .so / .debug |
The file never passes through this API. You presign, PUT the bytes straight to storage, then confirm. That is why uploads are not bound by request-size limits — the previous raw-body routes were rejected by the edge proxy for any real symbol file.
1. POST …/presign → { uploadId, uploadUrl, expiresAt }
2. PUT <uploadUrl> → the raw bytes, streamed to storage
3. POST …/uploads/{uploadId}/complete → verified, then queued
The API key goes on steps 1 and 3 only — never to the storage host, which is authorised by the presigned URL's own signature.
Matching is by debug UUID / build-id, not by release name — the identifier embedded in
the binary. That is why an upload from any build machine resolves crashes from any device
running that exact binary, and why release here is metadata for search rather than the
match key. (Source maps and mappings are the opposite: those match on release + platform.)
Declaring a sha256 that already exists for this project short-circuits as a duplicate
and returns no uploadUrl — the transfer is skipped entirely.
Most integrators should use the CLI, which performs all three steps, streams the file (flat memory regardless of size) and retries transient failures:
# Binary debug-symbol archives — Apple dSYM, Android native .so
npx @newinstance/bugwatch-cli symbols upload MyApp.xcarchive \
--release "1.4.2" --build-number "318"
# Text artifacts — R8/ProGuard mapping, source map, Dart symbols
npx @newinstance/bugwatch-cli artifacts upload mapping.txt \
--release "1.4.2" --platform android --type r8The two commands are not interchangeable: symbols upload accepts binary archives
only and validates magic bytes, so a mapping.txt or .map is rejected.
Headers
x-api-keyResponses
Successful response
nullhttp://localhost:5050/api/v1/bugwatch/debug-symbols/presignHeaders
x-api-keyRequest body
Code samples
curl -X POST 'http://localhost:5050/api/v1/bugwatch/debug-symbols/presign'/api/v1/bugwatch/debug-symbols/uploads/completeDebug symbols · step 2 – complete
Step 2 of 2 — confirm the archive landed and queue it for indexing.
Scope: symbols:upload
What is verified
completeconfirms the object actually landed in storage and that its size matches what was declared at presign.- The pending row is looked up scoped to the calling project, so one project can never finalise another's upload (a miss returns
404). - Only an
AWAITING_UPLOADrow can be completed, so a completion cannot be replayed to re-queue processing. - For debug symbols the worker re-hashes the real bytes before indexing and rejects a mismatch (
CHECKSUM_MISMATCH). A declared checksum is never trusted on its own.
Archive-type validation happens in the worker, not here, because this API never sees
the bytes. An archive that is not a zip / Mach-O / ELF lands as INVALID with
UNRECOGNIZED_ARCHIVE; an ELF declared under an Apple platform fails with
ARCHIVE_PLATFORM_MISMATCH.
Responses
200{ uploadId, status: "QUEUED" }— accepted; poll Get upload status for indexing progress.409— the archive was never PUT touploadUrl.400— declared size does not match the stored object.
Headers
x-api-keyResponses
Successful response
nullhttp://localhost:5050/api/v1/bugwatch/debug-symbols/uploads/completeHeaders
x-api-keyRequest body
Code samples
curl -X POST 'http://localhost:5050/api/v1/bugwatch/debug-symbols/uploads/complete'/api/v1/bugwatch/debug-symbols/uploadsList debug symbol uploads
List recent debug symbol uploads for this project, newest first.
Scope: symbols:read
Query params
limit(optional, int 1–200, default 50)status(optional, string) — filter byQUEUED|PROCESSING|DONE|FAILED
Success — 200 OK returns { uploads: [...] }
Headers
x-api-keyParameters
limitquerystringdefault: Number of results (default 50, max 200)Responses
200 – Uploads list
{
"uploads": [
{
"uploadId": "507f1f77bcf86cd799439011",
"status": "DONE",
"platform": "ios",
"release": "2.1.0",
"buildNumber": "1042",
"originalFilename": "MyApp.app.dSYM.zip",
"uploadedSize": 4823012,
"discoveredUuids": 3,
"validObjects": 3,
"invalidObjects": 0,
"createdAt": "2026-06-26T10:00:00.000Z",
"completedAt": "2026-06-26T10:00:15.000Z"
}
]
}http://localhost:5050/api/v1/bugwatch/debug-symbols/uploadsQuery parameters
limitHeaders
x-api-keyCode samples
curl -X GET 'http://localhost:5050/api/v1/bugwatch/debug-symbols/uploads'/api/v1/bugwatch/debug-symbols/uploads/reprocessReprocess – re-symbolicate waiting crashes
Trigger re-symbolication for crashes that arrived before symbols were uploaded.
Scope: symbols:reprocess
Previously-unsymbolicated crashes are then re-processed with the newly-uploaded symbols.
Body: empty
Success — 200 OK
{ "requeued": 12 }Headers
x-api-keyResponses
200 – Re-queued
{
"requeued": 12
}http://localhost:5050/api/v1/bugwatch/debug-symbols/uploads/reprocessHeaders
x-api-keyRequest body
Code samples
curl -X POST 'http://localhost:5050/api/v1/bugwatch/debug-symbols/uploads/reprocess'