API guides
Errors
Every failure returns the same JSON shape. Branch on error.code, a stable machine-readable string; message is for people and may change.
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"code": "invalid_request",
"message": "Request validation failed.",
"details": [{ "path": "title", "code": "too_small" }]
}
}details is present on validation errors: up to 20 entries, each with the field path (dotted, for example tag_ids.3) and a code such as too_small, invalid_format or unrecognized_keys.
Status codes
| Status | Meaning | Common codes |
|---|---|---|
| 400 | The request is malformed or fails validation. | invalid_request, invalid_json, invalid_cursor |
| 401 | No valid credentials. | unauthenticated |
| 403 | Authenticated, but not allowed. | forbidden, insufficient_scope, workspace_not_allowed, session_required, plan_required, owner_required |
| 404 | The resource does not exist in this workspace, or you cannot see it. | not_found |
| 409 | The request conflicts with the current state. | conflict, concurrent_update, idempotency_conflict, slug_taken, plan_limit |
| 413 | The body is too large. | body_too_large |
| 415 | The body is not JSON. | unsupported_media_type |
| 422 | Well-formed, but the change cannot be applied. | invalid_state, unknown_board, invalid_webhook_url, constraint_violation |
| 429 | Too many requests: wait for Retry-After seconds. | rate_limited |
| 500 | An unexpected error on our side. | internal_error |
| 503 | A dependency is unavailable: retry after Retry-After seconds when present. | unavailable, not_ready |
Operations add their own codes where it helps (for example hostname_taken for custom domains); the list above is not exhaustive, so handle unknown codes by status.
What to retry
- 429: always safe to retry after
Retry-Afterseconds; the request did not run. See rate limits. - 503 and network errors: retry with backoff. Retry a POST only with the sameIdempotency-Key, so it cannot apply twice.
- Other 4xx: fix the request; retrying the same call fails the same way.
With the SDK
@upvoiced/sdk throws an UpVoicedError carrying the same fields, and already retries 429, 503 and network errors where that is safe:
import { UpVoiced, UpVoicedError } from '@upvoiced/sdk';
const apiKey = process.env['UPVOICED_API_KEY']; // uv_live_… (Settings → API keys)
const workspace = process.env['UPVOICED_WORKSPACE_ID']; // sent as X-Workspace
if (!apiKey || !workspace) throw new Error('Set UPVOICED_API_KEY and UPVOICED_WORKSPACE_ID.');
const upvoiced = new UpVoiced({ apiKey, workspace });
try {
await upvoiced.post('/v1/posts', {
body: { board_id: '0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d', title: '' },
});
} catch (error) {
if (!(error instanceof UpVoicedError)) throw error;
console.log(error.status); // 400
console.log(error.code); // 'invalid_request'
console.log(error.details); // [{ path: 'title', code: 'too_small' }]
console.log(error.retryAfter); // seconds, on 429 and 503
}