Skip to content
StippaStippa
FeaturesPricingDemoFAQContact
Log inGet started

Getting started

  • Introduction
  • Authentication
  • Permissions

Core concepts

  • Errors
  • Pagination
  • Idempotency
  • Rate limits

Resources

  • Appointments
  • Customers
  • Services
  • Staff
  • Availability
  • Products
  • Payments
  • Webhooks

Recipes

  • Recipe: book from your own website
  • Recipe: sync the diary into a spreadsheet or BI tool
  • Recipe: a Slack message on every new booking

Reference

  • Changelog
  • OpenAPI 3.1 document

Idempotency

Why creates require an idempotency key, the three outcomes, and how to retry safely.

Last updated 1 August 2026

Creating endpoints require an Idempotency-Key header. Not optional, not ignored if absent: a create without one is a 400 IDEMPOTENCY_KEY_REQUIRED.

curl -X POST https://stippa.nl/api/v1/appointments \
  -H "Authorization: Bearer $STIPPA_KEY" \
  -H "Idempotency-Key: booking-8f42c1a0-2026-08-01" \
  -H "Content-Type: application/json" \
  -d '{ "serviceId": "...", "startTime": "2026-08-14T09:00:00Z", "customer": { "email": "..." } }'

Why it is required rather than offered

A connection can drop after we have created the appointment and before you have read the response. Your client cannot tell that apart from a request that never arrived, so it retries, and the customer now has two appointments and, depending on the business, two confirmation emails and two charges.

Making the header optional means the integrations that most need it are the ones that skip it. The difference between an integrator with retry safety and one who discovers they needed it after double-booking somebody is that we refused the request.

Which endpoints

EndpointHeader
POST /api/v1/appointmentsRequired
POST /api/v1/customersRequired
POST /api/v1/webhooksNot accepted
PATCH on any resourceAccepted and ignored
POST /api/v1/appointments/{id}/statusAccepted and ignored

PATCH and the status transition are naturally idempotent: setting a status to cancelled twice is one cancellation, and the second call is answered by the lifecycle rather than by creating anything.

POST /api/v1/webhooks is the deliberate exception among the creates. Zapier and tools like it send no idempotency header at all, and refusing them would make the REST-hook subscription flow impossible. A uniqueness constraint on the endpoint URL does the same job there instead: registering the same URL twice cannot produce two subscriptions.

The three outcomes

Retrying with the same key produces exactly one of these.

1

Same key, same body, first time: the work happens

A normal 201. Nothing distinguishes it from a create without a key.

2

Same key, same body, already finished: the stored response comes back

The same status and the same body as the original, plus an Idempotency-Replayed: true header. That header is how you tell a replay from a fresh create without diffing ids. This is the case you are protected by.

3

Same key, same body, still running: 409 IDEMPOTENCY_IN_PROGRESS

Two of your retries overlapped. Wait a second and retry the same key; do not mint a new one.

And one outcome that is a bug rather than a retry:

Same key, different body: 409 IDEMPOTENCY_KEY_REUSED. Reporting it is deliberate. Silently replaying the first response would hide a client bug and hand you a record for a request you did not make. The key covers the method and the path as well as the body, so reusing one key across POST /appointments and POST /customers is also a reuse conflict.

⚠️

The body is hashed as raw bytes, not as a canonicalised object. Two JSON documents differing only in key order or whitespace hash differently and count as different bodies. Serialise the retry from the same buffer rather than re-encoding your object.

Picking a key

Up to 255 characters. What makes a good one is that it is stable across your retries and unique across your creates.

Good. A uuid generated once, before the first attempt, and held for the whole retry sequence. Or a deterministic key derived from something in your own system that identifies this create uniquely, such as booking-{yourOrderId}.

Bad. A uuid generated inside the retry loop, which gives every attempt a fresh key and protects nothing. A timestamp, which collides under concurrency and changes between retries. Anything derived from the request body, since a body you corrected between attempts should be a new create and a body you did not correct already hashes the same.

// The key is minted once, outside the loop. That is the whole point.
const idempotencyKey = crypto.randomUUID();

for (let attempt = 0; attempt < 3; attempt++) {
  const response = await fetch('https://stippa.nl/api/v1/appointments', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${key}`,
      'Idempotency-Key': idempotencyKey,
      'Content-Type': 'application/json',
    },
    body,
  });

  if (response.ok) return response.json();
  if (response.status === 409) {
    const { error } = await response.json();
    // Still running: wait for the first attempt rather than starting a second.
    if (error.code === 'IDEMPOTENCY_IN_PROGRESS') {
      await new Promise((r) => setTimeout(r, 1000));
      continue;
    }
  }
  if (response.status < 500) throw new Error(`${response.status} ${await response.text()}`);

  await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
}

Keys are per business

Two businesses can use the same key string without colliding: the record is keyed on the business as well as the key, and the business comes from the credential. You do not need to namespace keys per tenant.

On this page

  • Why it is required rather than offered
  • Which endpoints
  • The three outcomes
  • Picking a key
  • Keys are per business
StippaStippa

Appointment scheduling, without the hassle.

For whom

  • For salons
  • Beauty salons
  • Physiotherapists
  • Coaches
  • Personal trainers

Features

  • Booking widget
  • Online payments
  • Reminders
  • No-show prevention
  • Online calendar
  • Client management

Product

  • Features
  • Pricing
  • FAQ
  • Contact
  • System status

Legal

  • Privacy policy
  • Terms of service
  • Data processing agreement

© 2026 Stippa. All rights reserved.

De Rechter Software · Molenwater 20, 4511 BN Breskens · KvK 98466402 · btw NL005332100B80

······