Reference
API
Submit natural-language file operations from any backend. Same engine as the web app.
Quick start
Base URL: https://refile.denoiselabs.com/api/v1. All requests are HTTPS; responses are JSON.
# 1. Get an upload URL
curl -X POST https://refile.denoiselabs.com/api/v1/uploads \
-H "Authorization: Bearer rf_live_..."
# → { "uploadUrl": "...", "expiresInSeconds": 1800 }
# 2. Upload your file to that URL
curl -X POST "$UPLOAD_URL" \
-H "Content-Type: image/png" \
--data-binary @photo.png
# → { "storageId": "kg2..." }
# 3. Submit the job (waits up to 25s for short jobs)
curl -X POST 'https://refile.denoiselabs.com/api/v1/jobs?wait=true' \
-H "Authorization: Bearer rf_live_..." \
-H "Content-Type: application/json" \
-d '{
"prompt": "make this black and white",
"files": [{"storageId":"kg2...","filename":"photo.png"}]
}'- Step 1 returns a short-lived signed URL — no auth on the upload itself.
- Step 2 POSTs the raw file bytes; the storage layer responds with a storageId.
- Step 3 creates the job. With ?wait=true the response blocks up to 25s for the finished job; without it, you get status=pending immediately.
Authentication
Send your key as a bearer token on every request: Authorization: Bearer rf_live_.... Keys are server-side credentials — never embed them in browser, mobile, or public code.
Endpoints
POST /api/v1/uploads
Mint a signed upload URL. No body. You then POST the raw file bytes to uploadUrl; that storage call returns a storageId to pass into a job.
// Request
POST /api/v1/uploads
Authorization: Bearer rf_live_...
// Response
{
"uploadUrl": "string", // signed URL — POST your file bytes here
"expiresInSeconds": 1800
}POST /api/v1/jobs
Create a job. Pass ?wait=true to long-poll up to 25 seconds for the finished result; omit it to return immediately. The response body is the job object.
POST /api/v1/jobs?wait=true
Authorization: Bearer rf_live_...
Content-Type: application/json
{
"prompt": "string (required, 1..2000 chars)",
"files": [
{ "storageId": "string", "filename": "string" }
], // optional
"chat_id": "string", // optional — group jobs into a thread
"webhook_url": "https://example.com/refile-events" // optional — POST when job settles
}GET /api/v1/jobs/:id
Fetch the current state of a job. Use this to poll after creating a job without ?wait=true, or to confirm a webhook delivery. Returns the same job object.
The job object
Every job endpoint returns the same shape:
{
"id": "string",
"chat_id": "string | null",
"status": "pending | generating | running | succeeded | failed",
"description": "string | null",
"kind": "command | chat | null",
"message": "string | null", // set when kind == "chat"
"input_files": ["string"],
"outputs": [
{ "storageId": "string", "filename": "string", "url": "string | null" }
],
"pipeline": [ // populated for multi-step jobs
{ "description": "string", "status": "pending | running | completed | failed" }
] | null,
"files_expired": "boolean", // outputs are GC'd after 24h
"created_at": "number", // unix ms
"error": { "code": "string", "message": "string" } | null
}Convex's internal completed state surfaces as succeeded in the API. Output url is a short-lived signed download link — fetch within the 24-hour retention window or files_expired will flip to true.
Errors
Errors are returned as {"error":{"code":"...","message":"..."}} with the matching HTTP status.
Codes marked job error are job-level failures, not request-level: HTTP 200, with status: "failed" and a populated error field on the job object. The request itself was accepted.
| Code | Status | When |
|---|---|---|
| unauthorized | 401 | Missing or invalid API key. |
| invalid_request | 400 | Validation failed on the request body. |
| not_found | 404 | Job id not found, or not yours. |
| quota_exceeded | 402 | Plan limits exceeded. |
| rate_limited | 429 | Too many requests. Retry-After header is set. |
| unprocessable_request | 200 (job error) | Prompt was too complex to plan. |
| no_output | 200 (job error) | Job ran but produced no output file. |
| execution_failed | 200 (job error) | Job ran but the file failed processing. |
| internal_error | 500 | Service problem. Safe to retry. |
Webhooks
- Pass webhook_url on job creation. We POST the full job object to it once the job settles (succeeded or failed).
- Delivery headers: X-Refile-Event: job.settled, X-Refile-Delivery: <uuid>, X-Refile-Signature: sha256=<hex> (HMAC-SHA256 over the raw body).
- Retried up to 3× with 1s / 5s backoff on any non-2xx response or network error.
Content-Type: application/json
X-Refile-Event: job.settled
X-Refile-Delivery: <uuid>
X-Refile-Signature: sha256=<hex> // HMAC-SHA256 over the raw bodyGET /jobs/:id to confirm state.Rate limits & quotas
- 10 requests/sec per key, burst of 20. 429 responses include a Retry-After header in seconds.
- File size, file count, and pipeline-step limits match your plan — identical to the web app. See /docs/limits-and-plans.
- Jobs are async by default. Pass ?wait=true to long-poll for up to 25 seconds; after that, poll GET /api/v1/jobs/:id.
Pricing
Pay-as-you-go, no monthly minimum. Usage is metered per job and billed monthly.
- $0.05 per command job.
- $0.05 per pipeline step (multi-step jobs are billed per step).
See /pricing for the full breakdown and current regional pricing.
What's next
- Create a key at /settings/api.
- See live pricing at /pricing.
- Read the rest of the docs at /docs.