Skip to content

FAQ

Quick developer-focused answers. For sales- and pricing-flavoured questions, see the marketing FAQ.

Builds & uploads

Which platforms are supported?

iOS and Android, both fully supported for uploads, channels, releases, testers, and installs.

  • iOS: signed .ipa. AppGantry generates the install manifest for over-the-air install.
  • Android: .apk, installed directly. .aab App Bundles are a Play Store publishing format rather than a directly installable artifact, and AppGantry rejects them at upload. Upload the .apk — that's the artifact AppGantry distributes to your testers. Publish store releases through the Google Play Console directly, because publishing from AppGantry is not operational.

The separate in-app updates SDK is a Swift package and is iOS-specific today. Distribution and SDK support are different things — the canonical table is Platform support. For in-app updates on Android, call the /sdk/v1 HTTP API yourself; see SDK HTTP API.

Can I publish to the App Store or Google Play from AppGantry?

No — not today. A store connection saves and validates your provider credentials, and registers Apple test devices. A publish request is accepted and creates a job, but every job ends failed and nothing reaches Apple or Google.

That matters because a failed job looks like a store rejection and isn't one. Your credentials are not the problem, and rotating a working key will not help. Ship store releases through App Store Connect or the Google Play Console directly. See Store connections & publishing and Feature availability.

Publishing a build into an AppGantry channel as a release is a different feature that shares the verb, and it works.

How big can a build be?

500 MiB per artifact. If you're regularly above that, contact us.

Oversized uploads fail with HTTP 413 and payload_too_large.

How do I upload a build over the API?

In three steps, not one:

  1. POST /api/v1/builds/initiate with the build's metadata (no size field — you don't declare the size up front).
  2. PUT the bytes to the signed URL you get back, with the x-ms-blob-type: BlockBlob header Azure Blob Storage requires and no AppGantry Authorization header.
  3. POST /api/v1/builds/{pending_upload_id}/complete with the SHA-256.

There is no multipart form upload, and no nested .../projects/{id}/builds endpoint. See API conventions and CI with project access tokens.

Can I retry a failed upload?

Yes. The bytes go straight to storage, so a failed PUT can simply be retried against the same signed URL while it's still valid.

The signed URL is valid for one hour. After that, start again from initiate — with a new build number or version name, because the abandoned pending row goes on claiming the old identity until it is swept, and initiating that same identity answers 409 until then. A project can hold 25 pending uploads at a time, so abandoned attempts eventually need clearing out or completing.

How do I verify a build's checksum?

Do it yourself, on both ends. The SHA-256 you send at upload-complete is recorded as the build's integrity fingerprint, not recomputed from the stored bytes — so AppGantry will not detect a mismatch on your behalf and will not reject the upload for one.

That makes checksum verification a client-side job:

  1. Hash the artifact before uploading, and send that value at upload-complete:

    shasum -a 256 app.ipa | cut -d' ' -f1
    
  2. Keep the value with your CI logs or release notes.

  3. After downloading the build, hash the downloaded file and compare it to the recorded fingerprint. A difference means the copies differ — re-upload from a known-good artifact.

Hashing a different local copy, or hashing after a line-ending-mangling transfer, is the usual cause of a surprise mismatch. See Security and First-build troubleshooting.

Do uploads block other API calls?

No. Bytes stream directly to the storage backend rather than through the API. You can read builds, manage testers, or upload to another project at the same time.

CI & project access tokens

How long does a project access token live?

Until you revoke it, or until the expiry you set at creation. A 90–180 day expiry on CI tokens is a good habit: a forgotten token then dies on its own.

Can one project access token cover multiple projects?

No. Each token is bound to one project (and its organization) at creation. Create one per project your CI pushes to. See Authorization.

What if I commit a token to a repository?

Revoke it immediately from the project's Access tokens page, then create a replacement. The revocation appears in your organization's audit feed.

The ag_prj_ prefix is deliberate: easy to scan for in a repository, easy to spot in logs, and visibly distinct from the ag_pat_ prefix on personal access tokens.

Where do I find my organization ID and project ID?

Org settings → Details → Organization ID and Project settings → Details → Project ID. Both are UUIDs and neither is a secret — store them as ordinary repository variables, not as secrets.

Should CI use a personal access token instead?

No. A personal access token acts as you everywhere your role reaches inside its organization, and it dies when your account does. A project access token is scoped to one project and survives staff changes. Use the project token.

Authentication

How long does an access token last?

10 minutes. Mint a new one with your refresh token via POST /api/v1/login/refresh.

Refresh tokens last 30 days, are single-use, and every refresh returns a replacement. See Authentication.

What if I refresh from two clients at once?

Don't. A refresh token can be used once; presenting an already-used one is treated as evidence of theft, and the whole token family is revoked. Both clients end up signed out.

Refresh from one place and share the resulting access token within your process, rather than having several clients race on the same refresh token.

What happens when I log out?

Ordinary sign-out ends that session. Other devices where you're signed in keep working, because each has its own session and its own refresh token.

To end everything at once, use log out all devices from your account settings. That revokes every session and invalidates outstanding access tokens.

My CI token works but my browser session keeps signing out

They're independent, so that's expected in one direction: signing out of the web app never affects a project access token.

If the browser itself keeps signing out, check your account's session list for revocations you didn't make, and see MFA, passkeys & sessions.

Errors & retries

Is it safe to retry a 5xx?

503 is an explicit "transient, try again" signal — for example storage_backend_unavailable — and may carry Retry-After. Retry it, honouring the header when present.

500 internal_error is not a documented-safe retry. There is no idempotency-key mechanism, so a blind retry of a non-idempotent write may perform the action twice. Retry reads freely, with backoff; for writes, check whether the first attempt landed before retrying. See Idempotency and retries.

I sent a bad field and got a 400. Where are the details?

Under details.errors. A request that fails schema validation answers 400 with the same flat envelope as every other error, with error: "validation_error" and one entry per failing field:

{
  "error": "validation_error",
  "message": "Field required",
  "details": {"errors": [{"type": "missing", "loc": ["body", "version_name"], "msg": "Field required"}]}
}

Each entry carries only type, loc and msg — the value you sent is never echoed back. Fix the request; retrying it unchanged will fail the same way. See Validation failures.

I got a 429. What should I do?

Back off and retry. Rate limits are enforced at the edge and the exact thresholds aren't published, so tune your client to the response rather than to a number: exponential backoff with jitter, honouring Retry-After if it's present. See Rate limits.

What does a 402 mean?

A billing state is blocking the write. The error field says which: spend_cap_exceeded, prepaid_credit_exhausted, billing_suspended, trial_ended, or payment_required.

Three of those — billing_suspended, payment_required, and trial_ended — are organization-level write locks and carry details.lock_reason: suspended, awaiting_payment, and grace respectively. grace means a no-card trial ran out of time, not that a card is currently failing; a payment still being retried doesn't lock the organization at all.

The other two, spend_cap_exceeded and prepaid_credit_exhausted, are per-request gates and carry no lock reason. A trial that burns through its starter credit returns prepaid_credit_exhausted, not trial_ended.

Existing builds stay reachable — a 402 stops new writes, not access to what you already have. The exception is trial_ended, which refuses downloads too until a payment method is added. See Errors, the canonical taxonomy.

A 402 appeared but I thought I had credit left

Concurrent work counts against the same balance, so the displayed figure can lag slightly behind reality. Top up, or raise the spend cap, to clear it. See Billing, usage & caps.

Webhooks

Why isn't my webhook firing?

Check the subscription first. Deliveries only go to subscriptions that list the event type in question and are active, and the subscription must belong to the organization the action happened in.

If there is no delivery record at all for an action you know happened, the subscription wasn't listening for that event, or the delivery could not be enqueued: fan-out is best-effort and never blocks the action that raised it, so the action can succeed with nothing delivered. Confirm the action itself in the audit feed, then look at the subscription.

If there is a delivery record, read its last status code and error — that is your endpoint talking. Use Send test event to prove the endpoint independently. See Webhook events.

Which events can I subscribe to?

All five resource events: release.created, release.deleted, release.rolled_back, build.uploaded, and build.deleted.

webhook.test is the exception — it is delivery-only and a subscription that lists it is rejected. It arrives only when you trigger Send test event on a subscription.

Can I rely on a webhook as the record of what happened?

No. Treat it as a signal. Delivery is at-least-once, unordered, and best-effort: an event you never receive does not mean the action didn't happen. Read the resource the payload names to get authoritative state, and use the audit feed when you need the historical record.

Can I verify a webhook signature?

Yes. X-AppGantry-Signature is sha256=<hex>, an HMAC-SHA256 over "<X-AppGantry-Timestamp>." + <raw body bytes> keyed with the subscription's signing secret. Verify over the raw bytes, compare in constant time, and reject deliveries whose timestamp is outside a tolerance you choose (five minutes is a sane default). The recipe, with a worked example, is in Signature verification.

Verifying proves a delivery came from AppGantry and was not altered. It does not prove the payload is current, so read authoritative state from the API before acting on anything consequential.

I lost my signing secret

Rotate it. Rotate signing secret issues a new one, shows it once, and leaves the subscription, its event selection, and its history intact. The secret is never displayed again after it is issued, so there is nothing to recover. See Webhooks.

In-app updates & the SDK

Is there an Android SDK?

No library, but the API is fully available. The Swift package is iOS-only; the /sdk/v1 HTTP surface it calls is platform-neutral and public — seven operations you can call from Kotlin, Flutter, React Native, Unity, or plain curl.

SDK HTTP API documents all seven: enrollment, token refresh, app metadata, the update check, download, the iOS install manifest, and install reporting.

Which credential does an app in the field use?

A per-install update token, minted by enrolling with your SDK app's app_secret. It is not a personal or project access token — never ship one of those inside an app. Tokens last 30 days; an install that has been idle longer simply enrolls again.

The /sdk/v1 call returned 401 and I can't tell why

By design. An unknown app key, a malformed header, an expired token, a revoked token, and a token belonging to another app all return the same 401, so the surface can't be probed for which app keys exist.

The correct client behaviour is one rule: enroll again and retry once; if enrollment itself returns 401, stop — the app secret has been revoked. See Every failure is the same 401.

Do I have to report installs?

No. Install reports are optional, fire-and-forget (202, empty body), and nothing else depends on them. They populate the install-status view for whoever cut the release, so reporting is a courtesy to your release manager rather than a requirement.

Testers & channels

What's the difference between a channel and a tester group?

A channel is where builds go: you release a build into a channel, and the testers with access to that channel can install it. A tester group is a reusable set of people you grant access with, at organization, project, or channel scope.

Channels answer "what can be installed"; groups answer "by whom". See Channels & releases and Managing testers & groups.

My tester says they can't see the build

Work down the list:

  1. Is there a current release on that channel? A build that was uploaded but never released into a channel is invisible to testers.
  2. Does the tester have access to that channel — directly, or through a group grant?
  3. Is the grant still pending? What that means depends on the level. A channel invitation grants nothing until the tester opens the link and accepts it, signed in as the invited address, within about 10 minutes; if it lapsed, mint a fresh one. An organization or project tester grant has no accept step at all. Which way it went was settled when you added it, by whether any AppGantry account held that address: with no account at all holding it, the grant is pending. An account that has not verified its email still takes the grant on the spot, so that tester is active from the moment you add them, and verifying is only what they need before they can sign in and use it. A grant that really is pending attaches by itself once somebody signs up with the address and verifies it.
  4. iOS only: is their device enrolled? Ad-hoc distribution installs only on registered devices. See Enrolling test devices.

Does adding testers cost more?

No. Pricing is on the bytes you move and keep, not on seats — teammates, reviewers, testers, and CI bots are all free to add. See Plans & pricing.

Can a tester be in more than one channel or project?

Yes. A tester can hold grants at several scopes at once, directly and through groups. The per-tester read on the organization and project tester routes returns the direct grant alongside the groups they belong to, which is the quickest way to answer "why can this person see this?".

Pagination

Why did my pagination restart from the beginning?

Almost certainly a malformed or hand-edited cursor. Cursors are opaque: pass back exactly the next_cursor you were given.

Today a cursor that can't be parsed quietly returns the first page. That's permissive behaviour, not a contract — don't build on it, or a future tightening will turn your infinite loop into an error you didn't plan for. See API conventions.

Regions & residency

Where does hosted storage live?

Hosted storage runs on Azure. If you have a specific residency requirement, ask support for the current answer for your account rather than relying on a docs page that may age.

BYOSA removes the question entirely: the storage account is yours, in whichever region you choose.

Is multi-region active-active supported?

Not on hosted storage. BYOSA puts the redundancy decision in your hands — pick whichever storage redundancy class fits.

BYOSA

How long does BYOSA setup take?

The Azure side takes roughly 15–30 minutes the first time. AppGantry then validates the connection automatically, and final activation is assisted by us, so it isn't instant. See BYOSA setup.

Can I migrate from hosted to BYOSA later?

Yes, but it's coordinated with support rather than self-serve. Existing artifacts live in AppGantry-managed storage, so you'll either have them copied across or accept that older builds stop being downloadable after the switch. Raise it before you have a year of history you care about.

What happens if I revoke AppGantry's access?

AppGantry can no longer read or write your container, so uploads and downloads fail until access is restored. Your artifacts stay exactly where they are — they're in your storage account. See Security & disclosure.

Web app

Why are there two cookies?

The web app uses two: one carrying your short-lived access token, and one carrying your refresh token. The refresh cookie is path-scoped to the login endpoints, so it's sent only on refresh and sign-out rather than on every request — which narrows where it can be captured.

Can I delete my account?

Yes. Account settings → Delete account. Your account is locked out immediately, scheduled for deletion after a grace period, and you get an email with a cancellation link in case you change your mind.

If you're the only administrator of an organization, deletion is blocked until you promote another admin or delete the organization — we don't orphan organizations. Project access tokens belong to the project, not to you, so revoke any that shouldn't outlive you first. See Account & data management.

Can I get a copy of my data?

Yes. Account settings → Download my data prepares an archive and offers it for download until the link expires. You can request a new one once every 24 hours, up to five times in a rolling 30 days, and each ready archive can be downloaded 20 times. See Downloading your data.

I have a question that isn't here

If it's a term rather than a question, check the Glossary.

Email us and we'll add the answer here.

See also