Start a browser direct-to-storage upload
const url = 'https://api.harperharbor.com/open/v1/image/uploadIntent';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"byteSize":1,"contentType":"example"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.harperharbor.com/open/v1/image/uploadIntent \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "byteSize": 1, "contentType": "example" }'Alternative to /image/upload for browsers: the bytes go straight to object storage through a short-lived signed URL instead of through the provider’s API proxy, which cuts an upload off after a fixed time. Checks the per-account item and byte quotas against the declared size, then returns uploadUrl (HTTP PUT the raw file there with Content-Type set to the returned contentType; that header and the exact byteSize are part of the signature, so any other type or size is refused by storage) and uploadId (an opaque, account-bound, expiring credential to pass to /image/uploadComplete). Same 100 MiB per-file limit as /image/upload. Success body is the legacy wrapper {code:0,data:{...}}.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
Exact size of the file in bytes; uploadComplete rejects the upload if the stored object differs.
Media type of the file you will upload, for example image/png, text/javascript or application/json. Defaults to image/png. Image, video, audio, font and WASM uploads are still identified from their bytes at uploadComplete; JavaScript and JSON have no byte signature, so they are accepted only when declared here. video/quicktime is refused (upload MP4).
Examplegenerated
{ "byteSize": 1, "contentType": "example"}Responses
Section titled “Responses”Signed upload target.
object
object
Opaque credential for /image/uploadComplete.
HTTP PUT the raw file body here before expiresAt.
Unix seconds; both the URL and the credential stop working after this.
Per-file limit in bytes.
Send exactly this Content-Type on the PUT (always application/octet-stream); the real type is sniffed from the bytes at uploadComplete.
Example
{ "code": 0}invalid_param (missing or non-positive byteSize, or an unsupported contentType; for video/quicktime the error carries detail.reason quicktime); quota_image_exceeded; quota_bytes_exceeded.
object
Error code (snake_case) or, for legacy handlers, a free-text/localized message.
Human-readable sentence. Present only on middleware errors and /me, /me/wallet.
Whether retrying may succeed. Present only on middleware errors, /me, /me/wallet and service/share routes.
Only on a first-turn context_capacity_exceeded: the smallest context tier that fits the whole card with room to talk. Save it as context in player settings and resend. Absent when no tier fits.
The token budget of required_context_tier for the selected model, for display.
Only on a first-turn context_capacity_exceeded: whether saving trimConstantLore=true lets the card play at the current tier. Absent on mid-conversation capacity errors.
Only on insufficient_credits: the credits this turn needs before it can start. Absent when the refusal came without an estimate.
Only on insufficient_credits: the credits the wallet could set aside for it.
Examplegenerated
{ "error": "example", "message": "example", "retryable": true, "required_context_tier": 1, "required_context_tokens": 1, "constant_lore_trim_fits": true, "credits_needed": 1, "credits_available": 1}unauthorized.
object
Error code (snake_case) or, for legacy handlers, a free-text/localized message.
Human-readable sentence. Present only on middleware errors and /me, /me/wallet.
Whether retrying may succeed. Present only on middleware errors, /me, /me/wallet and service/share routes.
Only on a first-turn context_capacity_exceeded: the smallest context tier that fits the whole card with room to talk. Save it as context in player settings and resend. Absent when no tier fits.
The token budget of required_context_tier for the selected model, for display.
Only on a first-turn context_capacity_exceeded: whether saving trimConstantLore=true lets the card play at the current tier. Absent on mid-conversation capacity errors.
Only on insufficient_credits: the credits this turn needs before it can start. Absent when the refusal came without an estimate.
Only on insufficient_credits: the credits the wallet could set aside for it.
Examplegenerated
{ "error": "example", "message": "example", "retryable": true, "required_context_tier": 1, "required_context_tokens": 1, "constant_lore_trim_fits": true, "credits_needed": 1, "credits_available": 1}file_too_large (over 104857600 bytes).
object
Error code (snake_case) or, for legacy handlers, a free-text/localized message.
Human-readable sentence. Present only on middleware errors and /me, /me/wallet.
Whether retrying may succeed. Present only on middleware errors, /me, /me/wallet and service/share routes.
Only on a first-turn context_capacity_exceeded: the smallest context tier that fits the whole card with room to talk. Save it as context in player settings and resend. Absent when no tier fits.
The token budget of required_context_tier for the selected model, for display.
Only on a first-turn context_capacity_exceeded: whether saving trimConstantLore=true lets the card play at the current tier. Absent on mid-conversation capacity errors.
Only on insufficient_credits: the credits this turn needs before it can start. Absent when the refusal came without an estimate.
Only on insufficient_credits: the credits the wallet could set aside for it.
Examplegenerated
{ "error": "example", "message": "example", "retryable": true, "required_context_tier": 1, "required_context_tokens": 1, "constant_lore_trim_fits": true, "credits_needed": 1, "credits_available": 1}