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
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-Keyheader (ask_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
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.