Developer docs · REST API
REST API reference
A complete reference for the SalesThumb REST API. Base URL, authentication, pagination, rate limits, and every CRUD endpoint across customers, vehicles, appointments, quotes, invoices, payments, and webhooks.
Overview
The SalesThumb REST API lets you read and write shop data programmatically — customers, vehicles, appointments, quotes, invoices, payments, and webhook subscriptions. All requests and responses use JSON. The API is versioned; the current stable version is v1.
If you only need to react to events (e.g. a new invoice was paid), prefer webhooks — they require no polling and are available on all plans.
Authentication
All API requests require an API key issued from Settings → API Keys. Pass the key in theX-SalesThumb-Key header. Do not send it as a query parameter or in the request body.
GET /v1/customers HTTP/1.1
Host: api.salesthumb.com
X-SalesThumb-Key: st_live_••••••••••••••••••••••••••••••••
Accept: application/jsonLanguage: httpAPI keys are scoped to a single shop. A key cannot access data belonging to another shop even if you manage multiple shops under the same account. Keys can be restricted to read-only or scoped to specific resource types from the Settings panel.
Base URL & versioning
https://api.salesthumb.com/v1Language: textAll endpoints in this document are relative to that base. Breaking changes ship under a new version prefix (e.g. /v2). Additive changes — new optional fields, new event types, new endpoints — are made to the current version without notice. Pin your code to named fields, not positional assumptions, to stay forward-compatible.
Pagination
All list endpoints use cursor-based pagination. Pass limit (max 100, default 20) and after (the cursor from the previous response) to page forward.
GET /v1/customers?limit=25&after=cur_01234567abcdefLanguage: httpEvery list response includes a pagination object:
{
"data": [ /* array of objects */ ],
"pagination": {
"next_cursor": "cur_abcdef012345",
"has_more": true,
"limit": 25
}
}Language: jsonWhen has_more is false, you have reached the end of the collection. next_cursor is null on the last page.
Rate limiting
The API allows 1,000 requests per minute per shop API key, measured in a rolling 60-second window. Rate limit headers are returned on every response:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 947
X-RateLimit-Reset: 1714140191
Retry-After: 14 # only present on 429 responsesLanguage: httpWhen you exceed the limit the API returns HTTP 429. Implement exponential backoff with jitter and respect the Retry-After value. Do not hammer the API in a tight loop — repeated 429s can result in temporary key suspension.
Idempotency
POST and PATCH endpoints support an optional Idempotency-Key header. Pass a unique string (UUID recommended) to make retries safe — the API will return the original response for duplicate keys received within 24 hours without executing the operation again.
POST /v1/invoices HTTP/1.1
X-SalesThumb-Key: st_live_••••••••••••••••••••••••••••••••
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/jsonLanguage: httpUse a fresh key for each logically distinct operation. Do not reuse the same key for different operations or you will get the cached response from the first one.
Error codes
All error responses follow this shape:
{
"error": {
"code": "not_found",
"message": "Customer 01234567-89ab-cdef-0123-456789abcdef not found.",
"status": 404,
"request_id": "req_abcdef012345"
}
}Language: json| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed JSON or missing required fields. |
| 401 | unauthorized | Missing or invalid API key. Check your X-SalesThumb-Key header. |
| 403 | forbidden | Your key does not have permission to perform this action. |
| 404 | not_found | The requested resource does not exist. |
| 422 | unprocessable_entity | Request was well-formed but failed business validation (e.g. overlapping appointment). |
| 429 | rate_limited | You have exceeded 1,000 requests/minute. Back off and retry after Retry-After seconds. |
| 500 | internal_server_error | Something went wrong on our end. Retrying with exponential backoff is safe. |
Customers
A Customer represents a person or business that has scheduled service, received a quote, or holds an invoice at the shop. Customers can have multiple vehicles, appointments, and invoices over their lifetime.
| Field | Type | Description |
|---|---|---|
| id | string (UUID) | Unique customer identifier. |
| name | string | Full name or business name. |
| string | null | Primary email address. | |
| phone | string | null | Phone number in E.164 format. |
| notes | string | null | Internal shop notes. |
| created_at | ISO 8601 datetime | When the record was created. |
| updated_at | ISO 8601 datetime | Last modification timestamp. |
/v1/customersList all customers for the shop, newest first.
// Response 200
{
"data": [
{
"id": "cus_01234567abcdef",
"name": "Jordan Rivera",
"email": "jordan@example.com",
"phone": "+15125550182",
"notes": null,
"created_at": "2026-03-15T10:22:00.000Z",
"updated_at": "2026-04-01T08:00:00.000Z"
}
],
"pagination": {
"next_cursor": "cur_abcdef012345",
"has_more": true,
"limit": 20
}
}Language: json/v1/customersCreate a new customer.
// Request body
{
"name": "Jordan Rivera",
"email": "jordan@example.com",
"phone": "+15125550182",
"notes": "Referred by Marcus."
}
// Response 201
{
"id": "cus_01234567abcdef",
"name": "Jordan Rivera",
"email": "jordan@example.com",
"phone": "+15125550182",
"notes": "Referred by Marcus.",
"created_at": "2026-05-25T14:00:00.000Z",
"updated_at": "2026-05-25T14:00:00.000Z"
}Language: json/v1/customers/{id}Retrieve a single customer by ID.
// Response 200
{
"id": "cus_01234567abcdef",
"name": "Jordan Rivera",
"email": "jordan@example.com",
"phone": "+15125550182",
"notes": null,
"created_at": "2026-03-15T10:22:00.000Z",
"updated_at": "2026-04-01T08:00:00.000Z"
}Language: json/v1/customers/{id}Update one or more fields on a customer. All fields are optional — only the provided fields are changed.
// Request body (partial update)
{
"phone": "+15125550199",
"notes": "Prefers text over email."
}
// Response 200 — full updated object
{
"id": "cus_01234567abcdef",
"name": "Jordan Rivera",
"email": "jordan@example.com",
"phone": "+15125550199",
"notes": "Prefers text over email.",
"created_at": "2026-03-15T10:22:00.000Z",
"updated_at": "2026-05-25T14:05:00.000Z"
}Language: jsonVehicles
A Vehicle belongs to a customer and carries the make/model/year data needed for service records and warranty registration.
| Field | Type | Description |
|---|---|---|
| id | string (UUID) | Unique vehicle identifier. |
| customer_id | string (UUID) | Owning customer. |
| year | integer | Model year (e.g. 2023). |
| make | string | Manufacturer (e.g. Toyota). |
| model | string | Model name (e.g. Camry). |
| trim | string | null | Trim level (e.g. XSE). |
| color | string | null | Exterior color. |
| vin | string | null | 17-character VIN. |
| license_plate | string | null | Plate number. |
| created_at | ISO 8601 datetime | Creation timestamp. |
/v1/customers/{customerId}/vehiclesList all vehicles belonging to a customer.
// Response 200
{
"data": [
{
"id": "veh_abcdef012345",
"customer_id": "cus_01234567abcdef",
"year": 2022,
"make": "Tesla",
"model": "Model 3",
"trim": "Long Range",
"color": "Pearl White",
"vin": "5YJ3E1EA4NF123456",
"license_plate": "ABC1234",
"created_at": "2026-03-15T10:25:00.000Z"
}
],
"pagination": { "next_cursor": null, "has_more": false, "limit": 20 }
}Language: json/v1/vehiclesCreate a new vehicle and associate it with an existing customer.
// Request body
{
"customer_id": "cus_01234567abcdef",
"year": 2022,
"make": "Tesla",
"model": "Model 3",
"trim": "Long Range",
"color": "Pearl White",
"vin": "5YJ3E1EA4NF123456"
}
// Response 201 — full vehicle objectLanguage: jsonAppointments
Appointments are the primary scheduling unit. Each appointment has a status that progresses through a defined lifecycle: pending → confirmed → checked_in → in_progress → completed. An appointment may also becanceled or no_show.
| Field | Type | Description |
|---|---|---|
| id | string (UUID) | Unique appointment ID. |
| customer_id | string (UUID) | Associated customer. |
| vehicle_id | string (UUID) | null | Associated vehicle, if any. |
| status | enum | pending | confirmed | checked_in | in_progress | completed | canceled | no_show |
| service_type | string | Free-text service label (e.g. 'Ceramic tint — full car'). |
| scheduled_at | ISO 8601 datetime | Appointment start time (UTC). |
| duration_minutes | integer | Estimated service duration in minutes. |
| notes | string | null | Internal notes for the technician. |
| created_at | ISO 8601 datetime | Creation timestamp. |
| updated_at | ISO 8601 datetime | Last updated timestamp. |
/v1/appointmentsList appointments. Supports optional query params: status, customer_id, after (cursor), limit.
GET /v1/appointments?status=confirmed&limit=10Language: http/v1/appointmentsCreate a new appointment.
// Request body
{
"customer_id": "cus_01234567abcdef",
"vehicle_id": "veh_abcdef012345",
"service_type": "Ceramic tint — full car",
"scheduled_at": "2026-06-10T09:00:00.000Z",
"duration_minutes": 240,
"notes": "Customer wants 35% on front, 20% rear."
}
// Response 201
{
"id": "apt_01234567abcdef",
"customer_id": "cus_01234567abcdef",
"vehicle_id": "veh_abcdef012345",
"status": "pending",
"service_type": "Ceramic tint — full car",
"scheduled_at": "2026-06-10T09:00:00.000Z",
"duration_minutes": 240,
"notes": "Customer wants 35% on front, 20% rear.",
"created_at": "2026-05-25T14:10:00.000Z",
"updated_at": "2026-05-25T14:10:00.000Z"
}Language: json/v1/appointments/{id}Update appointment fields. Commonly used to advance status or reschedule.
// Request body — confirm an appointment
{ "status": "confirmed" }
// Response 200 — full updated appointment objectLanguage: json/v1/appointments/{id}Cancel and permanently delete an appointment. Returns 204 No Content on success.
DELETE /v1/appointments/apt_01234567abcdef HTTP/1.1
X-SalesThumb-Key: st_live_••••
// Response 204 No ContentLanguage: httpQuotes
A Quote is an itemized price estimate sent to the customer before work begins. Once accepted and work is done, a quote is converted to an invoice via the convert-to-invoice action.
| Field | Type | Description |
|---|---|---|
| id | string (UUID) | Unique quote ID. |
| customer_id | string (UUID) | Associated customer. |
| vehicle_id | string (UUID) | null | Associated vehicle. |
| status | enum | draft | sent | accepted | declined | expired | converted |
| line_items | array | Array of { description, quantity, unit_price_cents }. |
| subtotal_cents | integer | Sum of line items in US cents. |
| tax_cents | integer | Tax amount in US cents. |
| total_cents | integer | subtotal + tax in US cents. |
| expires_at | ISO 8601 datetime | null | Quote expiry date. |
| created_at | ISO 8601 datetime | Creation timestamp. |
/v1/quotesList quotes. Supports status and customer_id query params.
/v1/quotesCreate a new quote.
// Request body
{
"customer_id": "cus_01234567abcdef",
"vehicle_id": "veh_abcdef012345",
"line_items": [
{
"description": "Ceramic window tint — full car (8 windows)",
"quantity": 1,
"unit_price_cents": 64900
},
{
"description": "Windshield tint — Ceramic IR 70%",
"quantity": 1,
"unit_price_cents": 18000
}
],
"tax_cents": 5040,
"expires_at": "2026-06-25T00:00:00.000Z"
}
// Response 201
{
"id": "quo_01234567abcdef",
"customer_id": "cus_01234567abcdef",
"vehicle_id": "veh_abcdef012345",
"status": "draft",
"line_items": [
{ "description": "Ceramic window tint — full car (8 windows)", "quantity": 1, "unit_price_cents": 64900 },
{ "description": "Windshield tint — Ceramic IR 70%", "quantity": 1, "unit_price_cents": 18000 }
],
"subtotal_cents": 82900,
"tax_cents": 5040,
"total_cents": 87940,
"expires_at": "2026-06-25T00:00:00.000Z",
"created_at": "2026-05-25T14:15:00.000Z"
}Language: json/v1/quotes/{id}Update quote fields, e.g. add a line item or advance status to sent.
/v1/quotes/{id}/convert-to-invoiceConvert an accepted quote into a payable invoice. The quote status becomes converted. Returns the new invoice object.
// No request body required.
// Response 201 — new invoice object
{
"id": "inv_01234567abcdef",
"quote_id": "quo_01234567abcdef",
"customer_id": "cus_01234567abcdef",
"status": "unpaid",
"total_cents": 87940,
"due_at": "2026-06-10T00:00:00.000Z",
"created_at": "2026-05-25T14:20:00.000Z"
}Language: jsonInvoices
Invoices represent money owed by a customer. Invoices may be created directly or generated from a quote. Once fully paid, an invoice moves to paid status automatically.
| Field | Type | Description |
|---|---|---|
| id | string (UUID) | Unique invoice ID. |
| quote_id | string (UUID) | null | Source quote, if converted. |
| customer_id | string (UUID) | Associated customer. |
| status | enum | draft | unpaid | partial | paid | voided |
| line_items | array | Array of { description, quantity, unit_price_cents }. |
| subtotal_cents | integer | Sum of line items. |
| tax_cents | integer | Tax amount. |
| total_cents | integer | Total due. |
| amount_paid_cents | integer | Total payments applied so far. |
| balance_cents | integer | total_cents − amount_paid_cents. |
| due_at | ISO 8601 datetime | null | Payment due date. |
| paid_at | ISO 8601 datetime | null | When fully paid. |
| created_at | ISO 8601 datetime | Creation timestamp. |
/v1/invoicesList invoices. Supports status and customer_id query params.
/v1/invoices/{id}Retrieve a single invoice with its full line items and payment history.
// Response 200
{
"id": "inv_01234567abcdef",
"quote_id": "quo_01234567abcdef",
"customer_id": "cus_01234567abcdef",
"status": "unpaid",
"line_items": [
{ "description": "Ceramic window tint — full car", "quantity": 1, "unit_price_cents": 64900 },
{ "description": "Windshield tint", "quantity": 1, "unit_price_cents": 18000 }
],
"subtotal_cents": 82900,
"tax_cents": 5040,
"total_cents": 87940,
"amount_paid_cents": 0,
"balance_cents": 87940,
"due_at": "2026-06-10T00:00:00.000Z",
"paid_at": null,
"created_at": "2026-05-25T14:20:00.000Z"
}Language: json/v1/invoices/{id}Update mutable fields: due_at, notes, or status (e.g. to void).
Payments
A Payment is a record of money received against an invoice. SalesThumb processes payments through Stripe; recording via the API lets you register payments collected outside the platform (cash, check, external terminal).
| Field | Type | Description |
|---|---|---|
| id | string (UUID) | Unique payment ID. |
| invoice_id | string (UUID) | Invoice this payment applies to. |
| amount_cents | integer | Payment amount in US cents. |
| method | enum | card | cash | check | ach | other |
| reference | string | null | External reference (check number, Stripe charge ID, etc.). |
| note | string | null | Internal note. |
| recorded_at | ISO 8601 datetime | When the payment was made (defaults to now). |
| created_at | ISO 8601 datetime | When the record was created in SalesThumb. |
/v1/paymentsList all payments for the shop. Supports invoice_id filter.
/v1/payments/{invoiceId}/recordRecord an offline payment against an invoice. If the payment brings the balance to zero, the invoice status advances to paid automatically.
// POST /v1/payments/inv_01234567abcdef/record
{
"amount_cents": 87940,
"method": "cash",
"recorded_at": "2026-06-10T11:30:00.000Z",
"note": "Customer paid in full at pickup."
}
// Response 201
{
"id": "pay_01234567abcdef",
"invoice_id": "inv_01234567abcdef",
"amount_cents": 87940,
"method": "cash",
"reference": null,
"note": "Customer paid in full at pickup.",
"recorded_at": "2026-06-10T11:30:00.000Z",
"created_at": "2026-05-25T14:25:00.000Z"
}Language: jsonWebhooks
Manage webhook subscriptions via API or through Settings → Webhooks in the app. For the full webhook event catalog and signature verification guide, see the Webhooks docs.
| Field | Type | Description |
|---|---|---|
| id | string (UUID) | Unique webhook subscription ID. |
| url | string (HTTPS) | Your endpoint URL. Must be HTTPS. |
| label | string | Human-readable name for this subscription. |
| events | string[] | Array of event type strings to subscribe to. Pass ['*'] for all events. |
| status | enum | active | disabled |
| created_at | ISO 8601 datetime | Creation timestamp. |
/v1/webhooksList all webhook subscriptions for the shop.
// Response 200
{
"data": [
{
"id": "whk_01234567abcdef",
"url": "https://your-app.example.com/hooks/salesthumb",
"label": "Production endpoint",
"events": ["invoice.paid", "appointment.created"],
"status": "active",
"created_at": "2026-04-01T08:00:00.000Z"
}
],
"pagination": { "next_cursor": null, "has_more": false, "limit": 20 }
}Language: json/v1/webhooksCreate a new webhook subscription. The signing secret is returned once and never shown again — store it securely.
// Request body
{
"url": "https://your-app.example.com/hooks/salesthumb",
"label": "Production endpoint",
"events": ["invoice.paid", "appointment.created", "customer.created"]
}
// Response 201
{
"id": "whk_01234567abcdef",
"url": "https://your-app.example.com/hooks/salesthumb",
"label": "Production endpoint",
"events": ["invoice.paid", "appointment.created", "customer.created"],
"status": "active",
"signing_secret": "whsec_••••••••••••••••••••••••••••••••",
"created_at": "2026-05-25T14:30:00.000Z"
}Language: jsonsigning_secret field is only returned on the create response. Store it immediately — you cannot retrieve it again. If you lose it, rotate from Settings or DELETE and re-create the subscription./v1/webhooks/{id}Delete a webhook subscription. All future events stop immediately. Returns 204 No Content.
DELETE /v1/webhooks/whk_01234567abcdef HTTP/1.1
X-SalesThumb-Key: st_live_••••
// Response 204 No ContentLanguage: httpNeed an endpoint we don't document yet?
Email info@roffik.com with the use case. We prioritize by real customer demand.
More developer docs