Sellvik / developers
Concepts

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:

  • GET of 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 returns 409 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.

On this page