Getting Started
The ServaloDesk API is organized around REST. All requests accept JSON-encoded bodies and return JSON-encoded responses. Authentication is handled via JWT bearer tokens or API keys.
Base URL
https://api.servalodesk.app/v1
Authentication
Pass your token in the Authorization header:
Authorization: Bearer <your_token>
API Keys
Generate API keys from your dashboard under Settings → API. Use API keys for server-to-server integrations. JWT tokens are for client-side use and expire after 1 hour.
Rate Limits
Rate limits are applied per API key per minute. Exceeding the limit returns 429 Too Many Requests.
| Plan | Requests/min | Burst |
| Starter ($29/mo) | 100 | 200 |
| Pro ($49/mo) | 500 | 1,000 |
| Studio ($79/mo) | 2,000 | 4,000 |
Errors
ServaloDesk uses standard HTTP status codes. All errors return a JSON body with error and message fields.
| Code | Meaning |
| 400 | Bad Request — invalid parameters |
| 401 | Unauthorized — missing or invalid token |
| 403 | Forbidden — insufficient permissions |
| 404 | Not Found |
| 409 | Conflict — e.g. scheduling overlap |
| 422 | Unprocessable — validation failed |
| 429 | Rate limit exceeded |
| 500 | Server error |
Webhooks
ServaloDesk sends webhook events to your server for async actions. Configure your endpoint URL in Settings → Webhooks.
Signature verification
Every request includes a X-ServaloDesk-Signature header. Verify it with:
HMAC-SHA256(rawBody, webhookSecret)
Supported events: appointment.confirmed, appointment.cancelled, deposit.paid, deposit.refunded, consent_form.signed, booking_request.received, waitlist.slot_offered
Authentication
Request Body
| Param | Type | Description |
| email | string | Artist email address required |
| password | string | Min 8 characters required |
| full_name | string | Display name required |
| studio_name | string | Creates a new tenant optional |
Response
200 OK
{
"user": {
"id": "usr_9f3e2a1b",
"email": "[email protected]",
"full_name": "Kai Rodriguez",
"role": "owner"
},
"token": "eyJhbGciOiJIUzI1NiJ9...",
"tenant_id": "ten_8d2c1f0e"
}
Request Body
| Param | Type | Description |
| email | string | required |
| password | string | required |
Response
200 OK
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"refresh_token": "rt_a1b2c3d4e5f6",
"expires_in": 3600,
"user": { "id": "usr_9f3e2a1b", "email": "[email protected]" }
}
Request Body
| Param | Type | Description |
| email | string | required |
Response
Studio
Response
200 OK
{
"id": "ten_8d2c1f0e",
"name": "Midnight Ink Studio",
"slug": "midnight-ink",
"plan": "pro",
"trial_ends_at": null,
"artists_count": 3,
"created_at": "2024-01-15T10:00:00Z"
}
Query Params
| Param | Type | Description |
| period | string | week | month | year (default: month) optional |
Response
200 OK
{
"appointments": { "total": 48, "confirmed": 32, "pending": 8, "completed": 6 },
"revenue": { "this_month": 4200, "last_month": 3800, "deposits_pending": 320 },
"clients": { "total": 142, "new_this_month": 12 },
"no_show_rate": 0.04
}
Artists
200 OK
{
"data": [
{
"id": "art_a1b2c3",
"full_name": "Kai Rodriguez",
"role": "owner",
"styles": ["traditional", "neo-traditional", "blackwork"],
"booking_link_slug": "kai-rodriguez",
"is_accepting_bookings": true
}
],
"total": 3
}
Query Params
| Param | Type | Description |
| start_date | string | ISO 8601 date required |
| end_date | string | ISO 8601 date required |
| duration | integer | Minutes needed (default: 120) optional |
Response
200 OK
{
"slots": [
{ "start": "2024-06-28T10:00:00Z", "end": "2024-06-28T12:00:00Z", "available": true },
{ "start": "2024-06-28T13:00:00Z", "end": "2024-06-28T15:00:00Z", "available": true }
]
}
200 OK
{
"display_name": "Kai Rodriguez",
"instagram_handle": "kai.ink",
"styles": ["traditional", "neo-traditional"],
"is_accepting_bookings": true,
"availability_note": "Booking 8 weeks out",
"min_deposit_percent": 30,
"portfolio_images": ["https://cdn.servalodesk.app/..."]
}
Clients
Query Params
| Param | Type | Description |
| q | string | Search by name, email, or phone optional |
| artist_id | string | Filter by preferred artist optional |
| limit | integer | Page size (default: 20, max: 100) optional |
| cursor | string | Pagination cursor optional |
Response
200 OK
{
"data": [
{
"id": "cli_x9y8z7",
"first_name": "Jasmine",
"last_name": "Torres",
"email": "[email protected]",
"phone": "+15125550142",
"total_spent": 847,
"appointment_count": 3,
"last_appointment_at": "2024-05-10T14:00:00Z"
}
],
"next_cursor": "eyJpZCI6ImNsaV94OXk4ejcifQ==",
"has_more": true,
"total": 142
}
Request Body
| Param | Type | Description |
| first_name | string | required |
| last_name | string | required |
| email | string | required |
| phone | string | optional |
| allergies | string | optional |
| medical_notes | string | optional |
| preferred_artist_id | string | optional |
Appointments
Request Body
| Param | Type | Description |
| client_id | string | required |
| artist_id | string | required |
| start_time | datetime | ISO 8601 required |
| duration_minutes | integer | required |
| tattoo_description | string | optional |
| style | string | optional |
| quoted_price | number | In USD optional |
| deposit_amount | number | Overrides % calc optional |
Response
201 Created
{
"id": "apt_d4e5f6a7",
"status": "confirmed",
"start_time": "2024-06-28T14:00:00Z",
"end_time": "2024-06-28T17:00:00Z",
"deposit_amount": 120,
"deposit_paid": false,
"client": { "id": "cli_x9y8z7", "full_name": "Jasmine Torres" },
"artist": { "id": "art_a1b2c3", "full_name": "Kai Rodriguez" }
}
Confirms the appointment and automatically sends a deposit request email to the client. Creates a pending deposit record.
Request Body
| Param | Type | Description |
| send_deposit_request | boolean | Default: true optional |
| deposit_due_date | datetime | Default: 48hrs from now optional |
Response
200 OK
{
"appointment": { "id": "apt_d4e5f6a7", "status": "confirmed" },
"deposit": { "id": "dep_b8c9d0e1", "amount": 120, "status": "pending", "due_date": "2024-06-14T10:00:00Z" },
"notification_sent": true
}
Marks the appointment as no_show and forfeits any paid deposit. Client is notified. An audit log entry is created.
Booking Requests
Request Body
| Param | Type | Description |
| artist_slug | string | required |
| client_name | string | required |
| client_email | string | required |
| client_phone | string | optional |
| tattoo_description | string | required |
| style | string | optional |
| body_placement | string | optional |
| size_estimate | string | small | medium | large | full_piece optional |
| preferred_dates | array | Up to 5 ISO date strings optional |
| budget | string | Budget range optional |
Response
201 Created
{
"id": "req_f1g2h3i4",
"status": "pending",
"message": "Your request has been submitted. Kai will respond within 24-48 hours."
}
Request Body
| Param | Type | Description |
| start_time | datetime | required |
| duration_minutes | integer | required |
| quoted_price | number | optional |
| notes | string | Message to client optional |
Deposits
Request Body
| Param | Type | Description |
| appointment_id | string | required |
| success_url | string | Redirect after payment optional |
| cancel_url | string | Redirect on cancel optional |
Response
200 OK
{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...",
"session_id": "cs_test_a1b2c3d4e5",
"amount": 12000,
"currency": "usd",
"expires_at": "2024-06-14T12:00:00Z"
}
Request Body
| Param | Type | Description |
| reason | string | artist_cancelled | client_request | other optional |
| amount | number | Partial refund amount. Defaults to full. optional |
Response
200 OK
{
"deposit": { "id": "dep_b8c9d0e1", "status": "refunded", "refunded_at": "2024-06-12T09:00:00Z" },
"stripe_refund_id": "re_3NpT8K2eZvKYlo2C1QjEz",
"timeline": "5-10 business days"
}
Consent Forms
Request Body
| Param | Type | Description |
| appointment_id | string | required |
| template_id | string | Defaults to studio default optional |
Response
200 OK
{
"form_id": "frm_j5k6l7m8",
"status": "sent",
"sign_url": "https://servalodesk.app/sign/tok_abc123",
"sent_to": "[email protected]"
}
Request Body
| Param | Type | Description |
| health_answers | object | Key-value health disclosure answers required |
| signature_data | string | Base64 SVG or PNG of signature required |
| agreed_to_terms | boolean | Must be true required |
Waitlist
Request Body
| Param | Type | Description |
| available_date | datetime | required |
| message | string | Custom note to client optional |
| expires_in_hours | integer | Offer expiry (default: 24) optional |
Webhooks
Verified via Stripe-Signature header using your webhook signing secret. Handles:
| Event | Action |
| checkout.session.completed | Mark deposit paid, send confirmation |
| customer.subscription.created | Activate tenant account |
| customer.subscription.updated | Update plan tier |
| customer.subscription.deleted | Downgrade to free / cancel |
| invoice.payment_failed | Mark past-due, trigger dunning email |
| charge.refunded | Update deposit status to refunded |
Changelog
v1.0.0 — June 2024 (Initial Release)
Full CRUD for studios, artists, clients, appointments, deposits, consent forms, and waitlist. Stripe webhook integration. Public booking request endpoint. Magic-link authentication.