Tools

SDKs & client snippets

The Salonify API is plain REST over HTTPS with JSON bodies, so any HTTP client works. Below are idiomatic, copy-paste starter clients for Node.js, Python, and PHP that handle authentication, pagination, idempotent creates, and errors the way the API expects.

Official SDKs are on the way

We are working on first-party packages for the most popular languages. Until then, the snippets here are production-ready and the API is stable. Because everything is standard REST with an X-API-Key header, any HTTP client (cURL, Axios, requests, Guzzle, your framework of choice) integrates in minutes. Have a community SDK to share? We are happy to link it.

Before you start

  • Base URL: https://api.salonify.eu/api/v1
  • Authenticate every request with the X-API-Key header (a sk_live_... secret key).
  • Store the key in an environment variable. Never embed it in client-side code or commit it to version control.
  • Most resources are scoped under your business: /businesses/{businessId}/...

Quick start

A minimal authenticated request that lists your services. Pick your language.

// No dependencies needed on Node 18+ (global fetch).
const BASE = "https://api.salonify.eu/api/v1";
const API_KEY = process.env.SALONIFY_API_KEY;
const BUSINESS_ID = process.env.SALONIFY_BUSINESS_ID;

const res = await fetch(`${BASE}/businesses/${BUSINESS_ID}/services`, {
  headers: { "X-API-Key": API_KEY },
});

if (!res.ok) {
  throw new Error(`Salonify ${res.status}: ${await res.text()}`);
}

const { data } = await res.json();
console.log(data);

A tiny client wrapper

Wrapping the base URL, key, and error handling once keeps your call sites clean. The wrappers below cover authentication, JSON encoding, and turning non-2xx responses into typed errors.

import { randomUUID } from "node:crypto";

export class SalonifyError extends Error {
  constructor(status, body) {
    super(body?.message || `Salonify request failed (${status})`);
    this.name = "SalonifyError";
    this.status = status;
    this.code = body?.code;
    this.body = body;
  }
}

export class SalonifyClient {
  constructor({ apiKey, businessId, baseUrl = "https://api.salonify.eu/api/v1" }) {
    this.apiKey = apiKey;
    this.businessId = businessId;
    this.baseUrl = baseUrl;
  }

  async request(method, path, { query, body, idempotencyKey } = {}) {
    const url = new URL(this.baseUrl + path);
    for (const [k, v] of Object.entries(query ?? {})) {
      if (v != null) url.searchParams.set(k, String(v));
    }
    const headers = { "X-API-Key": this.apiKey };
    if (body) headers["Content-Type"] = "application/json";
    if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;

    const res = await fetch(url, {
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    });

    const text = await res.text();
    const json = text ? JSON.parse(text) : null;
    if (!res.ok) throw new SalonifyError(res.status, json);
    return json;
  }

  // Convenience helpers scoped to the key's business.
  biz(path) {
    return `/businesses/${this.businessId}${path}`;
  }

  listServices(query) {
    return this.request("GET", this.biz("/services"), { query });
  }

  createBooking(input) {
    return this.request("POST", this.biz("/bookings"), {
      body: input,
      idempotencyKey: randomUUID(),
    });
  }
}

// Usage
const salonify = new SalonifyClient({
  apiKey: process.env.SALONIFY_API_KEY,
  businessId: process.env.SALONIFY_BUSINESS_ID,
});
const { data } = await salonify.listServices({ limit: 20 });

Paginating a list

List endpoints return a data array plus a meta object with the current page, page size, and total. Walk pages until you have collected everything. See Pagination for the exact envelope.

async function listAllCustomers(salonify) {
  const all = [];
  let page = 1;
  while (true) {
    const { data, meta } = await salonify.request(
      "GET",
      salonify.biz("/customers"),
      { query: { page, limit: 100 } }
    );
    all.push(...data);
    if (page * meta.limit >= meta.total) break;
    page += 1;
  }
  return all;
}

Creating with an idempotency key

Attach a unique Idempotency-Key to create calls so a network retry never produces a duplicate. The wrapper methods above generate one per call; here is the raw form. See Idempotency.

import { randomUUID } from "node:crypto";

const idempotencyKey = randomUUID();

const { data } = await salonify.request("POST", salonify.biz("/bookings"), {
  idempotencyKey,
  body: {
    serviceIds: ["svc_abc"],
    staffId: "stf_123",
    date: "2026-07-01",
    startTime: "10:00",
    customer: { name: "Jordan Lee", email: "[email protected]" },
  },
});
console.log("booking", data.id);

Handling errors and rate limits

Errors arrive as JSON with a stable code. On 429 the response carries a Retry-After header. Back off and retry. See Errors and Rate Limits.

async function withRetry(fn, { retries = 4 } = {}) {
  for (let attempt = 0; ; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const retryable = err.status === 429 || err.status >= 500;
      if (!retryable || attempt >= retries) throw err;
      // Prefer the Retry-After hint when present; else exponential backoff.
      const waitMs = (err.body?.retryAfter ?? 2 ** attempt) * 1000;
      await new Promise((r) => setTimeout(r, waitMs));
    }
  }
}

const services = await withRetry(() => salonify.listServices({ limit: 50 }));

Keep your key server-side

These clients hold a sk_live_... secret key, which grants the scopes you assigned it. Run them on your backend, never in a browser or mobile app. Rotate keys from your dashboard if one is ever exposed. See Authentication.

Prefer a visual reference? Browse every operation in the API Explorer or import the spec into your tooling from Postman & Insomnia.