SDK HTTP API¶
The in-app updates SDK is a Swift package, and it is iOS-only. The
HTTP surface it calls is not: /sdk/v1 is seven ordinary
operations, published in the
interactive API reference, that any HTTP client can
call.
This page is for you if you want in-app updates on Android, or in a cross-platform runtime — Kotlin, Flutter, React Native, Unity — or in a build tool. It is also the wire-level description of what the Swift package does, if you are debugging it.
This is the same surface, not a parallel one
There is no separate Android backend and no feature difference.
Android builds are distributed exactly as iOS builds are; only the
install step differs, because Android installs an .apk directly
rather than over the air from a manifest. See
Platform support.
Before you start¶
You need an SDK app: a credential pair bound to one channel. Create
it in the web app — open the channel, then In-app update SDK →
Manage SDK apps → Create SDK app — or over the customer API with
POST /api/v1/sdk-apps.
| Value | Where it comes from | Secret? |
|---|---|---|
app_key |
Returned at creation, and readable afterwards | No — it appears in URLs |
app_secret |
Shown once, at creation | Yes — it entitles an install to enroll |
The secret ships inside your binary, so treat it as recoverable by anyone with your app: its blast radius is deliberately one channel, enrollment only. Rotation means creating a new SDK app and shipping a build, not editing a setting — see Rotating the app secret.
The web app currently says otherwise
The screen that shows you a new app secret advises keeping it out of client code. That advice does not fit this credential: an install has to present the secret to enroll, so an app in the field must carry it. Ship it in the binary and rely on the narrow authority instead. See Security → build artifacts.
The base URL is https://api.appgantry.com/sdk/v1.
The examples are bash
The curl snippets below use $VAR interpolation and \ line
continuation. On Windows PowerShell, substitute your values inline
and use a backtick for continuation, or run them under Git Bash or
WSL. Every request body is also given as literal JSON, so you never
have to reverse-engineer one from a shell quoting trick.
The seven operations¶
| # | Operation | Credential | Purpose |
|---|---|---|---|
| 1 | POST /sdk/v1/apps/{app_key}/enroll |
App secret in the body | Register this install, get an update token |
| 2 | POST /sdk/v1/token/refresh |
Update token | Rotate the update token |
| 3 | GET /sdk/v1/apps/{app_key} |
Update token | App display metadata for your prompt |
| 4 | GET /sdk/v1/apps/{app_key}/updates/latest |
Update token | Is there a newer build? |
| 5 | GET /sdk/v1/releases/{release_id}/download |
Update token, or a capability token | Get the artifact |
| 6 | GET /sdk/v1/releases/{release_id}/manifest.plist |
Capability token | The iOS over-the-air install manifest |
| 7 | POST /sdk/v1/releases/{release_id}/install-reports |
Update token | Report install progress |
An Android client uses 1, 2, 3, 4, 5 and 7. Operation 6 exists only for the iOS over-the-air install, and operation 5 accepts a capability token for the same reason.
Authentication and the token lifecycle¶
/sdk/v1 does not use your developer credentials. Never put a
personal access token, a project access token, or a developer JWT in an
app you ship. The surface has its own credential: a per-install
update token, minted by enrollment.
flowchart TD
A[App launches] --> B{Stored update token?}
B -- no --> C[POST .../enroll with app_secret]
B -- yes --> D[GET .../updates/latest]
C --> E[Store install_id + update_token]
E --> D
D -- 401 --> C
D -- 200 --> F{update_available?}
F -- yes --> G[Download, install, report state]
F -- no --> H[Carry on]
That is the simplest correct client, and it never calls
POST /sdk/v1/token/refresh at all: re-enrolling covers every case a
refresh would. Refresh is there for a longer-lived client
that would rather rotate than re-enroll.
1. Enroll¶
Request body:
curl -sX POST "https://api.appgantry.com/sdk/v1/apps/$APP_KEY/enroll" \
-H 'Content-Type: application/json' \
-d '{"app_secret": "'"$APP_SECRET"'"}'
install_id is the stable identity of this install across token
rotations — keep it if you want to correlate your own telemetry.
update_token is the secret to store and present on every later call.
It is returned exactly once per mint.
Store it in whatever the platform's protected store is: the Android
Keystore-backed EncryptedSharedPreferences, the Keychain on Apple
platforms. Do not log it and do not put it in a crash report.
2. Refresh¶
curl -sX POST "https://api.appgantry.com/sdk/v1/token/refresh" \
-H "Authorization: Bearer $UPDATE_TOKEN"
Returns a fresh SdkInstallToken with the same install_id. Tokens
last 30 days, so an app that is opened regularly can refresh; an app
that has been closed for longer simply enrolls again.
You do not have to refresh. Re-enrolling is always available and costs one extra request. The simplest correct client never calls refresh at all and re-enrolls on any 401.
Every failure is the same 401¶
/sdk/v1 answers a uniform 401 for an unknown app key, a malformed
Authorization header, an expired token, a revoked token, and a token
that belongs to a different app. The cases are deliberately
indistinguishable so the surface cannot be probed for which app keys
exist.
The consequence for your client is simple, and it is the whole error strategy:
On 401: enroll again, retry the call once. If enrollment itself answers 401, stop — the app secret has been revoked, and no retry will fix it.
Do not build a state machine that tries to tell "expired" from "revoked". You cannot, by design.
Checking for an update¶
curl -s "https://api.appgantry.com/sdk/v1/apps/$APP_KEY/updates/latest\
?platform=android¤t_build=41¤t_version=1.4.1" \
-H "Authorization: Bearer $UPDATE_TOKEN"
| Parameter | Required | Notes |
|---|---|---|
platform |
Yes | One of ios, android, tvos, watchos, visionos, wearos |
current_build |
No | Your build number. This is the value the comparison uses |
current_version |
No | Your marketing version string, recorded for diagnostics only |
{
"update_available": true,
"already_current": false,
"is_mandatory": false,
"release_id": "3f0c…",
"latest_version_name": "1.5.0",
"latest_build_number": 44,
"release_notes": "Fixes the crash on cold start.",
"released_at": "2026-08-01T09:00:00Z",
"size_bytes": 41552384,
"platform": "android",
"download_url": "/sdk/v1/releases/3f0c…/download",
"ios_manifest_url": null
}
Three things to get right:
- Send
current_build, and make build numbers monotonic.current_versionis not compared; a client that sends only the marketing version gets told an update is available every time. update_availableandalready_currentcan both befalse. That means no release is published for that platform yet —release_idisnulland there is nothing to install. It is not an error.download_urlandios_manifest_urlare relative. Resolve them against the/sdk/v1host rather than assuming their shape.icon_url, when the metadata call returns one, is absolute and lives on a different host — see App metadata for the prompt.
is_mandatory reports how the release was marked. Whether you make your
prompt non-dismissible is your app's decision; the API only tells you.
App metadata for the prompt¶
GET /sdk/v1/apps/{app_key}?platform=android returns the app's display
name, the current release's version and minimum OS, and an icon_url.
has_release distinguishes "no published release for this platform"
(every latest_* field and icon_url are null) from a real release.
icon_url points outside /sdk/v1. When present it is an absolute
URL on AppGantry's public image CDN: fetch it as an ordinary image
request, without the Authorization header. Icons are extracted
from the build at upload time for Apple and Android artifacts alike, but
extraction doesn't always succeed, so icon_url can be null on a
release that otherwise exists — treat a missing icon as normal and
fall back to your own asset.
Downloading and installing¶
Android¶
curl -L -o app.apk \
"https://api.appgantry.com/sdk/v1/releases/$RELEASE_ID/download" \
-H "Authorization: Bearer $UPDATE_TOKEN"
The route answers 302 to a short-lived signed storage URL; follow
the redirect. Do not send the Authorization header to the storage
URL — the signature is the credential there, and some backends reject a
request carrying both.
Then hand the file to the platform installer. On Android that is an
ACTION_VIEW/package-installer intent over a FileProvider URI, and
your app needs the install-packages permission the user grants once. The
install itself is entirely the OS's business; AppGantry's part ends when
the bytes land.
iOS¶
The over-the-air path uses the manifest:
ios_manifest_urlfrom the update check already carries a short-lived capability token in its query string.- Open
itms-services://?action=download-manifest&url=<that URL>. - The system fetches the manifest with no headers of its own, which is exactly why the URL carries the token instead.
- The manifest points at the download route with a freshly minted
capability token, so the system can fetch the
.ipathe same way.
That is the only reason operations 5 and 6 accept ?token=: the
platform installer cannot send an Authorization header. Use the bearer
token whenever your code is making the request.
Capability tokens are minted by the update check, are bound to one release, and are short-lived. Don't cache them, and don't try to mint one yourself.
Reporting install state¶
curl -sX POST \
"https://api.appgantry.com/sdk/v1/releases/$RELEASE_ID/install-reports" \
-H "Authorization: Bearer $UPDATE_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"state": "installed"}'
Request body:
state |
Report it when |
|---|---|
downloading |
You have started fetching the artifact |
installing |
You have handed the artifact to the platform installer |
installed |
You have confirmed the new build is running |
failed |
Your flow did not get that far — add a short error_message |
Responses are 202 with an empty body. It is fire-and-forget telemetry: a failed report must never fail your update.
Two properties worth knowing:
- The build number is derived server-side from
release_id, so a client cannot misattribute a report to a build it isn't installing. - Reporting is optional. Nothing else depends on it. It is what
populates the install-status view for whoever cut the release, so
reporting
installedis a kindness to your release manager rather than a requirement.
error_message is only meaningful for failed. Keep it short, and keep
personal data out of it — see Privacy.
Writing a client that behaves¶
- Store the update token in protected storage, never in plain preferences and never in a log.
- Re-enroll on 401, retry once, then give up.
- Fail soft. If the check fails, the app carries on. An app that refuses to start because the update service was unreachable is worse than an app one version behind.
- Don't poll on a timer. Check on launch or on foreground. The edge
throttles bursts with a
429; back off rather than retrying tightly. - Ignore unknown response fields.
/sdk/v1changes additively withinv1; shipped apps cannot be patched from the server. See Backend compatibility. - Handle "no release yet". A brand-new channel returns
update_available: falsewith anullrelease_id.
See also¶
- SDK overview: the Swift package, if you are on iOS.
- Tokens and errors: how the Swift package wraps this lifecycle.
- Versioning & compatibility: platform support and app-secret rotation.
- Privacy: what an install report contains.
- API reference: the schema for all seven operations.
- API surface map: where
/sdk/v1sits among the other surfaces.