API conventions¶
How the AppGantry REST API is shaped on the wire, regardless of which route you're calling.
Base URL & versioning¶
All routes mount under /api/v1. Breaking changes go in a new prefix
(/api/v2) running side-by-side rather than mutating v1 in place.
Non-breaking additions land in v1.
Content type¶
JSON on the request and response, with these exceptions:
| Route | Content type |
|---|---|
POST /api/v1/builds |
multipart/form-data (the binary is one of the parts) |
POST /api/v1/developers/{id}/verify_email |
application/json or application/x-www-form-urlencoded |
GET /api/v1/developers/{id}/verify_email |
text/html (verification landing page) |
GET /api/v1/captcha |
text/html |
GET /api/v1/live, GET /api/v1/ready |
JSON {status, [checks]} |
Authentication¶
| Mechanism | Header |
|---|---|
| Access JWT | Authorization: Bearer <jwt> |
| Project access token | Authorization: Bearer ag_prj_... |
| Personal access token | Authorization: Bearer ag_pat_... |
| Browser cookie | Cookie: dfdeveloper=<jwt> (HttpOnly, set at login) |
See Authentication for token lifetimes, refresh behaviour, and scope details.
Success response envelopes¶
Single resource¶
A plain JSON object shaped by the route's response model. Example:
Lists (cursor-paginated)¶
next_cursor is null on the last page. Walk pages by passing
?cursor=<token> until you see null.
Cursor-paginated endpoints:
GET /api/v1/builds→BuildPageGET /api/v1/releases→ReleasePageGET /api/v1/testers→TesterPageGET /api/v1/project-access-tokens→ProjectAccessTokenPageGET /api/v1/pats→PatPage
Endpoints not enveloped because they're domain-bounded:
GET /api/v1/organizations: per-developer, capped at the configured per-developer org limit.GET /api/v1/releases/current/all: bounded by the platform enum.
The audit-feed endpoint (GET /api/v1/audit/organization/{id}) is
day-paginated, not cursor-paginated. See
Audit & spend caps.
Pagination details¶
?limit=<N>: page size. Default 100; hard cap 1000.?cursor=<token>: opaque cursor from a previous response'snext_cursor.- Treat the cursor as a black box. The server may change its encoding between releases.
- A malformed cursor returns the first page rather than an error Note: that conservative fallback means an outdated client cursor won't break pagination, it just resets it.
Error response envelope¶
Every 4xx and 5xx response from the API uses this shape. There are no hand-rolled alternatives:
details is always present (defaulting to {}) so clients can read
the key without conditional handling.
Error codes → HTTP status¶
| Error code | HTTP status |
|---|---|
not_found |
404 |
already_exists, dependency_exists |
409 |
invalid_input, validation_error |
400 |
unauthorized |
401 |
permission_denied, insufficient_role, email_not_verified |
403 |
operation_not_allowed, quota_exceeded |
400 |
rate_limit_exceeded |
429 |
spend_cap_exceeded, prepaid_credit_exhausted |
402 |
payload_too_large |
413 |
storage_backend_unavailable |
503 (+ Retry-After) |
internal_error |
500 |
Error codes are part of the API contract. The string value
("unauthorized", "permission_denied") is the canonical form clients
pattern-match on. Renaming one is a breaking change.
402 on uploads and downloads¶
Upload-initiate and download-URL requests are billing-gated. A request returns 402 with:
error: "prepaid_credit_exhausted": pre-paid Team org with a $0 balance, orerror: "spend_cap_exceeded": post-paid org that hit a configured monthly cap.
Read endpoints (metadata, listings) are never billing-gated.
Each admitted upload/download briefly reserves its projected cost while in flight, so:
- A 402 can appear slightly before your displayed balance reaches zero, because concurrent requests count against it.
- A download URL that's minted but never used reserves credit briefly; that reservation auto-releases within ~30 minutes.
A 402 is safe to retry once the balance is topped up or the next billing period begins. It carries no side effect.
503 on storage-backend blips¶
storage_backend_unavailable (HTTP 503) is the retryable-backend
signal: a transient blob-resolver / audit-feed-read failure carries a
Retry-After header. Back off and retry; the request has no side
effect.
Standard query parameters¶
Multi-tenant routes carry the parent id as a query parameter, not in the path:
?organization_id=<uuid>on every project / build / release / channel / tester / PAT route.?project_id=<uuid>on every channel / build / release / tester route.?channel_id=<uuid>on every release / tester-invite route.
Path parameters carry the resource id of the current operation
({build_id}, {release_id}, {tester_id}).
Idempotency¶
Mutating routes that take an Idempotency-Key header are safe to retry
with the same key. The server returns the original result rather than
performing the action twice. Use a fresh UUID per logical operation in
your client; reusing a key across logically distinct operations is
undefined.
Rate limits¶
Routes carry per-route rate limits. When you hit one you get:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{
"error": "rate_limit_exceeded",
"message": "...",
"details": {"retry_after_seconds": 30}
}
Respect Retry-After. Exponential backoff with jitter is good
practice; sustained rate-limit floods will eventually be account-level
flagged.
Cookies¶
Set on browser login:
| Cookie | Purpose | Scope |
|---|---|---|
dfdeveloper |
Access JWT | Whole API |
dfdeveloper_refresh |
Refresh token | Path-scoped to /api/v1/login |
Both are HttpOnly, SameSite=Lax, and Secure outside local dev.
The refresh cookie's path scope means the browser only sends it to
refresh / logout, not to every API request.
See also¶
- Authentication: login flow, JWT lifetimes, token formats.
- Authorization: role + scope model.
- Glossary: terms used across the API.