Hop-State: A_06FPARPGMNVEAN3XBVH0QD8 Hop-Proposal: R_06FPARNXMP3DGP5VQGGX4Z8 Hop-Task: T_06FPAR9KH2V5ED7V65JEN00 Hop-Attempt: AT_06FPAR9KH3NZ1CV8RGPJDSG
7.3 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.
Connect an application
There is no manual application-registration step. On the first connection request, Stuffbox creates or reuses a public identity bound to the submitted exact callback URL and returns its client_id. An app can send that client ID on later requests for the same callback.
A client ID identifies routing and policy state; it is not a secret, and the callback-derived identity 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.
On the first connection, send the exact callback with no identity field:
{
"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 returning app may also send the public client ID Stuffbox returned for that callback:
{
"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"
}
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 later connection requests. If an app moves to a different callback URL, omit the old client ID so Stuffbox can create or reuse the identity for the new exact callback.
When a client ID is sent, the callback must exactly match the URL bound to that identity. HTTPS callbacks are mandatory except loopback http://localhost, 127.0.0.1, and [::1] URLs used for development. URLs cannot contain credentials, query strings, 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.
For compatibility, Stuffbox still accepts the deprecated registration_mode: "self_hosted" field sent by SDK 0.1.0. New integrations should omit it.
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 app's same-origin upload-session proxy; the app 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.