API guides
Idempotency
A request whose response was lost may or may not have happened. Send an Idempotency-Key with a POST and you can repeat it safely: UpVoiced applies it once and answers every repeat with the first response.
curl -X POST https://api.upvoiced.com/v1/posts \
-H "Authorization: Bearer $UPVOICED_API_KEY" \
-H "X-Workspace: $UPVOICED_WORKSPACE_ID" \
-H "Idempotency-Key: crm-import-2026-10-22-row-17" \
-H "Content-Type: application/json" \
-d '{"board_id": "0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d", "title": "Dark mode"}'Rules
- The key is 8–128 characters of letters, digits,
_ . : -. A UUID works; a key derived from your own record (an import row, an order id) also survives restarts. - Keys are remembered for 24 hours, separately per workspace, per caller (the person behind the API key, or the end user) and per endpoint.
- A repeat with the same key and the same body returns the stored status and body with the header
Idempotent-Replayed: true. Concurrent repeats wait for the first one. - The same key with a different body is refused with
409 idempotency_conflict: use a new key for a new request. - A request that failed validation stores nothing; fix it and send it again with the same key.
77 of the 88 POST operations accept the header; the reference marks them. GET, PUT and DELETE are idempotent by nature. PATCH is not: do not repeat a PATCH blindly after a timeout, read the resource first.
With the SDK
With retries on (the default), @upvoiced/sdk sends a generated key with every POST and reuses it on each retry. Pass your own key to make a POST safe across process restarts too:
import { UpVoiced } 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 });
// A key derived from your own record makes the POST safe across restarts and re-runs: the API
// answers a repeat with the first response instead of creating a second post.
const { data, status, headers } = await upvoiced.request('post', '/v1/posts', {
body: { board_id: '0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d', title: 'Dark mode' },
idempotencyKey: 'crm-import-2026-10-22-row-17',
});
console.log(status, data.id, headers.get('idempotent-replayed')); // 'true' on a replay