Version

Idempotency

How Subscriby guarantees that retrying a write request never double-creates, double-mutates, or double-charges.

Every POST, PATCH, PUT, and DELETE request against /v1/* requires an Idempotency-Key header. The key makes retries safe: replaying with the same key returns the cached response, while replaying the same key with a different body is rejected.

Header contract

POST /v1/webhook-endpoints HTTP/1.1
Authorization: Bearer sbt_live_...
Idempotency-Key: 0199a4ae-6c02-7b5c-9f51-c5f0da5e2f12
Content-Type: application/json
  • Opaque string, required on every mutation.
  • Any format works — generate a UUID v4 per call.
  • Omit the header and Subscriby returns IDEMPOTENCY_KEY_MISSING (HTTP 400).

Replay semantics

ConditionServer responseReply header
First time this key is seenFresh response, normal HTTP status—
Same key, same request body hashCached response, same HTTP statusIdempotent-Replay: true
Same key, different request body hash409 IDEMPOTENCY_KEY_REUSED—
Same key, concurrent in-flight425 IDEMPOTENCY_REPLAY_IN_PROGRESS—

Successful 2xx JSON responses are cached under sha256(token_id:method:path:key) for 24 hours, with the response body encrypted at rest — the one-time secrets in a token mint or a webhook endpoint registration never sit in the cache in clear text. After that, the same key rotates out of cache and behaves as a fresh request. 4xx and 5xx responses are not cached — callers are free to retry them.

Request body fingerprint

Reuse detection hashes the full raw request body (SHA-256). Two requests are "the same" when their token, method, path, key, and body all match byte-for-byte. That's stricter than Stripe's fingerprinting model and keeps accidental reuse obvious.

Client-side recipe

A safe retry loop:

$key = (string) Str::uuid();

$response = Http::withHeaders([
    'Authorization' => "Bearer {$token}",
    'Idempotency-Key' => $key,
    'Content-Type' => 'application/json',
])->retry(
    times: 3,
    sleepMilliseconds: 500,
    when: fn ($exception, $request) => $exception?->response?->status() >= 500,
)->post('https://api.subscriby.net/v1/webhook-endpoints', [
    'name' => 'Zapier hook',
    'url' => 'https://hooks.zapier.com/...',
    'events' => ['subscription.created'],
]);

If the first attempt succeeds and the second attempt's retry fires anyway (e.g. your client timed out on a 200 it never saw), the second call hits the cache and returns the same response — you never double-register the endpoint.

Cached replays carry Idempotent-Replay: true so your integration can recognise them and skip any client-side side effects that might otherwise fire twice.

What doesn't require a key

  • GET requests — naturally idempotent.
  • Public routes such as /.well-known/ai-plugin.json, /abilities.json, /openapi.json, and /v1/ping.
  • Webhook deliveries from Subscriby → your endpoint (those carry their own event ID for consumer-side de-duplication).

How is this guide?

Version

On this page

Subscriby is a product
designed by you — for you.
No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request