Documentation
Platform
REST resource reference
https://www.servicevin.com/api/v1Account
Confirm which shop a key belongs to.
/api/v1/meIdentify the shop behind a key
Returns the shop the presented key unlocks, and what that key may do. This is the credential test — a 200 means the key is live, a 401 means it is wrong, revoked or expired. It exposes nothing the key holder does not already have, and it is the way to check a key's scope and expiry without opening the dashboard.
curl https://www.servicevin.com/api/v1/me \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"{
"data": {
"shop": {
"id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
"name": "Apex Detailing"
},
"key": {
"scope": "read",
"expires_at": null
}
}
}Response fields
| Field | Type | Description |
|---|---|---|
shop.id | string | The shop's id. |
shop.name | string | The shop's name. |
key.scope | string | What this key may do. read, full |
key.expires_at | string | null | When this key stops working, or null when it never does. Always in the future here — an expired key gets a 401 instead of this response. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
Leads
Read and create. Creating a lead runs the same pipeline as your website form.
/api/v1/leadsList leads
Every lead in the shop, newest first. Deleted leads are never returned. The email, phone and status filters turn this into a lookup — an unparseable phone matches nothing rather than erroring, because a search finding nothing is a valid answer.
curl https://www.servicevin.com/api/v1/[email protected] \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
emailstring | query | Exact match, lower-cased before comparing. |
phonestring | query | Normalized to E.164 before comparing, so any common format works. |
statusstring | query | Only leads at this pipeline status. One of:new, contacted, qualified, quoted, won, lost, cold |
limitinteger | query | Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default. Default:25 |
cursorstring | query | The previous page's |
{
"data": [
{
"id": "6f1c37a2-91b8-4d0e-8a55-2c7e4b1d9f03",
"name": "Jordan Reyes",
"email": "[email protected]",
"phone": "+14035550134",
"source": "google-ads",
"status": "new",
"created_at": "2026-07-16T18:03:11.482Z"
}
],
"meta": {
"next_cursor": null
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the lead. |
name | string | First and last name joined, falling back to the company name, then to |
email | string | null | Lowercased on write. |
phone | string | null | Normalized to E.164 on write. |
source | string | null | Whatever the creator declared. Leads created through this API default to |
status | string | Pipeline status. Every API-created lead starts at new, contacted, qualified, quoted, won, lost, cold |
assigned_to | string | null | The staff member who owns the lead — a user id, which is the |
next_followup_at | string | null | When this lead surfaces for follow-up. A fresh lead is set to now. |
created_at | string | When the lead was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The lead query failed. |
/api/v1/leadsFull-access keyCreate a lead
Creates a lead and fires the same lead.created pipeline as the website form — automations, follow-up agents and webhooks all run on it. There is no dedupe-merge: create means create, and you get the id back to own your own idempotency.
curl -X POST https://www.servicevin.com/api/v1/leads \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Jordan Reyes","email":"[email protected]","phone":"403-555-0134","source":"google-ads","notes":"Wants full-front PPF on a Model Y"}'Body
The lead to create.
| Field | Type | Description |
|---|---|---|
namerequired | stringmax 200 | Full name. Split into first and last on the first whitespace run. |
email | stringmax 200 | Lowercased before storing. |
phone | stringmax 40 | Any common format — normalized to E.164 before storing. |
source | stringmax 120 | Where the lead came from, e.g. api |
notes | stringmax 2000 | Free text. Lands on the lead's timeline as a note. |
{
"data": {
"id": "6f1c37a2-91b8-4d0e-8a55-2c7e4b1d9f03",
"name": "Jordan Reyes",
"email": "[email protected]",
"phone": "+14035550134",
"source": "google-ads",
"status": "new",
"created_at": "2026-07-16T18:03:11.482Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the lead. |
name | string | First and last name joined, falling back to the company name, then to |
email | string | null | Lowercased on write. |
phone | string | null | Normalized to E.164 on write. |
source | string | null | Whatever the creator declared. Leads created through this API default to |
status | string | Pipeline status. Every API-created lead starts at new, contacted, qualified, quoted, won, lost, cold |
assigned_to | string | null | The staff member who owns the lead — a user id, which is the |
next_followup_at | string | null | When this lead surfaces for follow-up. A fresh lead is set to now. |
created_at | string | When the lead was created. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body was not valid JSON. |
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 422 | validation_error | A field failed validation — the message names it, e.g. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The insert failed. |
/api/v1/leads/{id}Get one lead
The same shape the list returns, for one lead.
curl https://www.servicevin.com/api/v1/leads/6f1c37a2-91b8-4d0e-8a55-2c7e4b1d9f03 \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The lead's id. |
{
"data": {
"id": "6f1c37a2-91b8-4d0e-8a55-2c7e4b1d9f03",
"name": "Jordan Reyes",
"email": "[email protected]",
"phone": "+14035550134",
"source": "google-ads",
"status": "contacted",
"assigned_to": null,
"next_followup_at": "2026-07-16T18:03:11.482Z",
"created_at": "2026-07-16T18:03:11.482Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the lead. |
name | string | First and last name joined, falling back to the company name, then to |
email | string | null | Lowercased on write. |
phone | string | null | Normalized to E.164 on write. |
source | string | null | Whatever the creator declared. Leads created through this API default to |
status | string | Pipeline status. Every API-created lead starts at new, contacted, qualified, quoted, won, lost, cold |
assigned_to | string | null | The staff member who owns the lead — a user id, which is the |
next_followup_at | string | null | When this lead surfaces for follow-up. A fresh lead is set to now. |
created_at | string | When the lead was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 404 | not_found | No lead with that id in this shop. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
/api/v1/leads/{id}Full-access keyUpdate a lead
Moves a lead through the pipeline. A real status change appends lead.status_changed exactly as the dashboard does, so automations and webhooks fire — re-sending the status it already has does not, so a retry cannot double-fire anyone's Zap. Merging a lead or converting it to a customer stays in the app.
curl -X PATCH https://www.servicevin.com/api/v1/leads/6f1c37a2-91b8-4d0e-8a55-2c7e4b1d9f03 \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"qualified"}'Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The lead's id. |
Body
At least one field. Anything omitted is left alone.
| Field | Type | Description |
|---|---|---|
status | string | New pipeline status. One of:new, contacted, qualified, quoted, won, lost, cold |
assigned_to | string | null | Owner — the |
next_followup_at | string | null | When the lead next surfaces. Null clears it. |
{
"data": {
"id": "6f1c37a2-91b8-4d0e-8a55-2c7e4b1d9f03",
"name": "Jordan Reyes",
"email": "[email protected]",
"phone": "+14035550134",
"source": "google-ads",
"status": "qualified",
"assigned_to": null,
"next_followup_at": "2026-07-16T18:03:11.482Z",
"created_at": "2026-07-16T18:03:11.482Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the lead. |
name | string | First and last name joined, falling back to the company name, then to |
email | string | null | Lowercased on write. |
phone | string | null | Normalized to E.164 on write. |
source | string | null | Whatever the creator declared. Leads created through this API default to |
status | string | Pipeline status. Every API-created lead starts at new, contacted, qualified, quoted, won, lost, cold |
assigned_to | string | null | The staff member who owns the lead — a user id, which is the |
next_followup_at | string | null | When this lead surfaces for follow-up. A fresh lead is set to now. |
created_at | string | When the lead was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 404 | not_found | No lead with that id in this shop. |
| 422 | validation_error | The body carried no updatable field, or a field failed validation. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
Customers
Read, search by email or phone, create and update.
/api/v1/customersList or find customers
Every customer in the shop, newest first. Add email or phone to turn it into an exact-match lookup — the phone is normalized before matching, so any common format finds the record.
curl https://www.servicevin.com/api/v1/customers \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
emailstring | query | Exact match, case-insensitive. |
phonestring | query | Exact match after E.164 normalization. A number that cannot be normalized returns an empty page rather than an error. |
limitinteger | query | Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default. Default:25 |
cursorstring | query | The previous page's |
{
"data": [
{
"id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"name": "Jordan Reyes",
"email": "[email protected]",
"phone": "+14035550134",
"created_at": "2026-07-16T18:03:11.482Z"
}
],
"meta": {
"next_cursor": null
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the customer. |
name | string | The customer's display name, or |
email | string | null | Lowercased on write. |
phone | string | null | Normalized to E.164 on write. |
city | string | null | City on the customer's address. |
region | string | null | Province or state. |
postal_code | string | null | Postal or ZIP code. |
tags | string[] | Free-form labels the shop applies. Empty array when there are none. |
is_vip | boolean | The shop's own VIP flag. |
created_at | string | When the customer was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The customer query failed. |
/api/v1/customersFull-access keyCreate a customer
Creates a customer. A phone or an email is required — a name alone cannot be stored, because a customer nobody can contact is not a customer. If that contact is already on file you get a 409 naming the existing id, rather than a duplicate that would split their money and message history.
curl -X POST https://www.servicevin.com/api/v1/customers \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Jordan Reyes","email":"[email protected]","phone":"403-555-0134"}'Body
The customer to create. At least one of email or phone is required.
| Field | Type | Description |
|---|---|---|
namerequired | stringmax 200 | Full name. |
email | stringmax 200 | Required unless |
phone | stringmax 40 | Required unless |
consent_source | string | How this contact consented to be messaged. Posting a contact is not itself evidence of consent, so this defaults to unknownOne of: express_written, express_verbal, online_booking, web_form, implied_existing_business, implied_inquiry, imported, unknown |
consent_captured_at | string | When that consent was captured (ISO 8601 with an offset). Dropped when |
consent_note | stringmax 500 | Free-text provenance, e.g. the form name. |
{
"data": {
"id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"name": "Jordan Reyes",
"email": "[email protected]",
"phone": "+14035550134",
"created_at": "2026-07-16T18:03:11.482Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the customer. |
name | string | The customer's display name, or |
email | string | null | Lowercased on write. |
phone | string | null | Normalized to E.164 on write. |
city | string | null | City on the customer's address. |
region | string | null | Province or state. |
postal_code | string | null | Postal or ZIP code. |
tags | string[] | Free-form labels the shop applies. Empty array when there are none. |
is_vip | boolean | The shop's own VIP flag. |
created_at | string | When the customer was created. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body was not valid JSON. |
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 409 | conflict | That email or phone already belongs to a customer. The message carries the existing id. |
| 422 | validation_error | A field failed validation, or neither |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The insert failed. |
/api/v1/customers/{id}Get one customer
The same shape the list returns, for one customer.
curl https://www.servicevin.com/api/v1/customers/b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48 \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The customer's id. |
{
"data": {
"id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"name": "Jordan Reyes",
"email": "[email protected]",
"phone": "+14165550134",
"city": "Toronto",
"region": "ON",
"postal_code": "M5V 1J9",
"tags": [
"ppf"
],
"is_vip": false,
"created_at": "2026-07-16T18:03:11.482Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the customer. |
name | string | The customer's display name, or |
email | string | null | Lowercased on write. |
phone | string | null | Normalized to E.164 on write. |
city | string | null | City on the customer's address. |
region | string | null | Province or state. |
postal_code | string | null | Postal or ZIP code. |
tags | string[] | Free-form labels the shop applies. Empty array when there are none. |
is_vip | boolean | The shop's own VIP flag. |
created_at | string | When the customer was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 404 | not_found | No customer with that id in this shop. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
/api/v1/customers/{id}Full-access keyUpdate a customer
Updates contact details. Two guards apply to the MERGED record rather than the patch: a customer can never be left with neither an email nor a phone, and moving a contact onto a value another customer already holds returns 409 with that id instead of splitting one person's history across two records. Merging customers stays in the app.
curl -X PATCH https://www.servicevin.com/api/v1/customers/b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48 \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tags":["ppf","repeat"],"is_vip":true}'Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The customer's id. |
Body
At least one field. Anything omitted is left alone.
| Field | Type | Description |
|---|---|---|
name | stringmax 200 | Split into first and last. |
email | string | null | Lowercased. Null clears it. |
phone | string | null | Normalized to E.164. Null clears it. |
tags | string[] | Replaces the whole tag list. |
is_vip | boolean | The shop's VIP flag. |
{
"data": {
"id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"name": "Jordan Reyes",
"email": "[email protected]",
"phone": "+14165550134",
"city": "Toronto",
"region": "ON",
"postal_code": "M5V 1J9",
"tags": [
"ppf",
"repeat"
],
"is_vip": true,
"created_at": "2026-07-16T18:03:11.482Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the customer. |
name | string | The customer's display name, or |
email | string | null | Lowercased on write. |
phone | string | null | Normalized to E.164 on write. |
city | string | null | City on the customer's address. |
region | string | null | Province or state. |
postal_code | string | null | Postal or ZIP code. |
tags | string[] | Free-form labels the shop applies. Empty array when there are none. |
is_vip | boolean | The shop's own VIP flag. |
created_at | string | When the customer was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 404 | not_found | No customer with that id in this shop. |
| 409 | conflict | Another customer already holds the email or phone being moved. The message names the id. |
| 422 | validation_error | The body carried no updatable field, the phone was unparseable, or the change would leave the customer with no contact at all. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
Vehicles
Read, search by VIN or plate, and create — with VIN decode included.
/api/v1/vehiclesList vehicles
Vehicles on file, newest first. vin is normalized before matching (so a hyphenated VIN still finds its row) and license_plate matches case-insensitively, because plates are recorded inconsistently.
curl https://www.servicevin.com/api/v1/vehicles \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
customer_idstring | query | Only this customer's vehicles. |
vinstring | query | Exact match after normalizing. |
license_platestring | query | Case-insensitive match. |
limitinteger | query | Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default. Default:25 |
cursorstring | query | The previous page's |
{
"data": [
{
"id": "0d5e8b31-7f24-4a96-b3c8-e2517da9c064",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"label": "2024 BMW M3",
"year": 2024,
"make": "BMW",
"model": "M3",
"trim": "Competition",
"color": "Black",
"vin": "WBS43AY05RFR12345",
"license_plate": "ABC 123",
"mileage": 4200,
"vehicle_type": "car",
"created_at": "2026-07-16T18:03:11.482Z"
}
],
"meta": {
"next_cursor": null
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the vehicle. |
customer_id | string | The vehicle's owner. |
label | string | null | Year, make, model and trim as one string ( |
year | integer | null | Model year. |
make | string | null | Manufacturer. |
model | string | null | Model. |
trim | string | null | Trim level. |
color | string | null | Colour as the shop recorded it. |
vin | string | null | 17-character VIN, upper-cased. |
license_plate | string | null | Plate as recorded. |
mileage | integer | null | Odometer reading when recorded. |
vehicle_type | string | Body category, filled from the VIN decode when one was available. |
created_at | string | When the vehicle was added. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The vehicle query failed. |
/api/v1/vehiclesFull-access keyCreate a vehicle
Adds a vehicle to an existing customer. Sending just customer_id and vin is enough — the VIN is decoded through the same NHTSA path the app's scanner uses, filling year, make, model and trim. Anything you supply explicitly wins over the decode, and an unreachable decoder yields a vehicle carrying the raw VIN rather than a failed request.
curl -X POST https://www.servicevin.com/api/v1/vehicles \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"customer_id":"b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48","vin":"WBS43AY05RFR12345"}'Body
The vehicle. Needs a VIN, or at least a make or model.
| Field | Type | Description |
|---|---|---|
customer_idrequired | string | Must be a customer of this shop. |
vin | stringmax 32 | 17 characters, excluding I, O and Q. |
year | integer | Model year. |
make | stringmax 80 | Overrides the decode. |
model | stringmax 80 | Overrides the decode. |
trim | stringmax 80 | Overrides the decode. |
color | stringmax 40 | Colour. |
license_plate | stringmax 20 | Plate. |
mileage | integer | Odometer reading. |
vehicle_type | string | Overrides the decoded body category. |
{
"data": {
"id": "0d5e8b31-7f24-4a96-b3c8-e2517da9c064",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"label": "2024 BMW M3",
"year": 2024,
"make": "BMW",
"model": "M3",
"trim": "Competition",
"color": null,
"vin": "WBS43AY05RFR12345",
"license_plate": null,
"mileage": null,
"vehicle_type": "car",
"created_at": "2026-07-16T18:03:11.482Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the vehicle. |
customer_id | string | The vehicle's owner. |
label | string | null | Year, make, model and trim as one string ( |
year | integer | null | Model year. |
make | string | null | Manufacturer. |
model | string | null | Model. |
trim | string | null | Trim level. |
color | string | null | Colour as the shop recorded it. |
vin | string | null | 17-character VIN, upper-cased. |
license_plate | string | null | Plate as recorded. |
mileage | integer | null | Odometer reading when recorded. |
vehicle_type | string | Body category, filled from the VIN decode when one was available. |
created_at | string | When the vehicle was added. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 404 | not_found | No customer with that id in this shop. |
| 409 | conflict | That VIN is already on file for this shop. The message names the existing vehicle id. |
| 422 | validation_error | The VIN was malformed, or the body carried neither a VIN nor a make or model. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
Quotes
Read-only. Quotes are built in the app; the API reports them.
/api/v1/quotesList quotes
Quotes for the shop, newest first. Read-only: quote lines, taxes and margins are built in the app, and this endpoint reports the outcome.
curl https://www.servicevin.com/api/v1/quotes?status=accepted \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
statusstring | query | Return only quotes in this status. An unrecognised value is a 422, never a silent full list. One of:draft, sent, viewed, accepted, declined, expired |
limitinteger | query | Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default. Default:25 |
cursorstring | query | The previous page's |
{
"data": [
{
"id": "1c8a5f2e-6b74-4d39-a0e1-58c3f7d2b916",
"number": 1042,
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"status": "accepted",
"total": 2899.5,
"created_at": "2026-07-16T18:03:11.482Z"
}
],
"meta": {
"next_cursor": null
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the quote. |
number | integer | The shop-visible quote number. |
customer_id | string | The customer this quote belongs to. |
status | string | Where the quote stands. One of:draft, sent, viewed, accepted, declined, expired |
total | number | Quote total, tax included, in the shop's currency. |
created_at | string | When the quote was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 422 | validation_error |
|
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The quote query failed. |
Invoices
Read-only. Invoices are issued and collected in the app; the API reports them.
/api/v1/invoicesList invoices
Invoices for the shop, newest first, each with a live balance. Read-only: invoices are minted and collected in the app, and this endpoint reports them. Lists invoices created in Service VIN; a shop's migrated billing history is behind ?imported=true.
curl https://www.servicevin.com/api/v1/invoices?status=open \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
statusstring | query | Return only invoices in this status. An unrecognised value is a 422. One of:draft, open, partial, paid, void, refunded |
importedstring | query | Return only records that arrived through a data import. Default lists only records created in Service VIN. falseOne of: true, false |
limitinteger | query | Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default. Default:25 |
cursorstring | query | The previous page's |
{
"data": [
{
"id": "7e3b9d41-0a52-4c8f-b6d7-91f4a2e5c308",
"number": 587,
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"status": "partial",
"total": 2899.5,
"balance": 1399.5,
"created_at": "2026-07-16T18:03:11.482Z"
}
],
"meta": {
"next_cursor": null
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the invoice. |
number | integer | The shop-visible invoice number. |
customer_id | string | The customer this invoice belongs to. |
status | string | Where the invoice stands. One of:draft, open, partial, paid, void, refunded |
total | number | Invoice total, tax included. |
balance | number | null | Amount still owing. |
created_at | string | When the invoice was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 422 | validation_error |
|
| 422 | validation_error |
|
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The invoice query failed. |
| 500 | internal_error |
|
Jobs
Read, and move a job between stages. Job creation returns 501 on purpose — the reason is spelled out below.
/api/v1/jobsList jobs
Jobs for the shop, newest first. Read-only — see the note under POST /api/v1/jobs. Lists work created in Service VIN; jobs carried in by a data import are behind ?imported=true.
curl https://www.servicevin.com/api/v1/jobs?stage=scheduled \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
stagestring | query | Return only jobs at this stage. An unrecognised value is a 422. One of:lead, scheduled, in_progress, curing, qa, ready, completed, canceled |
customer_idstring | query | Only this customer's jobs — their whole history, newest first. A migrated customer's pre-switch work sits behind |
importedstring | query | Return only records that arrived through a data import. Default lists only records created in Service VIN. falseOne of: true, false |
limitinteger | query | Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default. Default:25 |
cursorstring | query | The previous page's |
{
"data": [
{
"id": "4a92c7f1-8d36-4b05-9e2a-1f6b3d8c50e7",
"number": 317,
"title": "Full-front PPF",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"vehicle_id": "0d5e8b31-7f24-4a96-b3c8-e2517da9c064",
"stage": "scheduled",
"scheduled_start": "2026-07-20T15:00:00.000Z",
"scheduled_end": "2026-07-20T21:00:00.000Z",
"created_at": "2026-07-16T18:03:11.482Z"
}
],
"meta": {
"next_cursor": null
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the job. |
number | integer | The shop-visible job number. |
title | string | null | What the job is, e.g. |
customer_id | string | The customer this job belongs to. |
vehicle_id | string | null | The vehicle being worked on, when one is attached. |
stage | string | Where the job sits on the board. One of:lead, scheduled, in_progress, curing, qa, ready, completed, canceled |
assignee_id | string | null | Assigned staff — a staff profile id, which is the |
scheduled_start | string | null | Booked start, or |
scheduled_end | string | null | Booked end, or |
actual_start | string | null | When work really began — stamped the first time the job reaches |
actual_end | string | null | When work finished. Re-stamped on every entry into |
location_address | string | null | Where the work happens, for mobile jobs. |
created_at | string | When the job was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 422 | validation_error |
|
| 422 | validation_error |
|
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The job query failed. |
/api/v1/jobsFull-access keyNot offered — always 501
Creating a job is not a bare insert here: it allocates a shop sequence number, verifies the customer, vehicle and bay references, respects the bay double-booking rule, and geocodes mobile jobs. An API insert would skip those and create jobs the scheduler cannot trust, so this endpoint says so instead of guessing. Create a lead with POST /api/v1/leads and convert it in the dashboard, or book the job on the calendar.
curl -X POST https://www.servicevin.com/api/v1/jobs \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"{
"error": {
"code": "not_implemented",
"message": "Creating jobs via the API isn't supported yet — book jobs from the Service VIN calendar, or create a lead here and convert it in the app."
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 501 | not_implemented | Every request. |
/api/v1/jobs/{id}Get one job
The same shape the list returns, for one job.
curl https://www.servicevin.com/api/v1/jobs/4a92c7f1-8d36-4b05-9e2a-1f6b3d8c50e7 \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The job's id. |
{
"data": {
"id": "4a92c7f1-8d36-4b05-9e2a-1f6b3d8c50e7",
"number": 317,
"title": "Full-front PPF",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"vehicle_id": "0d5e8b31-7f24-4a96-b3c8-e2517da9c064",
"stage": "in_progress",
"assignee_id": null,
"scheduled_start": "2026-07-16T18:03:11.482Z",
"scheduled_end": null,
"actual_start": "2026-07-16T18:03:11.482Z",
"actual_end": null,
"location_address": null,
"created_at": "2026-07-16T18:03:11.482Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the job. |
number | integer | The shop-visible job number. |
title | string | null | What the job is, e.g. |
customer_id | string | The customer this job belongs to. |
vehicle_id | string | null | The vehicle being worked on, when one is attached. |
stage | string | Where the job sits on the board. One of:lead, scheduled, in_progress, curing, qa, ready, completed, canceled |
assignee_id | string | null | Assigned staff — a staff profile id, which is the |
scheduled_start | string | null | Booked start, or |
scheduled_end | string | null | Booked end, or |
actual_start | string | null | When work really began — stamped the first time the job reaches |
actual_end | string | null | When work finished. Re-stamped on every entry into |
location_address | string | null | Where the work happens, for mobile jobs. |
created_at | string | When the job was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 404 | not_found | No job with that id in this shop. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
/api/v1/jobs/{id}Full-access keyMove a job to another stage
The one job mutation with no scheduling invariant to violate, and it behaves exactly like a drag on the board: actual_start is stamped the first time the job reaches in_progress or beyond and never cleared by a move backwards, actual_end is re-stamped on every entry into completed and cleared when it leaves, and the move goes through the same event rail, so automations, the activity timeline and webhooks all see it. Setting the stage the job already has is a no-op that returns the job, so a retry is safe. If the job moved underneath you, you get a 409 rather than a silent overwrite.
curl -X PATCH https://www.servicevin.com/api/v1/jobs/4a92c7f1-8d36-4b05-9e2a-1f6b3d8c50e7 \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"stage":"completed"}'Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The job's id. |
Body
The stage to move to.
| Field | Type | Description |
|---|---|---|
stagerequired | string | The board stage. One of:lead, scheduled, in_progress, curing, qa, ready, completed, canceled |
{
"data": {
"id": "4a92c7f1-8d36-4b05-9e2a-1f6b3d8c50e7",
"number": 317,
"title": "Full-front PPF",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"vehicle_id": "0d5e8b31-7f24-4a96-b3c8-e2517da9c064",
"stage": "completed",
"assignee_id": null,
"scheduled_start": "2026-07-16T18:03:11.482Z",
"scheduled_end": null,
"actual_start": "2026-07-16T18:03:11.482Z",
"actual_end": "2026-07-16T18:03:11.482Z",
"location_address": null,
"created_at": "2026-07-16T18:03:11.482Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the job. |
number | integer | The shop-visible job number. |
title | string | null | What the job is, e.g. |
customer_id | string | The customer this job belongs to. |
vehicle_id | string | null | The vehicle being worked on, when one is attached. |
stage | string | Where the job sits on the board. One of:lead, scheduled, in_progress, curing, qa, ready, completed, canceled |
assignee_id | string | null | Assigned staff — a staff profile id, which is the |
scheduled_start | string | null | Booked start, or |
scheduled_end | string | null | Booked end, or |
actual_start | string | null | When work really began — stamped the first time the job reaches |
actual_end | string | null | When work finished. Re-stamped on every entry into |
location_address | string | null | Where the work happens, for mobile jobs. |
created_at | string | When the job was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 404 | not_found | No job with that id in this shop. |
| 409 | conflict | The job moved to a different stage while the request was in flight, or the move collided with a bay that is already booked for that time. |
| 422 | validation_error |
|
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
Appointments
Scheduled work in a time window, in diary order, with customer and vehicle inlined.
/api/v1/appointmentsList scheduled work in a time window
Jobs that have a scheduled_start, in chronological order, with the customer and vehicle inlined so a reminder needs one call rather than three. Windowed rather than paginated: the shared cursor walks created_at descending, which is the wrong order for a diary, so narrow from/to instead of paging. Canceled jobs are excluded unless you ask for that stage explicitly, and so are appointments that arrived through a data import — this is a diary of work happening in the shop's bays, and migrated bookings are reached with ?imported=true. meta.truncated is true when the window hit the row ceiling and hid appointments.
curl https://www.servicevin.com/api/v1/appointments?from=2026-08-01T00:00:00Z \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
fromstring | query | ISO 8601 start of the window. Defaults to now. |
tostring | query | ISO 8601 end of the window. Defaults to 30 days after |
stagestring | query | Only this stage. Supplying it also lifts the canceled-jobs exclusion. One of:lead, scheduled, in_progress, curing, qa, ready, completed, canceled |
importedstring | query | Return only records that arrived through a data import. Default lists only records created in Service VIN. falseOne of: true, false |
{
"data": [
{
"id": "4a92c7f1-8d36-4b05-9e2a-1f6b3d8c50e7",
"job_number": 317,
"title": "Full-front PPF",
"stage": "scheduled",
"assignee_id": null,
"scheduled_start": "2026-08-04T16:00:00.000Z",
"scheduled_end": "2026-08-04T20:00:00.000Z",
"location_address": null,
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"customer_name": "Jordan Reyes",
"customer_email": "[email protected]",
"customer_phone": "+14035550134",
"vehicle_id": "0d5e8b31-7f24-4a96-b3c8-e2517da9c064",
"vehicle": "2024 BMW M3",
"created_at": "2026-07-16T18:03:11.482Z"
}
],
"meta": {
"from": "2026-08-01T00:00:00.000Z",
"to": "2026-08-31T00:00:00.000Z",
"truncated": false
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | The job's id. |
job_number | integer | The shop-visible job number. |
title | string | null | What the work is. |
stage | string | Where the job sits on the board. One of:lead, scheduled, in_progress, curing, qa, ready, completed, canceled |
assignee_id | string | null | Assigned staff — a staff profile id ( |
scheduled_start | string | Booked start. |
scheduled_end | string | null | Booked end, when one is set. |
location_address | string | null | Where the work happens, for mobile jobs. |
customer_id | string | The customer. |
customer_name | string | null | Inlined so a reminder needs one call. |
customer_email | string | null | Inlined. |
customer_phone | string | null | Inlined, E.164. |
vehicle_id | string | null | The vehicle, when one is attached. |
vehicle | string | null |
|
created_at | string | When the job was created. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 422 | validation_error |
|
| 422 | validation_error |
|
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The appointment query failed. |
Messages
Read the message log, and text a customer through the shop's compliant send path.
/api/v1/messagesList messages
The shop's message log, newest first — the polling companion to the message.received webhook, and the way to read a thread you were told about.
curl https://www.servicevin.com/api/v1/messages?direction=inbound \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
customer_idstring | query | Only messages matched to this customer. |
conversation_idstring | query | Only messages in this inbox thread. |
directionstring | query |
inbound, outbound |
limitinteger | query | Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default. Default:25 |
cursorstring | query | The previous page's |
{
"data": [
{
"id": "5d1e9a73-2f48-4c60-b915-8e3a7c2d4f06",
"conversation_id": "a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"channel": "sms",
"direction": "inbound",
"body": "Is the car ready for pickup today?",
"from": "+14035550134",
"to": "+14035557890",
"status": "delivered",
"created_at": "2026-07-16T18:03:11.482Z"
}
],
"meta": {
"next_cursor": null
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the message. |
conversation_id | string | The inbox thread this message belongs to. |
customer_id | string | null | The customer, when the thread is matched to one. |
channel | string |
|
direction | string |
inbound, outbound |
body | string | null | The message text. |
from | string | null | Sending address or number. |
to | string | null | Receiving address or number. |
status | string | Delivery status as the provider last reported it. |
created_at | string | When the message was recorded. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 422 | validation_error |
|
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
/api/v1/messagesFull-access keyText a customer
Sends one SMS to a customer of this shop, through the same compliant core the dashboard uses: the shop's messaging switch and Twilio config, a sending number re-proved against the shop's own lines, the opt-out re-check at the send moment, quiet hours, and the inbox thread so the reply lands somewhere. The recipient is always a customer already on file — there is deliberately no raw to parameter, because that would make this endpoint a spam cannon borrowing the shop's A2P registration. Every refusal is a 409 naming the reason, since 'opted out', 'quiet hours' and 'messaging not configured' have different fixes.
curl -X POST https://www.servicevin.com/api/v1/messages \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"customer_id":"b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48","body":"Your car is ready for pickup."}'Body
Who to text, and what to say.
| Field | Type | Description |
|---|---|---|
customer_idrequired | string | Must be a customer of this shop, with a phone on file. |
bodyrequired | stringmax 1600 | The message. 1600 is the carrier ceiling for a concatenated SMS, so longer is refused here rather than by the provider. |
channel | string | Only smsOne of: sms |
{
"data": {
"id": "5d1e9a73-2f48-4c60-b915-8e3a7c2d4f06",
"conversation_id": "a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"channel": "sms",
"direction": "outbound",
"body": "Your car is ready for pickup.",
"from": "+14035557890",
"to": "+14035550134",
"status": "sent",
"created_at": "2026-07-16T18:03:11.482Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the message. |
conversation_id | string | The inbox thread this message belongs to. |
customer_id | string | null | The customer, when the thread is matched to one. |
channel | string |
|
direction | string |
inbound, outbound |
body | string | null | The message text. |
from | string | null | Sending address or number. |
to | string | null | Receiving address or number. |
status | string | Delivery status as the provider last reported it. |
created_at | string | When the message was recorded. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 404 | not_found | No customer with that id in this shop. |
| 409 | conflict | The customer opted out, it is outside the shop's texting hours, or the shop has no working sender. The message says which. |
| 422 | validation_error | The body was empty or too long, or that customer has no phone number on file. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
Conversations
The inbox: read threads, find what is waiting on a reply, and change one thing about a thread per call.
/api/v1/conversationsList conversations
The shop's inbox, most recently active first. ?unread=true is the one to reach for: unread here means the customer's last message landed after our last outbound — the ball is in the shop's court — which is what "what needs answering" actually means. It is derived from two timestamps rather than being a flag somebody set.
status is the shop's disposition rather than the customer's: open is live, closed is dealt with (Quo calls this done), snoozed is parked. A snoozed thread reopens on the next inbound.
curl https://www.servicevin.com/api/v1/conversations?status=open \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
statusstring | query | Only threads at this disposition. One of:open, snoozed, closed |
channelstring | query | Only threads on this channel. One of:sms, email, call, webchat, whatsapp |
customer_idstring | query | Only this customer's threads. |
assigned_tostring | query | Only threads owned by this teammate. |
unassignedstring | query |
true |
unreadstring | query |
true |
limitinteger | query | How many to return, 1-100. Default:25 |
offsetinteger | query | How many to skip. Pass 0 |
{
"data": [
{
"id": "a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"channel": "sms",
"status": "open",
"subject": null,
"contact_number": "+14035550134",
"assigned_to": null,
"pinned": false,
"unread": true,
"is_ai_managed": false,
"last_message_at": "2026-07-16T18:03:11.482Z",
"last_inbound_at": "2026-07-16T18:03:11.482Z",
"last_outbound_at": null,
"last_human_outbound_at": null,
"created_at": "2026-07-16T18:03:11.482Z"
}
],
"meta": {
"total": 12,
"omitted": 11,
"next_offset": 1
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the thread. |
customer_id | string | null | The customer, when the thread is matched to one. |
channel | string | What the thread runs on. One of:sms, email, call, webchat, whatsapp |
status | string | The shop's disposition. open, snoozed, closed |
subject | string | null | Subject line, on an email thread. |
contact_number | string | null | The number on the other end, when the thread is a text thread. |
assigned_to | string | null | The teammate who owns it, or null when nobody has taken it. |
pinned | boolean | Pinned to the top of the shop's inbox. |
unread | boolean | The customer's last message landed after our last outbound — i.e. the ball is in the shop's court. Derived from the two timestamps, not a flag somebody set. |
is_ai_managed | boolean | Service VIN's own agent is answering this thread. |
last_message_at | string | null | When anything last happened on it. |
last_inbound_at | string | null | When the customer last wrote. |
last_outbound_at | string | null | When the shop last wrote — including anything sent automatically. |
last_human_outbound_at | string | null | When a PERSON at the shop last replied. Automated sends — an after-hours acknowledgement, a missed-call text-back, an automation rule — move |
created_at | string | When it opened. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 422 | validation_error |
|
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
/api/v1/conversations/{id}Get a conversation
One thread, plus the two things a reply depends on: who the contact is, and whether the shop is allowed to text them.
Read texting_blocked BEFORE composing anything. True means a STOP or a staff block is on file, and the send endpoint will refuse — correctly. marketing_consent is a different question with a different answer: it governs campaigns, not replies, and a customer who consented to nothing may still be answered when they wrote in first.
The messages themselves come from GET /api/v1/messages?conversation_id=….
curl https://www.servicevin.com/api/v1/conversations/{id} \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The thread's id. |
{
"data": {
"conversation": {
"id": "a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"channel": "sms",
"status": "open",
"subject": null,
"contact_number": "+14035550134",
"assigned_to": null,
"pinned": false,
"unread": true,
"is_ai_managed": false,
"last_message_at": "2026-07-16T18:03:11.482Z",
"last_inbound_at": "2026-07-16T18:03:11.482Z",
"last_outbound_at": null,
"last_human_outbound_at": null,
"created_at": "2026-07-16T18:03:11.482Z"
},
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"customer_name": "Ben Carter",
"customer_phone": "+14035550134",
"customer_email": "[email protected]",
"texting_blocked": false,
"marketing_consent": true
}
}Response fields
| Field | Type | Description |
|---|---|---|
conversation | object | The thread record — the same fields the list returns. |
customer_id | string | null | The contact behind the thread. |
customer_name | string | null | Their display name. |
customer_phone | string | null | Their number. |
customer_email | string | null | Their address. |
texting_blocked | boolean | A STOP or a staff block is on file. Sending will be refused; do not draft a text, and do not retry the refusal. |
marketing_consent | boolean | Whether campaigns may reach them. Not the same question as replying. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 404 | not_found | No thread with that id in this shop. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
/api/v1/conversations/{id}Full-access keyChange one thing about a conversation
A VERB CALL, NOT A PATCH, and deliberately so. Send exactly ONE of mark_read, mark_unread, mark_done, mark_open, snooze, pin, unpin, staff_id (hand it to that teammate) or unassign. Sending none is refused, and so is sending two — one call that both closed a thread and reassigned it would be a single audit row covering two decisions nobody could separate afterwards.
The MESSAGES are not touchable from here or anywhere: a delivered text is history, and no endpoint on this API edits or deletes one.
Assigning PINGS somebody, so assign when a person is meant to act. The target must be a teammate whose role grants inbox access — anyone else is refused rather than silently parked, because a thread assigned to someone who cannot open the inbox is work that never happens.
curl -X POST https://www.servicevin.com/api/v1/conversations/{id} \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mark_done":true}'Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The thread's id. |
Body
Exactly one verb.
| Field | Type | Description |
|---|---|---|
mark_read | boolean | Mark the thread read. |
mark_unread | boolean | Put it back to unread. |
mark_done | boolean | Close it (Quo calls this done). |
mark_open | boolean | Reopen a closed or snoozed thread. |
snooze | boolean | Park it until the next inbound. |
pin | boolean | Pin it to the top of the inbox. |
unpin | boolean | Unpin it. |
staff_id | string | Hand it to this teammate. They are notified. |
unassign | boolean | Take it off whoever holds it. |
{
"data": {
"id": "a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"channel": "sms",
"status": "closed",
"subject": null,
"contact_number": "+14035550134",
"assigned_to": null,
"pinned": false,
"unread": false,
"is_ai_managed": false,
"last_message_at": "2026-07-16T18:03:11.482Z",
"last_inbound_at": "2026-07-16T18:03:11.482Z",
"last_outbound_at": "2026-07-16T18:03:11.482Z",
"last_human_outbound_at": "2026-07-16T18:03:11.482Z",
"created_at": "2026-07-16T18:03:11.482Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the thread. |
customer_id | string | null | The customer, when the thread is matched to one. |
channel | string | What the thread runs on. One of:sms, email, call, webchat, whatsapp |
status | string | The shop's disposition. open, snoozed, closed |
subject | string | null | Subject line, on an email thread. |
contact_number | string | null | The number on the other end, when the thread is a text thread. |
assigned_to | string | null | The teammate who owns it, or null when nobody has taken it. |
pinned | boolean | Pinned to the top of the shop's inbox. |
unread | boolean | The customer's last message landed after our last outbound — i.e. the ball is in the shop's court. Derived from the two timestamps, not a flag somebody set. |
is_ai_managed | boolean | Service VIN's own agent is answering this thread. |
last_message_at | string | null | When anything last happened on it. |
last_inbound_at | string | null | When the customer last wrote. |
last_outbound_at | string | null | When the shop last wrote — including anything sent automatically. |
last_human_outbound_at | string | null | When a PERSON at the shop last replied. Automated sends — an after-hours acknowledgement, a missed-call text-back, an automation rule — move |
created_at | string | When it opened. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body was not JSON. |
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 404 | not_found | No thread with that id in this shop. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | No verb was given, more than one was, or the named teammate's role does not grant inbox access. |
Calls
The phone log, plus signed short-lived links to recordings and voicemail, and the transcripts. Read-only — a recorded call is history.
/api/v1/callsList calls
The shop's phone log, newest first. Service VIN records, transcribes and summarizes every call; this is the record of what happened, not the audio and not the words. has_recording, has_voicemail, has_transcript and has_ai_summary say which artefacts exist, and each is fetched from its own endpoint — so a key that reads the log does not thereby hold every customer's recorded voice.
Pages by offset rather than by the cursor the older resources use. meta.total is the exact number of calls matching the filter and meta.omitted how many are not in this page: answer "how many did we miss" from total, never from the length of data.
curl https://www.servicevin.com/api/v1/calls?direction=inbound \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
directionstring | query |
inbound, outbound |
statusstring | query | How the call ended. ringing, in_progress, completed, missed, failed, canceled |
customer_idstring | query | Only this customer's calls. |
sincestring | query | ISO instant. Only calls that started at or after it. |
untilstring | query | ISO instant. Only calls that started before it. |
limitinteger | query | How many to return, 1-100. Default:25 |
offsetinteger | query | How many to skip. Pass 0 |
{
"data": [
{
"id": "8c14f0b7-3d95-42ae-9b6c-0e7a5d3f21b8",
"conversation": "a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"direction": "inbound",
"status": "missed",
"from": "+14035550134",
"to": "+14035557890",
"started_at": "2026-07-16T18:03:11.482Z",
"ended_at": "2026-07-16T18:03:11.482Z",
"duration_seconds": 24,
"answered_by": null,
"has_recording": false,
"recording_duration_seconds": null,
"has_voicemail": true,
"voicemail_duration_seconds": 17,
"has_transcript": true,
"has_ai_summary": false
}
],
"meta": {
"total": 41,
"omitted": 40,
"next_offset": 1
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the call. |
conversation | string | null | The inbox thread this call was filed into. Null when the number was never matched to a contact. |
customer_id | string | null | The customer, when the caller is on file. |
direction | string |
inbound, outbound |
status | string | How it ended. ringing, in_progress, completed, missed, failed, canceled |
from | string | The calling number, E.164. |
to | string | The number called, E.164. |
started_at | string | When it started. |
ended_at | string | null | When it ended. Null while the call is still up. |
duration_seconds | integer | null | The whole call, ringing included. |
answered_by | string | null | Who or what picked it up — a member of staff, or the AI receptionist. |
has_recording | boolean | Audio of the conversation exists. Fetch a signed link from GET /api/v1/calls/{id}/recording. |
recording_duration_seconds | integer | null | The recorded part only, which starts when somebody answers — shorter than |
has_voicemail | boolean | The caller left a message. Fetch it from the same endpoint with |
voicemail_duration_seconds | integer | null | How long the voicemail runs. |
has_transcript | boolean | The words are available from GET /api/v1/calls/{id}/transcript. |
has_ai_summary | boolean | Service VIN has analysed this call and produced a recap. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 422 | validation_error |
|
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
/api/v1/calls/{id}Get a call
One call's record. There is no PUT, PATCH or DELETE on this path and there will not be: a recorded call is history, and the same is true of every message on this API.
duration_seconds is the whole call; recording_duration_seconds is only the part after somebody answered. They differ on a call that rang first, and reporting one as the other overstates talk time.
curl https://www.servicevin.com/api/v1/calls/{id} \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The call's id. |
{
"data": {
"id": "8c14f0b7-3d95-42ae-9b6c-0e7a5d3f21b8",
"conversation": "a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"direction": "inbound",
"status": "completed",
"from": "+14035550134",
"to": "+14035557890",
"started_at": "2026-07-16T18:03:11.482Z",
"ended_at": "2026-07-16T18:03:11.482Z",
"duration_seconds": 214,
"answered_by": "Front desk",
"has_recording": true,
"recording_duration_seconds": 196,
"has_voicemail": false,
"voicemail_duration_seconds": null,
"has_transcript": true,
"has_ai_summary": true
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the call. |
conversation | string | null | The inbox thread this call was filed into. Null when the number was never matched to a contact. |
customer_id | string | null | The customer, when the caller is on file. |
direction | string |
inbound, outbound |
status | string | How it ended. ringing, in_progress, completed, missed, failed, canceled |
from | string | The calling number, E.164. |
to | string | The number called, E.164. |
started_at | string | When it started. |
ended_at | string | null | When it ended. Null while the call is still up. |
duration_seconds | integer | null | The whole call, ringing included. |
answered_by | string | null | Who or what picked it up — a member of staff, or the AI receptionist. |
has_recording | boolean | Audio of the conversation exists. Fetch a signed link from GET /api/v1/calls/{id}/recording. |
recording_duration_seconds | integer | null | The recorded part only, which starts when somebody answers — shorter than |
has_voicemail | boolean | The caller left a message. Fetch it from the same endpoint with |
voicemail_duration_seconds | integer | null | How long the voicemail runs. |
has_transcript | boolean | The words are available from GET /api/v1/calls/{id}/transcript. |
has_ai_summary | boolean | Service VIN has analysed this call and produced a recap. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 404 | not_found | No call with that id in this shop. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
/api/v1/calls/{id}/recordingGet a signed link to a call recording
Returns a SHORT-LIVED SIGNED URL for one call's audio, and the instant it stops working. It does not return the audio, and it never returns the provider's own URL — that link is copyable, outlives the API key that revealed it, and on one of our two upstreams is fetched with the platform's own account credential.
THE RETURNED LINK IS BEARER-EQUIVALENT until it expires: anyone holding it can play that one call's audio with no further authentication. That is what makes it usable by a media player, and it is why the window is minutes. Do not store it — fetch a fresh one.
It names ONE call and ONE media kind and cannot be widened. ?ttl= is clamped to 900 seconds however large a number you send, so read expires_at rather than assuming what you asked for.
Calls on this platform are recorded under a disclosure regime the shop configures; that decision was made when the call was answered and fetching the audio does not change it.
curl https://www.servicevin.com/api/v1/calls/{id}/recording \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The call's id. |
mediastring | query | The recorded conversation, or the message the caller left. Default:recordingOne of: recording, voicemail |
ttlinteger | query | Seconds the link should live, 30-900. Larger values are clamped to 900. Default:300 |
{
"data": {
"call": "8c14f0b7-3d95-42ae-9b6c-0e7a5d3f21b8",
"kind": "recording",
"url": "https://www.servicevin.com/api/calls/media/8c14f0b7-3d95-42ae-9b6c-0e7a5d3f21b8?token=eyJz…",
"expires_at": "2026-07-16T18:03:11.482Z",
"duration_seconds": 196
}
}Response fields
| Field | Type | Description |
|---|---|---|
call | string | The call the link is for. |
kind | string | Which audio the link fetches. One of:recording, voicemail |
url | string | The signed link. Points at Service VIN, never at the storage provider. |
expires_at | string | When the link stops working. The authoritative value — read it. |
duration_seconds | integer | null | How long the audio runs. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 404 | not_found | No call with that id in this shop. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | That call has no audio of the requested kind, or this deployment has no public site URL configured. |
/api/v1/calls/{id}/transcriptRead a call transcript
What was said on one call, as text. Separate from the call record on purpose: a call's LOG entry and its customer's WORDS are different sensitivities, and folding them together would mean every list-and-fetch integration held the words whether it needed them or not.
Not every call has one — a short wrong number has audio and no transcript — and has_transcript on the call record says which before you ask. source names what produced it: a live-agent capture, or an after-the-fact machine transcription that carries the usual mishearings of names, plates and prices. Treat a figure read out of a transcript as something to confirm, never as the price.
curl https://www.servicevin.com/api/v1/calls/{id}/transcript \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The call's id. |
{
"data": {
"call": "8c14f0b7-3d95-42ae-9b6c-0e7a5d3f21b8",
"transcript": "Caller: Hi, is the black Model 3 ready?\\nShop: It is — we finished the front bumper this morning.",
"source": "live_agent",
"started_at": "2026-07-16T18:03:11.482Z",
"duration_seconds": 214
}
}Response fields
| Field | Type | Description |
|---|---|---|
call | string | The call. |
transcript | string | null | What was said. Null when the call was never transcribed. |
source | string | null | What produced it — a live capture, or a transcription of the recording. |
started_at | string | When the call started. |
duration_seconds | integer | null | How long the call ran. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 404 | not_found | No call with that id in this shop. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
Tasks
The shop's to-do queue, anchored to a thread, a customer, a job and the message it came from. One change per call, and nothing deletes.
/api/v1/tasksList tasks
The shop's to-do queue in the order it is worked: OPEN first (soonest due, undated last, then newest), then finished (most recently cleared first). That ordering answers two questions in one list — what is next, and what did we just clear.
A Service VIN task is anchored to a conversation AND a customer AND a job AND the message it came out of, so a to-do that reads "call them back about the price" still carries everything needed to act on it.
?overdue=true overrides ?status=: an overdue FINISHED task is a contradiction, so the lens wins rather than AND-ing into an always-empty result.
curl https://www.servicevin.com/api/v1/tasks \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
statusstring | query |
openOne of: open, done, all |
assigned_tostring | query | Only this teammate's tasks. |
conversation_idstring | query | Only tasks filed against this thread. |
overduestring | query |
true |
limitinteger | query | How many to return, 1-100. Default:25 |
offsetinteger | query | How many to skip. Pass 0 |
{
"data": [
{
"id": "c6b0f2d8-5a41-4e93-87bd-2f4c9e1a6053",
"title": "Call Ben back with the ceramic price",
"notes": null,
"status": "open",
"priority": "high",
"due_at": "2026-07-16T18:03:11.482Z",
"overdue": true,
"completed_at": null,
"created_at": "2026-07-16T18:03:11.482Z",
"conversation": "a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05",
"conversation_label": "Ben Carter",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"job_id": null,
"message_id": "5d1e9a73-2f48-4c60-b915-8e3a7c2d4f06",
"assigned_to": "e08c3a71-9d46-4b25-a1f7-63b90d5c284e",
"assignee_name": "Dana"
}
],
"meta": {
"total": 7,
"omitted": 6,
"next_offset": 1
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the task. |
title | string | What needs doing, in one line. |
notes | string | null | Any detail behind the title. |
status | string |
open, done, canceled |
priority | string | How urgent. One of:low, normal, high |
due_at | string | null | When it is due. Null for an undated to-do, which sorts last. |
overdue | boolean | Open and past its due date. Computed at read time, so it is always current. |
completed_at | string | null | When it was finished. Cleared on reopen, so a re-completed task reports the real finish. |
created_at | string | When it was created. |
conversation | string | null | The inbox thread it belongs to. |
conversation_label | string | null | Display name for that thread's contact. |
customer_id | string | null | The customer it is about. |
job_id | string | null | The job it hangs off. |
message_id | string | null | The thread line the task was created from — the customer's own words rather than a paraphrase. |
assigned_to | string | null | The teammate who owns it. |
assignee_name | string | null | That teammate's display name. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 422 | validation_error |
|
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
/api/v1/tasksFull-access keyCreate a task
Adds one to-do. title is all that is required; everything else is an ANCHOR, and a task with anchors is worth several times one without.
Anchor it to what caused it. message_id is the strongest — the thread line the task came out of, so whoever picks it up reads the customer's own words rather than a paraphrase. conversation_id files it against the thread, customer_id against the person, job_id against the car. They are independent; pass every one you know.
Assigning PINGS somebody, and the target must be a teammate whose role grants inbox access. It is NOT idempotent: calling twice makes two tasks.
curl -X POST https://www.servicevin.com/api/v1/tasks \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Call Ben back with the ceramic price","conversation":"a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05","customer_id":"b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48","priority":"high"}'Body
The task, and whatever it is about.
| Field | Type | Description |
|---|---|---|
titlerequired | stringmax 200 | What needs doing, in one line. This is what the shop sees. |
notes | stringmax 4000 | Detail behind the title. |
priority | string | How urgent. Default:normalOne of: low, normal, high |
due_at | string | ISO instant it is due. Resolve "tomorrow" against the SHOP's timezone before sending. |
conversation | string | The inbox thread it belongs to. |
customer_id | string | The customer it is about. |
job_id | string | The job it hangs off. |
message_id | string | The thread line that caused it. |
staff_id | string | Hand it to this teammate. They are notified. |
{
"data": {
"id": "c6b0f2d8-5a41-4e93-87bd-2f4c9e1a6053",
"title": "Call Ben back with the ceramic price",
"notes": null,
"status": "open",
"priority": "high",
"due_at": null,
"overdue": false,
"completed_at": null,
"created_at": "2026-07-16T18:03:11.482Z",
"conversation": "a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05",
"conversation_label": "Ben Carter",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"job_id": null,
"message_id": null,
"assigned_to": null,
"assignee_name": null
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the task. |
title | string | What needs doing, in one line. |
notes | string | null | Any detail behind the title. |
status | string |
open, done, canceled |
priority | string | How urgent. One of:low, normal, high |
due_at | string | null | When it is due. Null for an undated to-do, which sorts last. |
overdue | boolean | Open and past its due date. Computed at read time, so it is always current. |
completed_at | string | null | When it was finished. Cleared on reopen, so a re-completed task reports the real finish. |
created_at | string | When it was created. |
conversation | string | null | The inbox thread it belongs to. |
conversation_label | string | null | Display name for that thread's contact. |
customer_id | string | null | The customer it is about. |
job_id | string | null | The job it hangs off. |
message_id | string | null | The thread line the task was created from — the customer's own words rather than a paraphrase. |
assigned_to | string | null | The teammate who owns it. |
assignee_name | string | null | That teammate's display name. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body was not JSON. |
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 422 | validation_error |
|
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The named teammate's role does not grant inbox access, so the task would sit somewhere they cannot see it. |
/api/v1/tasks/{id}Get a task
One task and every anchor it carries. The anchors are the point: a to-do that says "call them back about the price" is useless on its own and complete with a thread id, and message_id is the exact sentence that caused it.
overdue is computed against now rather than stored, so it is current when you read it. completed_at is cleared on reopen, so a task completed by mistake and finished properly later reports the real finish rather than the first one's.
curl https://www.servicevin.com/api/v1/tasks/{id} \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The task's id. |
{
"data": {
"id": "c6b0f2d8-5a41-4e93-87bd-2f4c9e1a6053",
"title": "Call Ben back with the ceramic price",
"notes": "He asked about the 5-year package.",
"status": "open",
"priority": "high",
"due_at": "2026-07-16T18:03:11.482Z",
"overdue": true,
"completed_at": null,
"created_at": "2026-07-16T18:03:11.482Z",
"conversation": "a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05",
"conversation_label": "Ben Carter",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"job_id": "4a92c7f1-8d36-4b05-9e2a-1f6b3d8c50e7",
"message_id": "5d1e9a73-2f48-4c60-b915-8e3a7c2d4f06",
"assigned_to": "e08c3a71-9d46-4b25-a1f7-63b90d5c284e",
"assignee_name": "Dana"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the task. |
title | string | What needs doing, in one line. |
notes | string | null | Any detail behind the title. |
status | string |
open, done, canceled |
priority | string | How urgent. One of:low, normal, high |
due_at | string | null | When it is due. Null for an undated to-do, which sorts last. |
overdue | boolean | Open and past its due date. Computed at read time, so it is always current. |
completed_at | string | null | When it was finished. Cleared on reopen, so a re-completed task reports the real finish. |
created_at | string | When it was created. |
conversation | string | null | The inbox thread it belongs to. |
conversation_label | string | null | Display name for that thread's contact. |
customer_id | string | null | The customer it is about. |
job_id | string | null | The job it hangs off. |
message_id | string | null | The thread line the task was created from — the customer's own words rather than a paraphrase. |
assigned_to | string | null | The teammate who owns it. |
assignee_name | string | null | That teammate's display name. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 404 | not_found | No task with that id in this shop. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
/api/v1/tasks/{id}Full-access keyChange one thing about a task
A VERB CALL. Send exactly ONE of title, notes, priority, complete, reopen, cancel, staff_id, unassign, due_at, remove_due_date, link_conversation or unlink_conversation. Two is refused, naming both — a single audit row cannot honestly describe two decisions.
Complete, reopen and cancel are three different outcomes. complete means it was done; cancel means it will not be, and the row stays so "we were going to and decided not to" is still visible weeks later; reopen clears the completion stamp so a re-completed task reports the real finish.
THERE IS NO DELETE, here or anywhere on this API. A task is the record of something a shop said it would do, and cancel is the honest version of removing one.
curl -X POST https://www.servicevin.com/api/v1/tasks/{id} \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"complete":true}'Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The task's id. |
Body
Exactly one change.
| Field | Type | Description |
|---|---|---|
title | stringmax 200 | Rewrite the one-line title. |
notes | stringmax 4000 | Replace the notes. An empty string clears them. |
priority | string | Set urgency. One of:low, normal, high |
complete | boolean | Mark it done. |
reopen | boolean | Put a done or cancelled task back to open. |
cancel | boolean | It will not be done. The row stays. |
staff_id | string | Hand it to this teammate. They are notified. |
unassign | boolean | Take it off whoever holds it. |
due_at | string | ISO instant it is due, resolved against the shop's timezone. |
remove_due_date | boolean | Clear the due date entirely. |
link_conversation | string | Re-file it under this thread. |
unlink_conversation | boolean | Detach it from its thread. |
{
"data": {
"id": "c6b0f2d8-5a41-4e93-87bd-2f4c9e1a6053",
"title": "Call Ben back with the ceramic price",
"notes": null,
"status": "done",
"priority": "high",
"due_at": "2026-07-16T18:03:11.482Z",
"overdue": false,
"completed_at": "2026-07-16T18:03:11.482Z",
"created_at": "2026-07-16T18:03:11.482Z",
"conversation": "a2c79d31-6b40-4e28-9f57-3d8b1a6c2e05",
"conversation_label": "Ben Carter",
"customer_id": "b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48",
"job_id": null,
"message_id": "5d1e9a73-2f48-4c60-b915-8e3a7c2d4f06",
"assigned_to": "e08c3a71-9d46-4b25-a1f7-63b90d5c284e",
"assignee_name": "Dana"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the task. |
title | string | What needs doing, in one line. |
notes | string | null | Any detail behind the title. |
status | string |
open, done, canceled |
priority | string | How urgent. One of:low, normal, high |
due_at | string | null | When it is due. Null for an undated to-do, which sorts last. |
overdue | boolean | Open and past its due date. Computed at read time, so it is always current. |
completed_at | string | null | When it was finished. Cleared on reopen, so a re-completed task reports the real finish. |
created_at | string | When it was created. |
conversation | string | null | The inbox thread it belongs to. |
conversation_label | string | null | Display name for that thread's contact. |
customer_id | string | null | The customer it is about. |
job_id | string | null | The job it hangs off. |
message_id | string | null | The thread line the task was created from — the customer's own words rather than a paraphrase. |
assigned_to | string | null | The teammate who owns it. |
assignee_name | string | null | That teammate's display name. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body was not JSON. |
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 404 | not_found | No task with that id in this shop. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | No change was given, more than one was, or the named teammate's role does not grant inbox access. |
Phone numbers
The lines this shop sends and receives on, and which of them can actually send.
/api/v1/phone-numbersList the shop's phone numbers
The lines this shop sends and receives on, primary first. is_primary is the number a customer actually sees — the one automations, click-to-call and outbound texts use when nothing names a line.
is_sending is whether a line can send AT ALL, which is not the same as the shop holding it: a number suspended for billing, or a reservation that never completed at the carrier, is a real row that will not carry a text. Released numbers never appear, and neither does an abandoned purchase.
meta.messaging_service is set when the shop texts through a messaging service rather than a single line; the individual numbers under it still list normally.
curl https://www.servicevin.com/api/v1/phone-numbers \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
limitinteger | query | How many to return, 1-100. A shop holds a handful. Default:50 |
{
"data": [
{
"id": "2f7d4c91-8b03-45e6-a17c-9d5028f3b64a",
"number": "+14035557890",
"name": "Front desk",
"provider": "twilio",
"status": "active",
"country": "CA",
"is_primary": true,
"is_sending": true,
"purchased_at": "2026-07-16T18:03:11.482Z"
}
],
"meta": {
"messaging_service": null,
"total": 1,
"omitted": 0
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the line. |
number | string | The number itself, E.164. |
name | string | null | The owner's own label — "Front desk", "Mobile crew". |
provider | string | Who the line is held with. |
status | string |
|
country | string | null | Two-letter country code. |
is_primary | boolean | The number automations, click-to-call and outbound texts use when nothing names a line — the one a customer sees. |
is_sending | boolean | Whether the line can actually send. Not the same as holding it: a suspended line is held and cannot carry a text. |
purchased_at | string | null | When the shop bought it. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
Services & staff
The service menu and the roster — the lists that fill assignment dropdowns.
/api/v1/servicesList the service menu
The shop's services, alphabetical, with the category name inlined. Active services only unless you pass active=false. Not paginated — a menu is a menu.
curl https://www.servicevin.com/api/v1/services \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
activestring | query |
trueOne of: true, false |
{
"data": [
{
"id": "8f2b6d14-3a95-4e07-b2c6-71d5e9a3f480",
"name": "Full-front PPF",
"description": "Hood, fenders, mirrors and bumper.",
"sku": "PPF-FF",
"category": "Paint protection film",
"base_price": 2200,
"pricing_model": "flat",
"default_duration_minutes": 480,
"is_active": true
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Service VIN's id for the service. |
name | string | Menu name. |
description | string | null | Longer description. |
sku | string | null | The shop's own code. |
category | string | null | Category name, inlined. |
base_price | number | null | List price in the shop's currency. |
pricing_model | string | How the price is worked out. |
default_duration_minutes | integer | null | Default booking length. |
is_active | boolean | Whether the shop still sells it. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The service query failed. |
/api/v1/staffList staff
The shop's roster, alphabetical, for assignment dropdowns. Not paginated. The two ids are not interchangeable: id is a staff-profile id and assigns a JOB, user_id is a login id and assigns a LEAD. user_id is null for a staff profile with no login, which is why they cannot be collapsed into one field.
curl https://www.servicevin.com/api/v1/staff \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
bookablestring | query | Narrow to staff who can (or cannot) be scheduled. One of:true, false |
{
"data": [
{
"id": "c73e5a91-8d24-4f6b-90e7-2a5c8f1b3d46",
"user_id": "e15c3b78-4a92-4d05-8f61-7b2e9c4a6d38",
"name": "Alex Mercer",
"is_bookable": true,
"skills": [
"ppf",
"tint"
]
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Staff-profile id. This is what |
user_id | string | null | The person's login id, and what |
name | string | Display name, falling back to the login's name and then its email. |
is_bookable | boolean | Whether they can be scheduled. |
skills | string[] | Skill tags. Empty array when none are set. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The staff query failed. |
Webhooks
Subscribe a URL to events, list your subscriptions, unsubscribe.
/api/v1/hooksList webhook subscriptions
Every webhook endpoint on the shop, newest first. Signing secrets are never returned here — they are shown once, at subscribe time. This list is not paginated: the ceiling is 10 endpoints per shop, so there is no meta block.
curl https://www.servicevin.com/api/v1/hooks \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"{
"data": [
{
"id": "3e7a1c4e-5b92-4f08-a6d3-8c204f7b1e59",
"url": "https://hooks.zapier.com/hooks/catch/123456/abcdef/",
"description": "Zapier/Make subscription",
"events": [
"lead.created"
],
"active": true,
"consecutive_failures": 0,
"created_at": "2026-07-16T18:03:11.482Z"
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Pass this to |
url | string | Where deliveries are POSTed. |
description | string | null | Label shown in the dashboard. |
events | string[] | Subscribed events. An empty array means every event. One of:lead.created, lead.status_changed, lead.assigned, quote.sent, quote.accepted, quote.declined, job.created, job.stage_changed, job.completed, job.assigned, appointment.booked, appointment.rescheduled, appointment.canceled, invoice.paid, message.received, message.sent, message.delivery_failed, conversation.assigned, call.missed, call.completed, call.recording_ready, call.transcript_ready, warranty.issued, training.requested, training.enrolled, training.deposit_paid, training.completed, training.certified, review.private_feedback |
active | boolean | Endpoints auto-pause after 10 consecutive delivery failures and are resumed from the dashboard. |
consecutive_failures | integer | Failed deliveries in a row. A single success resets it to 0. |
created_at | string | When the subscription was made. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The endpoint query failed. |
/api/v1/hooksFull-access keySubscribe to events
Registers an https URL to receive signed event deliveries. The response carries the signing secret once — store it, because it is never shown again. The URL is checked against private, loopback and link-local ranges before it is saved, and again on every delivery.
curl -X POST https://www.servicevin.com/api/v1/hooks \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://hooks.zapier.com/hooks/catch/123456/abcdef/","events":["lead.created"]}'Body
The endpoint to subscribe.
| Field | Type | Description |
|---|---|---|
urlrequired | stringmax 2000 | Must be |
events | string[] | Which events to receive. Omit it, or send [] (every event)One of: lead.created, lead.status_changed, lead.assigned, quote.sent, quote.accepted, quote.declined, job.created, job.stage_changed, job.completed, job.assigned, appointment.booked, appointment.rescheduled, appointment.canceled, invoice.paid, message.received, message.sent, message.delivery_failed, conversation.assigned, call.missed, call.completed, call.recording_ready, call.transcript_ready, warranty.issued, training.requested, training.enrolled, training.deposit_paid, training.completed, training.certified, review.private_feedback |
{
"data": {
"id": "3e7a1c4e-5b92-4f08-a6d3-8c204f7b1e59",
"url": "https://hooks.zapier.com/hooks/catch/123456/abcdef/",
"events": [
"lead.created"
],
"secret": "whsec_EXAMPLE_ONLY_yours_will_differ"
}
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Pass this to |
url | string | Where deliveries will be POSTed. |
events | string[] | What you subscribed to. One of:lead.created, lead.status_changed, lead.assigned, quote.sent, quote.accepted, quote.declined, job.created, job.stage_changed, job.completed, job.assigned, appointment.booked, appointment.rescheduled, appointment.canceled, invoice.paid, message.received, message.sent, message.delivery_failed, conversation.assigned, call.missed, call.completed, call.recording_ready, call.transcript_ready, warranty.issued, training.requested, training.enrolled, training.deposit_paid, training.completed, training.certified, review.private_feedback |
secret | string | The |
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body was not valid JSON. |
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | plan_required | Webhooks are a Growth-plan feature and this shop is on a lower plan. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 409 | conflict | The shop already has 10 endpoints. Delete one first. |
| 422 | validation_error |
|
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The subscription could not be stored. |
/api/v1/hooks/{id}Full-access keyUnsubscribe
Removes the subscription and its signing secret. Scoped to your shop: an id belonging to another shop reads as not_found, never as forbidden-but-real.
curl -X DELETE https://www.servicevin.com/api/v1/hooks/3e7a1c4e-5b92-4f08-a6d3-8c204f7b1e59 \
-H "Authorization: Bearer $SERVICEVIN_API_KEY"Parameters
| Name | In | Description |
|---|---|---|
idrequiredstring | path | The subscription id from the subscribe response or the list. |
204 Removed. No body.
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed, unknown, revoked, or past its expiry date. Unknown and revoked share one message on purpose, so a probe learns nothing; an expired key says so, since only its holder can ever see that. |
| 402 | plan_lapsed | The Service VIN account behind this key has no active subscription — its free trial ended, its plan was canceled, or a payment failed past its retry window. The shop owner reactivates it under Settings → Plan & billing; retrying will not. |
| 403 | org_suspended | The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not. |
| 403 | insufficient_scope | The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created. |
| 404 | not_found | No such subscription on this shop. |
| 429 | rate_limited | More than 120 requests in 60 seconds on this key. |
| 500 | internal_error | The delete failed. |
OpenAPI spec
The machine-readable version of this page.
/api/v1/openapi.jsonNo authThe OpenAPI 3.1 document
This whole reference as a machine-readable spec — import it into Postman, Insomnia, or any OpenAPI client generator. No authentication required, because a spec is not data.
curl https://www.servicevin.com/api/v1/openapi.json{
"openapi": "3.1.0",
"info": {
"title": "Service VIN API",
"version": "1.0.0"
},
"servers": [
{
"url": "https://www.servicevin.com"
}
],
"paths": {
"/api/v1/me": {
"get": {
"operationId": "get-me"
}
}
}
}