Idempotency
Make retries safe.
Most Sellvik writes are not yet idempotent across retries. Until they are, the rules below let you build a safe integration without double-charging, double-creating, or double-shipping.
Safe to retry without ceremony
These are idempotent by construction — repeating them produces the same state:
GETof any kind.PATCH /v1/admin/products/{id}(and similar PATCH endpoints) when the body is the full target state.DELETE(already-deleted =404, treat as success).
Not safe to blindly retry
POST /v1/admin/products— creates a new product each time.POST /v1/admin/categories— same.POST /v1/admin/inventory/adjust— each call posts a movement.POST /v1/store/checkout— creates an order each time.POST /v1/store/auth/signup— second call returns409 email_exists, which is at least loud.
For these, you need application-level idempotency until we ship request-level
keys (planned for v1.1; we'll honour Idempotency-Key headers).
Patterns for safe retry today
Use a deterministic slug or SKU
duplicate_slug and similar 409 conflicts are your friend. If your
integration generates products from a stable source (e.g. an ERP), pass the
ERP's product key as slug or sku:
async function upsertProduct(input: Product) {
const res = await sellvik.POST("/api/v1/admin/products", { body: input })
if (res.error?.code === "duplicate_slug") {
// Already exists; PATCH by slug-derived id instead.
return sellvik.PATCH("/api/v1/admin/products/{id}", {
params: { path: { id: await lookupBySlug(input.slug) } },
body: input,
})
}
return res.data
}Check before create
For low-volume sync (e.g. nightly inventory):
const existing = await sellvik.GET("/api/v1/admin/products", {
params: { query: { q: input.sku, limit: 1 } },
})
if (existing.data?.items.length) {
await sellvik.PATCH("/api/v1/admin/products/{id}", { /* … */ })
} else {
await sellvik.POST("/api/v1/admin/products", { /* … */ })
}The race between GET and POST is real but rare for nightly jobs. For
high-frequency writes, lean on 409 instead.
For inventory: pass a reason key
POST /v1/admin/inventory/adjust accepts a reason field. Make it
include a stable external ID:
{
"productId": "…",
"delta": -3,
"reason": "stripe:ch_3MyChargeId fulfillment"
}The movement is still posted twice on a retry — but you can detect and reverse the duplicate from the audit log. Better than nothing until proper idempotency keys land.
What v1.1 will add
We're shipping Idempotency-Key header support on all unsafe POST
endpoints. The contract will be:
- 24-hour replay window.
- Identical key + identical body ⇒ same response (200/201, not a new write).
- Identical key + different body ⇒
409 idempotency_key_mismatch.
Until then, the patterns above are the safe playbook.