Skip to main content

Endpoints

Base URL: https://nativeupdatebe.aoneahsan.com/api/public/v1 Auth header on every request: Authorization: Bearer nu_pat_…

The examples assume:

export NATIVE_UPDATE_TOKEN=nu_pat_…
export NU=https://nativeupdatebe.aoneahsan.com/api/public/v1

{app} accepts the numeric id or the string app_id (com.example.app). Use app_id in CI — it survives re-creating an app.

Token

GET /token

What this token is and what it may touch. Call it first: it saves an agent from guessing app ids, and confirms whether deletion is allowed.

curl -H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" $NU/token
{ "data": {
"name": "GitHub Actions",
"permissions": ["manage"],
"apps": [{ "id": 12, "app_id": "com.example.app", "name": "Example" }],
"last_used_at": "2026-07-16T09:12:44+00:00",
"expires_at": null,
"created_at": "2026-07-16T08:00:00+00:00"
} }

The token secret is never returned here. Copy it from the dashboard instead.

Apps

GET /apps

The apps this token manages. Paginated (20, max 50).

curl -H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" "$NU/apps?per_page=50"

GET /apps/{app}

curl -H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" $NU/apps/com.example.app
{ "data": {
"id": 12,
"app_id": "com.example.app",
"bundle_id": "com.example.app",
"name": "Example",
"platform": "android",
"platforms": ["android", "ios"],
"channels": ["production", "staging"],
"created_at": "2026-06-01T10:00:00+00:00"
} }

Creating and deleting apps stays in the dashboard, which keeps a leaked token's blast radius small.

Builds

GET /apps/{app}/builds

Newest first. Filter with channel and status.

curl -H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" \
"$NU/apps/com.example.app/builds?channel=production&status=active"
ParamValues
channelproduction · staging · development
statusuploading · processing · active · archived · failed · paused
per_page1–50 (default 20)
pagePage number

An unknown channel or status returns 422 rather than silently ignoring the filter.

GET /apps/{app}/builds/{build}

{ "data": {
"id": 42,
"version": "1.2.0",
"channel": "production",
"status": "active",
"checksum": "e3b0c44298fc1c14…",
"checksum_algorithm": "SHA-256",
"signature": "…",
"file_size": 2438012,
"release_notes": "Bug fixes",
"min_native_version": null,
"rollout_percentage": 100,
"mandatory": false,
"promoted_from_id": null,
"created_at": "2026-07-16T09:00:00+00:00"
} }

POST /apps/{app}/builds — upload

Always queued. Returns 202 with a job to poll — see Queued jobs.

curl -X POST \
-H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" \
-F "file=@./bundle.zip" \
-F "version=1.2.0" \
-F "channel=production" \
-F "release_notes=Bug fixes" \
-F "rollout_percentage=10" \
$NU/apps/com.example.app/builds
FieldRequiredNotes
fileyesA .zip bundle. Max 100 MB by default.
versionyesStrict semver: 1.2.0. Must be newer than the channel's head.
channelyesproduction · staging · development
release_notesno≤ 5000 characters
min_native_versionnoMinimum native app version
rollout_percentageno1–100, default 100
{ "data": {
"job_id": "01hq2xk8vt9r3m4n5p6q7s8t9v",
"status_url": "https://nativeupdatebe.aoneahsan.com/api/public/v1/jobs/01hq2xk8…",
"build": { "id": 42, "version": "1.2.0", "status": "processing" }
} }

Two refusals happen immediately, before any job is created:

StatusCodeWhy
409VERSION_ALREADY_EXISTSThat version is already in the channel
422VERSION_NOT_NEWERNot newer than the channel's active head

The monotonic rule blocks downgrades on a live channel — both an easy mistake and a way to re-ship a version you already patched.

PATCH /apps/{app}/builds/{build} — pause, resume, archive

# Stop serving a bad release immediately
curl -X PATCH -H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"paused"}' \
$NU/apps/com.example.app/builds/42
FieldValues
statusactive · paused · archived
release_notes≤ 5000 characters
min_native_versionstring or null

Only active builds are served to devices, so pausing takes effect on the next update check.

POST /apps/{app}/builds/{build}/promote

Copies a build into another channel without re-uploading it — the new row points at the same stored bundle.

curl -X POST -H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_channel":"production"}' \
$NU/apps/com.example.app/builds/42/promote

Returns 201 with the new build (promoted_from_id points back at the source). Promoting into its own channel returns 422 ALREADY_IN_CHANNEL.

PATCH /apps/{app}/builds/{build}/rollout

curl -X PATCH -H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"rollout_percentage":25}' \
$NU/apps/com.example.app/builds/42/rollout

rollout_percentage is 0–100. Ramp a release: 10 → 25 → 50 → 100.

DELETE /apps/{app}/builds/{build}

Needs a token with builds.delete; otherwise 403 TOKEN_PERMISSION_DENIED.

curl -X DELETE -H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" \
$NU/apps/com.example.app/builds/42

Deletion is permanent, and the stored bundle is cleaned up in the background. A build promoted elsewhere keeps working — promotions share one stored object, which the cleanup respects.

Jobs

GET /jobs/{jobId}

curl -H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" $NU/jobs/01hq2xk8…
{ "data": {
"id": "01hq2xk8vt9r3m4n5p6q7s8t9v",
"type": "build.process",
"status": "completed",
"attempts": 1,
"result": { "build_id": 42, "version": "1.2.0", "channel": "production" },
"error": null,
"status_url": "https://nativeupdatebe.aoneahsan.com/api/public/v1/jobs/01hq2xk8…",
"queued_at": "2026-07-16T09:00:00+00:00",
"finished_at": "2026-07-16T09:00:12+00:00"
} }

A job is visible to any token that manages its app, so rotating a token does not strand an in-flight deploy.

Errors

StatusCodeMeaning
401MISSING_ACCESS_TOKENNo token sent
401INVALID_ACCESS_TOKEN_FORMATNot a nu_pat_… token (an nu_app_… key belongs to the device plane)
401INVALID_ACCESS_TOKEN_CHECKSUMTruncated or mistyped
401ACCESS_TOKEN_NOT_FOUNDUnknown or revoked
401ACCESS_TOKEN_INVALIDExpired
403TOKEN_PERMISSION_DENIEDMissing an opt-in permission — see details.required_permission
404APP_NOT_FOUND · BUILD_NOT_FOUND · JOB_NOT_FOUNDAbsent or outside this token's apps
409VERSION_ALREADY_EXISTSVersion already in that channel
422VERSION_NOT_NEWER · ALREADY_IN_CHANNELSemantically refused
429120/min, or 30 uploads/hour
500UPLOAD_FAILEDThe bundle could not be accepted for processing