Hop-State: A_06FPAF0M83E7T0JJ7EP2WQ0 Hop-Proposal: R_06FPAF04S0D4G8TA8TNK3C0 Hop-Task: T_06FPADTV2YD4PD5GMTK3150 Hop-Attempt: AT_06FPADTV2ZYKEY8GH3JTK98
7.5 KiB
Stuffbox protocol v1
All control endpoints return JSON. Successful response fields use snake_case; SDK objects use idiomatic TypeScript names. Errors have a stable envelope:
{
"error": {
"code": "quota_exceeded",
"message": "Upload exceeds remaining storage quota",
"details": {}
}
}
Clients must not parse the human message. Branch on error.code. A 429 response includes Retry-After when available. Token responses and canonical redirects use Cache-Control: no-store.
Scopes
assets:read— list the user’s active media library and retrieve usable canonical file URLs.assets:write— create and complete upload sessions.assets:delete— tombstone assets and delete their stored objects.
Register and connect an application
Stuffbox supports two application identities:
- A conventional hosted application is registered once in the Stuffbox developer dashboard. Stuffbox assigns a public
client_idand stores one or more exact callback URLs. - A self-hosted application registers itself during its first connection request. Stuffbox creates or reuses an identity for that exact callback URL and returns its public
client_id; the person running the installation does not visit Stuffbox to register it manually.
A client ID identifies routing and policy state; it is not a secret. Automatic self-hosted registration does not assert ownership of the callback domain. Exact callback binding, PKCE, state verification, user consent, and scoped tokens protect the flow.
The application creates and retains a random PKCE verifier (43–128 RFC 7636 unreserved characters), its S256 challenge, and a random state value.
POST /api/v1/connection-requests
No bearer credential. Rate limited.
A conventional hosted application, or a returning self-hosted installation, sends its persisted client ID:
{
"client_id": "app_public-client-id",
"callback_url": "https://node.example/settings/stuffbox/callback",
"code_challenge": "base64url-sha256-challenge",
"code_challenge_method": "S256",
"scopes": ["assets:read"],
"state": "node-generated-state"
}
A self-hosted installation without a persisted client ID sends:
{
"registration_mode": "self_hosted",
"callback_url": "https://node.example/settings/stuffbox/callback",
"code_challenge": "base64url-sha256-challenge",
"code_challenge_method": "S256",
"scopes": ["assets:read"],
"state": "node-generated-state"
}
These examples request read-only gallery access. Add assets:write or assets:delete only when the application has a feature that needs that permission.
Both forms return:
{
"request_id": "cr_...",
"client_id": "app_public-client-id",
"callback_url": "https://node.example/settings/stuffbox/callback",
"authorization_url": "https://stuffbox.example/connect/cr_...",
"expires_at": "2026-07-15T00:10:00.000Z"
}
Persist the returned client_id and canonical callback_url before redirecting the user's browser to authorization_url. Use that pair for the authorization-code exchange and all later connection requests. If a self-hosted installation moves to a different callback URL, begin a new self-hosted registration for that exact URL.
For a managed application, the callback must exactly match one registered for its client ID. For a self-hosted application, Stuffbox creates or reuses the identity for the submitted exact callback. HTTPS callbacks are mandatory except loopback http://localhost, 127.0.0.1, and [::1] URLs used for development. URLs cannot contain credentials or fragments.
The consent page requires a Stuffbox login and identifies the application by the exact callback domain, alongside every requested permission. Approval redirects only to the stored callback with code and the original state. The application must compare state before exchanging the code.
POST /api/v1/token
Authorization-code exchange:
{
"grant_type": "authorization_code",
"client_id": "app_public-client-id",
"code": "sbc_opaque-code",
"code_verifier": "original-pkce-verifier",
"redirect_uri": "https://node.example/settings/stuffbox/callback"
}
Refresh rotation:
{
"grant_type": "refresh_token",
"refresh_token": "sbr_opaque-refresh-token"
}
The client_id and redirect_uri must be the values persisted from the connection-request response. The response contains access_token, refresh_token, token_type: "Bearer", expires_in, and a space-delimited scope. Replace the old refresh token atomically with the returned one. Reuse of a consumed token revokes the entire grant and returns refresh_token_reuse; the application must reconnect.
POST /api/v1/revoke
{ "token": "sbr_or_sba_value", "token_type_hint": "refresh_token" }
Revokes the token's whole grant. Unknown values return success to avoid becoming a token oracle.
Direct upload
All asset and upload endpoints use Authorization: Bearer {access_token}. The browser should normally call the node's same-origin upload-session proxy; the node calls Stuffbox and returns only the signed upload details.
POST /api/v1/uploads
Requires assets:write and is rate limited.
{
"filename": "photo.png",
"mime_type": "image/png",
"size": 12345,
"sha256": "optional-64-character-lowercase-hex"
}
Stuffbox validates policy and atomically reserves quota, then returns:
{
"upload_id": "upl_...",
"upload_url": "https://object-store.example/signed-put",
"method": "PUT",
"required_headers": {
"content-type": "image/png",
"if-none-match": "*"
},
"expires_at": "2026-07-15T00:10:00.000Z"
}
The browser sends the file body directly to upload_url with exactly the returned method and headers. It must not send the signed URL to logs or analytics.
POST /api/v1/uploads/{uploadId}/complete
Requires assets:write on the grant that created the pending upload. Stuffbox loads the server-selected provider/key, performs HEAD, requires exact size/type, compares a provider checksum when available, and atomically turns reserved bytes into used bytes. A successful response is the asset described below. Completion is idempotent.
Assets
GET /api/v1/assets
Requires assets:read. The library is user-owned rather than grant-owned, so the response includes active media uploaded through any app the same Stuffbox user connected. Query parameters are limit (1–100, default 100) and an opaque cursor returned by the previous page. Results use a stable newest-first order and return { "items": [asset], "next_cursor": "..." } when another page is available. Treat cursors as opaque and omit cursor for the first page.
GET /api/v1/assets/{assetId}
Requires assets:read and predicates the query on the grant owner's user ID. A cross-owner ID is indistinguishable from a missing ID.
DELETE /api/v1/assets/{assetId}
Requires assets:delete. Tombstones the asset and releases used quota in a transaction before deleting the provider object. Canonical delivery stops immediately. Provider deletion is idempotent and retryable.
Asset fields are id, public_id, canonical_url, original_filename, mime_type, byte_size, optional sha256, status, created_at, and optional deleted_at. Internal object keys and provider credentials are absent.
GET or HEAD /f/{publicAssetId}
No bearer credential. Active assets return a temporary 307 redirect to a CDN/provider URL; Stuffbox never proxies the file body. Deleted assets return 410 and unknown IDs return 404.