Idempotency - Spacefast Docs

Idempotency

Send Idempotency-Key on any POST to make it safe to retry, with 24-hour replay, conflict rules, and the anonymous publish principal.

A dropped connection on a publish should not create two versions. After this page you can retry any POST and know exactly which of the two possible answers you will get: the original result replayed, or a conflict telling you the first attempt is still running.

Send a key

curlSDK

curl -sS -X POST https://api.spacefast.com/v1/publish \
  -H "Authorization: Bearer $SPACEFAST_API_KEY" \
  -H "Idempotency-Key: 01J8ZQ7WYX3M4N5P6R7S8T9V0W" \
  -H "Content-Type: application/json" \
  -d '{"spaceId":"spc_4a7f9c2e8b1d0a6f5c3e9d2b7a1f8e4c","files":[{"path":"index.html","size":128}]}'
const receipt = await client.post("/v1/publish", {
  body: { spaceId, files },
  idempotencyKey: "01J8ZQ7WYX3M4N5P6R7S8T9V0W",
});

Every POST honours the header, not only the ones that document it. Generate one key per logical attempt, a UUID or ULID, and reuse it for every retry of that attempt.

Property Value
Header Idempotency-Key
Methods POST only
Maximum length 255 characters
Retention 24 hours
Scope Your tenant, your credential, and the exact METHOD path

What a retry returns

The first attempt finished. The server replays the stored status, headers, and body, and adds Idempotency-Replayed: true. Nothing executes a second time.

The first attempt is still in flight. You get 409 idempotency_conflict_in_progress. This is not a failure. Wait and ask again with the same key until it resolves into the stored response.

You changed the body. You get 409 idempotency_key_reused. One key means one request payload.

What is not stored

Some outcomes deliberately release the key so the next attempt actually re-executes.

Outcome Behaviour
5xx or 429 Not cached. The key is released, so a retry runs again. Caching a 500 would poison the operation for the whole 24-hour window.
Deterministic 4xx Cached and replayed. Sending the same bytes would earn the same rejection anyway.
Response larger than 4 MiB The key is released and the response is returned unstored.

Two header rules on a replay. content-length, transfer-encoding, RateLimit-*, and Retry-After are never replayed, because the live request has real values for those. set-cookie is never stored at all. Stored bodies are encrypted at rest.

Scope, in detail

A key is scoped so two callers cannot collide, and so one caller’s retry always finds its own record:

Because the scope includes METHOD path, the same key value on POST /v1/publish and on a finalize are two independent records.

Anonymous publishing needs a principal

POST /v1/publish accepts no credential at all, which leaves nothing to scope a key by. If you send an Idempotency-Key on a credential-free publish, you must also send:

X-Spacefast-Idempotency-Principal: 3f9a1c7e5b2d8a4f6c0e2b8d1a5f7c3e9b4d6a0f2c8e1b5d7a3f9c4e6b0d2a8f

Exactly 64 hexadecimal characters, stable across every retry of that attempt, and known only to you. Without it the request fails:

{
  "status": 400,
  "code": "invalid_idempotency_key",
  "detail": "Anonymous publish retries require X-Spacefast-Idempotency-Principal with 64 hexadecimal characters."
}

The header is a secret, not an identifier. An anonymous publish receipt contains one-time claim and upload credentials, and the principal is what stops another anonymous caller from replaying your receipt out of a shared scope. Generate 32 random bytes per attempt and hex-encode them.

Where it matters most

Four operations document the header because a duplicate is expensive:

Operation Why
POST /v1/publish A retry without a key can create a second Space or a second version.
POST /v1/spaces/{spaceId}/builds A retry can queue a second build. Required when the build is triggered from managed source.
POST /v1/spaces/{spaceId}/versions/{versionId}/finalize Finalize waits by default, so a client timeout is exactly when you want to retry.
POST /v1/spaces/{spaceId}/versions/{versionId}/promote A retry can move a channel twice.

From the SDK

Passing idempotencyKey also makes the call eligible for the retry policy in the SDK, which otherwise only retries GET, HEAD, and OPTIONS. The transport treats a 409 idempotency_conflict_in_progress answering your own key as “still running, ask again” and keeps polling into the stored response. See the SDK page.

Last updated on September 5, 2026