Skip to main content
Documentation

Platform

REST resource reference

A REST API over one shop's leads, customers, vehicles, quotes, invoices and jobs, plus signed webhooks. Authenticate with a key you mint yourself — no OAuth dance, no approval queue. Read Authentication, Pagination and Errors first — they are true of every endpoint below.
Base URLhttps://www.servicevin.com/api/v1

Account

Confirm which shop a key belongs to.

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.

Request
curl https://www.servicevin.com/api/v1/me \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

200 — The shop this key is scoped to, and the key's own limits.
{
  "data": {
    "shop": {
      "id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
      "name": "Apex Detailing"
    },
    "key": {
      "scope": "read",
      "expires_at": null
    }
  }
}

Response fields

FieldTypeDescription
shop.idstring

The shop's id.

shop.namestring

The shop's name.

key.scopestring

What this key may do. read calls every GET; full also writes. Fixed when the key is minted.

One of: read, full
key.expires_atstring | 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

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

429rate_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.

GET/api/v1/leads

List 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.

Request
curl https://www.servicevin.com/api/v1/[email protected] \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
emailstringquery

Exact match, lower-cased before comparing.

phonestringquery

Normalized to E.164 before comparing, so any common format works.

statusstringquery

Only leads at this pipeline status.

One of: new, contacted, qualified, quoted, won, lost, cold
limitintegerquery

Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default.

Default: 25
cursorstringquery

The previous page's meta.next_cursor. Opaque — decode nothing, pass it back verbatim.

200 — A page of leads, newest first.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the lead.

namestring

First and last name joined, falling back to the company name, then to Unnamed lead.

emailstring | null

Lowercased on write.

phonestring | null

Normalized to E.164 on write.

sourcestring | null

Whatever the creator declared. Leads created through this API default to api.

statusstring

Pipeline status. Every API-created lead starts at new.

One of: new, contacted, qualified, quoted, won, lost, cold
assigned_tostring | null

The staff member who owns the lead — a user id, which is the user_id from GET /api/v1/staff, not that row's id.

next_followup_atstring | null

When this lead surfaces for follow-up. A fresh lead is set to now.

created_atstring

When the lead was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The lead query failed.

POST/api/v1/leadsFull-access key

Create 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.

Request
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.

FieldTypeDescription
namerequiredstringmax 200

Full name. Split into first and last on the first whitespace run.

emailstringmax 200

Lowercased before storing.

phonestringmax 40

Any common format — normalized to E.164 before storing.

sourcestringmax 120

Where the lead came from, e.g. google-ads. Shows up in lead-source reporting.

Default: api
notesstringmax 2000

Free text. Lands on the lead's timeline as a note.

201 — The created lead, in the same shape the list returns.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the lead.

namestring

First and last name joined, falling back to the company name, then to Unnamed lead.

emailstring | null

Lowercased on write.

phonestring | null

Normalized to E.164 on write.

sourcestring | null

Whatever the creator declared. Leads created through this API default to api.

statusstring

Pipeline status. Every API-created lead starts at new.

One of: new, contacted, qualified, quoted, won, lost, cold
assigned_tostring | null

The staff member who owns the lead — a user id, which is the user_id from GET /api/v1/staff, not that row's id.

next_followup_atstring | null

When this lead surfaces for follow-up. A fresh lead is set to now.

created_atstring

When the lead was created.

Errors

StatusCodeWhen
400invalid_request

The body was not valid JSON.

401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

422validation_error

A field failed validation — the message names it, e.g. name: name is required.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The insert failed.

GET/api/v1/leads/{id}

Get one lead

The same shape the list returns, for one lead.

Request
curl https://www.servicevin.com/api/v1/leads/6f1c37a2-91b8-4d0e-8a55-2c7e4b1d9f03 \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
idrequiredstringpath

The lead's id.

200 — The lead.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the lead.

namestring

First and last name joined, falling back to the company name, then to Unnamed lead.

emailstring | null

Lowercased on write.

phonestring | null

Normalized to E.164 on write.

sourcestring | null

Whatever the creator declared. Leads created through this API default to api.

statusstring

Pipeline status. Every API-created lead starts at new.

One of: new, contacted, qualified, quoted, won, lost, cold
assigned_tostring | null

The staff member who owns the lead — a user id, which is the user_id from GET /api/v1/staff, not that row's id.

next_followup_atstring | null

When this lead surfaces for follow-up. A fresh lead is set to now.

created_atstring

When the lead was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

404not_found

No lead with that id in this shop.

429rate_limited

More than 120 requests in 60 seconds on this key.

PATCH/api/v1/leads/{id}Full-access key

Update 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.

Request
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

NameInDescription
idrequiredstringpath

The lead's id.

Body

At least one field. Anything omitted is left alone.

FieldTypeDescription
statusstring

New pipeline status.

One of: new, contacted, qualified, quoted, won, lost, cold
assigned_tostring | null

Owner — the user_id from GET /api/v1/staff, not that row's id. Null clears it.

next_followup_atstring | null

When the lead next surfaces. Null clears it.

200 — The updated lead.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the lead.

namestring

First and last name joined, falling back to the company name, then to Unnamed lead.

emailstring | null

Lowercased on write.

phonestring | null

Normalized to E.164 on write.

sourcestring | null

Whatever the creator declared. Leads created through this API default to api.

statusstring

Pipeline status. Every API-created lead starts at new.

One of: new, contacted, qualified, quoted, won, lost, cold
assigned_tostring | null

The staff member who owns the lead — a user id, which is the user_id from GET /api/v1/staff, not that row's id.

next_followup_atstring | null

When this lead surfaces for follow-up. A fresh lead is set to now.

created_atstring

When the lead was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

404not_found

No lead with that id in this shop.

422validation_error

The body carried no updatable field, or a field failed validation.

429rate_limited

More than 120 requests in 60 seconds on this key.

Customers

Read, search by email or phone, create and update.

GET/api/v1/customers

List 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.

Request
curl https://www.servicevin.com/api/v1/customers \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
emailstringquery

Exact match, case-insensitive.

phonestringquery

Exact match after E.164 normalization. A number that cannot be normalized returns an empty page rather than an error.

limitintegerquery

Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default.

Default: 25
cursorstringquery

The previous page's meta.next_cursor. Opaque — decode nothing, pass it back verbatim.

200 — A page of customers, newest first.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the customer.

namestring

The customer's display name, or when the record has none.

emailstring | null

Lowercased on write.

phonestring | null

Normalized to E.164 on write.

citystring | null

City on the customer's address.

regionstring | null

Province or state.

postal_codestring | null

Postal or ZIP code.

tagsstring[]

Free-form labels the shop applies. Empty array when there are none.

is_vipboolean

The shop's own VIP flag.

created_atstring

When the customer was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The customer query failed.

POST/api/v1/customersFull-access key

Create 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.

Request
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.

FieldTypeDescription
namerequiredstringmax 200

Full name.

emailstringmax 200

Required unless phone is sent.

phonestringmax 40

Required unless email is sent. Normalized to E.164.

consent_sourcestring

How this contact consented to be messaged. Posting a contact is not itself evidence of consent, so this defaults to unknown rather than the API inventing a basis. Send it only when you genuinely know.

Default: unknownOne of: express_written, express_verbal, online_booking, web_form, implied_existing_business, implied_inquiry, imported, unknown
consent_captured_atstring

When that consent was captured (ISO 8601 with an offset). Dropped when consent_source is unknown, so a bare timestamp can never later read as evidence.

consent_notestringmax 500

Free-text provenance, e.g. the form name.

201 — The created customer.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the customer.

namestring

The customer's display name, or when the record has none.

emailstring | null

Lowercased on write.

phonestring | null

Normalized to E.164 on write.

citystring | null

City on the customer's address.

regionstring | null

Province or state.

postal_codestring | null

Postal or ZIP code.

tagsstring[]

Free-form labels the shop applies. Empty array when there are none.

is_vipboolean

The shop's own VIP flag.

created_atstring

When the customer was created.

Errors

StatusCodeWhen
400invalid_request

The body was not valid JSON.

401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

409conflict

That email or phone already belongs to a customer. The message carries the existing id.

422validation_error

A field failed validation, or neither email nor phone was sent.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The insert failed.

GET/api/v1/customers/{id}

Get one customer

The same shape the list returns, for one customer.

Request
curl https://www.servicevin.com/api/v1/customers/b41d8e57-2c9a-4f6b-8d13-7a0e5c2f9b48 \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
idrequiredstringpath

The customer's id.

200 — The customer.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the customer.

namestring

The customer's display name, or when the record has none.

emailstring | null

Lowercased on write.

phonestring | null

Normalized to E.164 on write.

citystring | null

City on the customer's address.

regionstring | null

Province or state.

postal_codestring | null

Postal or ZIP code.

tagsstring[]

Free-form labels the shop applies. Empty array when there are none.

is_vipboolean

The shop's own VIP flag.

created_atstring

When the customer was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

404not_found

No customer with that id in this shop.

429rate_limited

More than 120 requests in 60 seconds on this key.

PATCH/api/v1/customers/{id}Full-access key

Update 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.

Request
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

NameInDescription
idrequiredstringpath

The customer's id.

Body

At least one field. Anything omitted is left alone.

FieldTypeDescription
namestringmax 200

Split into first and last.

emailstring | null

Lowercased. Null clears it.

phonestring | null

Normalized to E.164. Null clears it.

tagsstring[]

Replaces the whole tag list.

is_vipboolean

The shop's VIP flag.

200 — The updated customer.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the customer.

namestring

The customer's display name, or when the record has none.

emailstring | null

Lowercased on write.

phonestring | null

Normalized to E.164 on write.

citystring | null

City on the customer's address.

regionstring | null

Province or state.

postal_codestring | null

Postal or ZIP code.

tagsstring[]

Free-form labels the shop applies. Empty array when there are none.

is_vipboolean

The shop's own VIP flag.

created_atstring

When the customer was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

404not_found

No customer with that id in this shop.

409conflict

Another customer already holds the email or phone being moved. The message names the id.

422validation_error

The body carried no updatable field, the phone was unparseable, or the change would leave the customer with no contact at all.

429rate_limited

More than 120 requests in 60 seconds on this key.

Vehicles

Read, search by VIN or plate, and create — with VIN decode included.

GET/api/v1/vehicles

List 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.

Request
curl https://www.servicevin.com/api/v1/vehicles \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
customer_idstringquery

Only this customer's vehicles.

vinstringquery

Exact match after normalizing.

license_platestringquery

Case-insensitive match.

limitintegerquery

Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default.

Default: 25
cursorstringquery

The previous page's meta.next_cursor. Opaque — decode nothing, pass it back verbatim.

200 — A page of vehicles, newest first.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the vehicle.

customer_idstring

The vehicle's owner.

labelstring | null

Year, make, model and trim as one string (2024 BMW M3) — for message templates that want the car in a single field.

yearinteger | null

Model year.

makestring | null

Manufacturer.

modelstring | null

Model.

trimstring | null

Trim level.

colorstring | null

Colour as the shop recorded it.

vinstring | null

17-character VIN, upper-cased.

license_platestring | null

Plate as recorded.

mileageinteger | null

Odometer reading when recorded.

vehicle_typestring

Body category, filled from the VIN decode when one was available.

created_atstring

When the vehicle was added.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The vehicle query failed.

POST/api/v1/vehiclesFull-access key

Create 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.

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.

FieldTypeDescription
customer_idrequiredstring

Must be a customer of this shop.

vinstringmax 32

17 characters, excluding I, O and Q.

yearinteger

Model year.

makestringmax 80

Overrides the decode.

modelstringmax 80

Overrides the decode.

trimstringmax 80

Overrides the decode.

colorstringmax 40

Colour.

license_platestringmax 20

Plate.

mileageinteger

Odometer reading.

vehicle_typestring

Overrides the decoded body category.

201 — The created vehicle.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the vehicle.

customer_idstring

The vehicle's owner.

labelstring | null

Year, make, model and trim as one string (2024 BMW M3) — for message templates that want the car in a single field.

yearinteger | null

Model year.

makestring | null

Manufacturer.

modelstring | null

Model.

trimstring | null

Trim level.

colorstring | null

Colour as the shop recorded it.

vinstring | null

17-character VIN, upper-cased.

license_platestring | null

Plate as recorded.

mileageinteger | null

Odometer reading when recorded.

vehicle_typestring

Body category, filled from the VIN decode when one was available.

created_atstring

When the vehicle was added.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

404not_found

No customer with that id in this shop.

409conflict

That VIN is already on file for this shop. The message names the existing vehicle id.

422validation_error

The VIN was malformed, or the body carried neither a VIN nor a make or model.

429rate_limited

More than 120 requests in 60 seconds on this key.

Quotes

Read-only. Quotes are built in the app; the API reports them.

GET/api/v1/quotes

List 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.

Request
curl https://www.servicevin.com/api/v1/quotes?status=accepted \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
statusstringquery

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
limitintegerquery

Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default.

Default: 25
cursorstringquery

The previous page's meta.next_cursor. Opaque — decode nothing, pass it back verbatim.

200 — A page of quotes, newest first.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the quote.

numberinteger

The shop-visible quote number.

customer_idstring

The customer this quote belongs to.

statusstring

Where the quote stands.

One of: draft, sent, viewed, accepted, declined, expired
totalnumber

Quote total, tax included, in the shop's currency.

created_atstring

When the quote was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

422validation_error

status is not one of the listed values.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The quote query failed.

Invoices

Read-only. Invoices are issued and collected in the app; the API reports them.

GET/api/v1/invoices

List 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.

Request
curl https://www.servicevin.com/api/v1/invoices?status=open \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
statusstringquery

Return only invoices in this status. An unrecognised value is a 422.

One of: draft, open, partial, paid, void, refunded
importedstringquery

Return only records that arrived through a data import. Default lists only records created in Service VIN. 1 and 0 are accepted as aliases; any other value is a 422 rather than a silent fallback, because a caller who typos this has been given a wrong answer about which records exist.

Default: falseOne of: true, false
limitintegerquery

Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default.

Default: 25
cursorstringquery

The previous page's meta.next_cursor. Opaque — decode nothing, pass it back verbatim.

200 — A page of invoices, newest first.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the invoice.

numberinteger

The shop-visible invoice number.

customer_idstring

The customer this invoice belongs to.

statusstring

Where the invoice stands.

One of: draft, open, partial, paid, void, refunded
totalnumber

Invoice total, tax included.

balancenumber | null

Amount still owing. 0 once the invoice is paid in full.

created_atstring

When the invoice was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

422validation_error

status is not one of the listed values.

422validation_error

imported is not true or false (or their 1/0 aliases).

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The invoice query failed.

500internal_error

imported=true on a database without migration 0187/0152 — invoices are the one resource whose provenance can be unanswerable, and slicing by it cannot be faked. The default live list still answers, and the request succeeds on retry once the shop applies the migration.

Jobs

Read, and move a job between stages. Job creation returns 501 on purpose — the reason is spelled out below.

GET/api/v1/jobs

List 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.

Request
curl https://www.servicevin.com/api/v1/jobs?stage=scheduled \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
stagestringquery

Return only jobs at this stage. An unrecognised value is a 422.

One of: lead, scheduled, in_progress, curing, qa, ready, completed, canceled
customer_idstringquery

Only this customer's jobs — their whole history, newest first. A migrated customer's pre-switch work sits behind ?imported=true on this same customer_id: the provenance rule keys on the record, never on the person.

importedstringquery

Return only records that arrived through a data import. Default lists only records created in Service VIN. 1 and 0 are accepted as aliases; any other value is a 422 rather than a silent fallback, because a caller who typos this has been given a wrong answer about which records exist.

Default: falseOne of: true, false
limitintegerquery

Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default.

Default: 25
cursorstringquery

The previous page's meta.next_cursor. Opaque — decode nothing, pass it back verbatim.

200 — A page of jobs, newest first.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the job.

numberinteger

The shop-visible job number.

titlestring | null

What the job is, e.g. Full-front PPF.

customer_idstring

The customer this job belongs to.

vehicle_idstring | null

The vehicle being worked on, when one is attached.

stagestring

Where the job sits on the board.

One of: lead, scheduled, in_progress, curing, qa, ready, completed, canceled
assignee_idstring | null

Assigned staff — a staff profile id, which is the id from GET /api/v1/staff, not that row's user_id.

scheduled_startstring | null

Booked start, or null while the job is unscheduled.

scheduled_endstring | null

Booked end, or null while the job is unscheduled.

actual_startstring | null

When work really began — stamped the first time the job reaches in_progress or beyond, and never cleared by a move backwards.

actual_endstring | null

When work finished. Re-stamped on every entry into completed, and cleared when the job leaves it.

location_addressstring | null

Where the work happens, for mobile jobs.

created_atstring

When the job was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

422validation_error

stage is not one of the listed values.

422validation_error

imported is not true or false (or their 1/0 aliases).

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The job query failed.

POST/api/v1/jobsFull-access key

Not 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.

Request
curl -X POST https://www.servicevin.com/api/v1/jobs \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

501 — Always. There is no success case.
{
  "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

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

429rate_limited

More than 120 requests in 60 seconds on this key.

501not_implemented

Every request.

GET/api/v1/jobs/{id}

Get one job

The same shape the list returns, for one job.

Request
curl https://www.servicevin.com/api/v1/jobs/4a92c7f1-8d36-4b05-9e2a-1f6b3d8c50e7 \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
idrequiredstringpath

The job's id.

200 — The job.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the job.

numberinteger

The shop-visible job number.

titlestring | null

What the job is, e.g. Full-front PPF.

customer_idstring

The customer this job belongs to.

vehicle_idstring | null

The vehicle being worked on, when one is attached.

stagestring

Where the job sits on the board.

One of: lead, scheduled, in_progress, curing, qa, ready, completed, canceled
assignee_idstring | null

Assigned staff — a staff profile id, which is the id from GET /api/v1/staff, not that row's user_id.

scheduled_startstring | null

Booked start, or null while the job is unscheduled.

scheduled_endstring | null

Booked end, or null while the job is unscheduled.

actual_startstring | null

When work really began — stamped the first time the job reaches in_progress or beyond, and never cleared by a move backwards.

actual_endstring | null

When work finished. Re-stamped on every entry into completed, and cleared when the job leaves it.

location_addressstring | null

Where the work happens, for mobile jobs.

created_atstring

When the job was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

404not_found

No job with that id in this shop.

429rate_limited

More than 120 requests in 60 seconds on this key.

PATCH/api/v1/jobs/{id}Full-access key

Move 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.

Request
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

NameInDescription
idrequiredstringpath

The job's id.

Body

The stage to move to.

FieldTypeDescription
stagerequiredstring

The board stage.

One of: lead, scheduled, in_progress, curing, qa, ready, completed, canceled
200 — The job at its new stage.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the job.

numberinteger

The shop-visible job number.

titlestring | null

What the job is, e.g. Full-front PPF.

customer_idstring

The customer this job belongs to.

vehicle_idstring | null

The vehicle being worked on, when one is attached.

stagestring

Where the job sits on the board.

One of: lead, scheduled, in_progress, curing, qa, ready, completed, canceled
assignee_idstring | null

Assigned staff — a staff profile id, which is the id from GET /api/v1/staff, not that row's user_id.

scheduled_startstring | null

Booked start, or null while the job is unscheduled.

scheduled_endstring | null

Booked end, or null while the job is unscheduled.

actual_startstring | null

When work really began — stamped the first time the job reaches in_progress or beyond, and never cleared by a move backwards.

actual_endstring | null

When work finished. Re-stamped on every entry into completed, and cleared when the job leaves it.

location_addressstring | null

Where the work happens, for mobile jobs.

created_atstring

When the job was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

404not_found

No job with that id in this shop.

409conflict

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.

422validation_error

stage was missing or not a board stage.

429rate_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.

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.

Request
curl https://www.servicevin.com/api/v1/appointments?from=2026-08-01T00:00:00Z \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
fromstringquery

ISO 8601 start of the window. Defaults to now.

tostringquery

ISO 8601 end of the window. Defaults to 30 days after from.

stagestringquery

Only this stage. Supplying it also lifts the canceled-jobs exclusion.

One of: lead, scheduled, in_progress, curing, qa, ready, completed, canceled
importedstringquery

Return only records that arrived through a data import. Default lists only records created in Service VIN. 1 and 0 are accepted as aliases; any other value is a 422 rather than a silent fallback, because a caller who typos this has been given a wrong answer about which records exist.

Default: falseOne of: true, false
200 — Appointments in the window, earliest first.
{
  "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

FieldTypeDescription
idstring

The job's id.

job_numberinteger

The shop-visible job number.

titlestring | null

What the work is.

stagestring

Where the job sits on the board.

One of: lead, scheduled, in_progress, curing, qa, ready, completed, canceled
assignee_idstring | null

Assigned staff — a staff profile id (id from GET /api/v1/staff).

scheduled_startstring

Booked start.

scheduled_endstring | null

Booked end, when one is set.

location_addressstring | null

Where the work happens, for mobile jobs.

customer_idstring

The customer.

customer_namestring | null

Inlined so a reminder needs one call.

customer_emailstring | null

Inlined.

customer_phonestring | null

Inlined, E.164.

vehicle_idstring | null

The vehicle, when one is attached.

vehiclestring | null

2024 BMW M3, inlined.

created_atstring

When the job was created.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

422validation_error

from or to was not an ISO 8601 timestamp, or stage was not a board stage.

422validation_error

imported is not true or false (or their 1/0 aliases).

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The appointment query failed.

Messages

Read the message log, and text a customer through the shop's compliant send path.

GET/api/v1/messages

List 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.

Request
curl https://www.servicevin.com/api/v1/messages?direction=inbound \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
customer_idstringquery

Only messages matched to this customer.

conversation_idstringquery

Only messages in this inbox thread.

directionstringquery

inbound is from the customer.

One of: inbound, outbound
limitintegerquery

Rows per page. Values outside 1–100 are clamped; anything unparseable falls back to the default.

Default: 25
cursorstringquery

The previous page's meta.next_cursor. Opaque — decode nothing, pass it back verbatim.

200 — A page of messages, newest first.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the message.

conversation_idstring

The inbox thread this message belongs to.

customer_idstring | null

The customer, when the thread is matched to one.

channelstring

sms, email, call, webchat or whatsapp.

directionstring

inbound is from the customer.

One of: inbound, outbound
bodystring | null

The message text.

fromstring | null

Sending address or number.

tostring | null

Receiving address or number.

statusstring

Delivery status as the provider last reported it.

created_atstring

When the message was recorded.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

422validation_error

direction was not inbound or outbound.

429rate_limited

More than 120 requests in 60 seconds on this key.

POST/api/v1/messagesFull-access key

Text 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.

Request
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.

FieldTypeDescription
customer_idrequiredstring

Must be a customer of this shop, with a phone on file.

bodyrequiredstringmax 1600

The message. 1600 is the carrier ceiling for a concatenated SMS, so longer is refused here rather than by the provider.

channelstring

Only sms today.

Default: smsOne of: sms
201 — The sent message. `id` is null in the rare case where the text went out but the log insert failed — reported truthfully rather than as an error, because a retry would double-text the customer.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the message.

conversation_idstring

The inbox thread this message belongs to.

customer_idstring | null

The customer, when the thread is matched to one.

channelstring

sms, email, call, webchat or whatsapp.

directionstring

inbound is from the customer.

One of: inbound, outbound
bodystring | null

The message text.

fromstring | null

Sending address or number.

tostring | null

Receiving address or number.

statusstring

Delivery status as the provider last reported it.

created_atstring

When the message was recorded.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

404not_found

No customer with that id in this shop.

409conflict

The customer opted out, it is outside the shop's texting hours, or the shop has no working sender. The message says which.

422validation_error

The body was empty or too long, or that customer has no phone number on file.

429rate_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.

GET/api/v1/conversations

List 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.

Request
curl https://www.servicevin.com/api/v1/conversations?status=open \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
statusstringquery

Only threads at this disposition.

One of: open, snoozed, closed
channelstringquery

Only threads on this channel.

One of: sms, email, call, webchat, whatsapp
customer_idstringquery

Only this customer's threads.

assigned_tostringquery

Only threads owned by this teammate.

unassignedstringquery

true for threads nobody has taken. Ignored when assigned_to is given.

One of: true
unreadstringquery

true for threads whose last word was the customer's.

One of: true
limitintegerquery

How many to return, 1-100.

Default: 25
offsetintegerquery

How many to skip. Pass meta.next_offset from the previous response to continue.

Default: 0
200 — A page of threads, most recently active first, with `meta.total`, `meta.omitted` and `meta.next_offset`.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the thread.

customer_idstring | null

The customer, when the thread is matched to one.

channelstring

What the thread runs on.

One of: sms, email, call, webchat, whatsapp
statusstring

The shop's disposition. closed is what Quo calls done; a new inbound reopens a thread either way.

One of: open, snoozed, closed
subjectstring | null

Subject line, on an email thread.

contact_numberstring | null

The number on the other end, when the thread is a text thread.

assigned_tostring | null

The teammate who owns it, or null when nobody has taken it.

pinnedboolean

Pinned to the top of the shop's inbox.

unreadboolean

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_managedboolean

Service VIN's own agent is answering this thread.

last_message_atstring | null

When anything last happened on it.

last_inbound_atstring | null

When the customer last wrote.

last_outbound_atstring | null

When the shop last wrote — including anything sent automatically.

last_human_outbound_atstring | null

When a PERSON at the shop last replied. Automated sends — an after-hours acknowledgement, a missed-call text-back, an automation rule — move last_outbound_at and never this, so last_inbound_at > last_human_outbound_at is the shop's own "still waiting on us" lens. Null when nobody has ever replied by hand.

created_atstring

When it opened.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

422validation_error

status or channel was not one of the listed values.

429rate_limited

More than 120 requests in 60 seconds on this key.

GET/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=….

Request
curl https://www.servicevin.com/api/v1/conversations/{id} \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
idrequiredstringpath

The thread's id.

200 — The thread and its contact's messaging standing.
{
  "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

FieldTypeDescription
conversationobject

The thread record — the same fields the list returns.

customer_idstring | null

The contact behind the thread.

customer_namestring | null

Their display name.

customer_phonestring | null

Their number.

customer_emailstring | null

Their address.

texting_blockedboolean

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_consentboolean

Whether campaigns may reach them. Not the same question as replying.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

404not_found

No thread with that id in this shop.

429rate_limited

More than 120 requests in 60 seconds on this key.

POST/api/v1/conversations/{id}Full-access key

Change 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.

Request
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

NameInDescription
idrequiredstringpath

The thread's id.

Body

Exactly one verb.

FieldTypeDescription
mark_readboolean

Mark the thread read.

mark_unreadboolean

Put it back to unread.

mark_doneboolean

Close it (Quo calls this done).

mark_openboolean

Reopen a closed or snoozed thread.

snoozeboolean

Park it until the next inbound.

pinboolean

Pin it to the top of the inbox.

unpinboolean

Unpin it.

staff_idstring

Hand it to this teammate. They are notified.

unassignboolean

Take it off whoever holds it.

200 — The thread as it now stands.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the thread.

customer_idstring | null

The customer, when the thread is matched to one.

channelstring

What the thread runs on.

One of: sms, email, call, webchat, whatsapp
statusstring

The shop's disposition. closed is what Quo calls done; a new inbound reopens a thread either way.

One of: open, snoozed, closed
subjectstring | null

Subject line, on an email thread.

contact_numberstring | null

The number on the other end, when the thread is a text thread.

assigned_tostring | null

The teammate who owns it, or null when nobody has taken it.

pinnedboolean

Pinned to the top of the shop's inbox.

unreadboolean

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_managedboolean

Service VIN's own agent is answering this thread.

last_message_atstring | null

When anything last happened on it.

last_inbound_atstring | null

When the customer last wrote.

last_outbound_atstring | null

When the shop last wrote — including anything sent automatically.

last_human_outbound_atstring | null

When a PERSON at the shop last replied. Automated sends — an after-hours acknowledgement, a missed-call text-back, an automation rule — move last_outbound_at and never this, so last_inbound_at > last_human_outbound_at is the shop's own "still waiting on us" lens. Null when nobody has ever replied by hand.

created_atstring

When it opened.

Errors

StatusCodeWhen
400invalid_request

The body was not JSON.

401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

404not_found

No thread with that id in this shop.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_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.

GET/api/v1/calls

List 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.

Request
curl https://www.servicevin.com/api/v1/calls?direction=inbound \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
directionstringquery

inbound is the customer ringing the shop.

One of: inbound, outbound
statusstringquery

How the call ended. missed is what a call-back list wants.

One of: ringing, in_progress, completed, missed, failed, canceled
customer_idstringquery

Only this customer's calls.

sincestringquery

ISO instant. Only calls that started at or after it.

untilstringquery

ISO instant. Only calls that started before it.

limitintegerquery

How many to return, 1-100.

Default: 25
offsetintegerquery

How many to skip. Pass meta.next_offset from the previous response to continue.

Default: 0
200 — A page of calls, newest first. `meta.total` is the exact match count, `meta.omitted` how many this page left out, and `meta.next_offset` the offset to ask for next (null when there is no more).
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the call.

conversationstring | null

The inbox thread this call was filed into. Null when the number was never matched to a contact.

customer_idstring | null

The customer, when the caller is on file.

directionstring

inbound is the customer ringing the shop.

One of: inbound, outbound
statusstring

How it ended. ringing and in_progress are calls happening right now, so a report about yesterday should exclude them.

One of: ringing, in_progress, completed, missed, failed, canceled
fromstring

The calling number, E.164.

tostring

The number called, E.164.

started_atstring

When it started.

ended_atstring | null

When it ended. Null while the call is still up.

duration_secondsinteger | null

The whole call, ringing included.

answered_bystring | null

Who or what picked it up — a member of staff, or the AI receptionist.

has_recordingboolean

Audio of the conversation exists. Fetch a signed link from GET /api/v1/calls/{id}/recording.

recording_duration_secondsinteger | null

The recorded part only, which starts when somebody answers — shorter than duration_seconds on a call that rang first.

has_voicemailboolean

The caller left a message. Fetch it from the same endpoint with ?media=voicemail.

voicemail_duration_secondsinteger | null

How long the voicemail runs.

has_transcriptboolean

The words are available from GET /api/v1/calls/{id}/transcript.

has_ai_summaryboolean

Service VIN has analysed this call and produced a recap.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

422validation_error

direction or status was not one of the listed values.

429rate_limited

More than 120 requests in 60 seconds on this key.

GET/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.

Request
curl https://www.servicevin.com/api/v1/calls/{id} \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
idrequiredstringpath

The call's id.

200 — The call.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the call.

conversationstring | null

The inbox thread this call was filed into. Null when the number was never matched to a contact.

customer_idstring | null

The customer, when the caller is on file.

directionstring

inbound is the customer ringing the shop.

One of: inbound, outbound
statusstring

How it ended. ringing and in_progress are calls happening right now, so a report about yesterday should exclude them.

One of: ringing, in_progress, completed, missed, failed, canceled
fromstring

The calling number, E.164.

tostring

The number called, E.164.

started_atstring

When it started.

ended_atstring | null

When it ended. Null while the call is still up.

duration_secondsinteger | null

The whole call, ringing included.

answered_bystring | null

Who or what picked it up — a member of staff, or the AI receptionist.

has_recordingboolean

Audio of the conversation exists. Fetch a signed link from GET /api/v1/calls/{id}/recording.

recording_duration_secondsinteger | null

The recorded part only, which starts when somebody answers — shorter than duration_seconds on a call that rang first.

has_voicemailboolean

The caller left a message. Fetch it from the same endpoint with ?media=voicemail.

voicemail_duration_secondsinteger | null

How long the voicemail runs.

has_transcriptboolean

The words are available from GET /api/v1/calls/{id}/transcript.

has_ai_summaryboolean

Service VIN has analysed this call and produced a recap.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

404not_found

No call with that id in this shop.

429rate_limited

More than 120 requests in 60 seconds on this key.

GET/api/v1/calls/{id}/recording

Get 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.

Request
curl https://www.servicevin.com/api/v1/calls/{id}/recording \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
idrequiredstringpath

The call's id.

mediastringquery

The recorded conversation, or the message the caller left.

Default: recordingOne of: recording, voicemail
ttlintegerquery

Seconds the link should live, 30-900. Larger values are clamped to 900.

Default: 300
200 — A signed URL and the instant it expires.
{
  "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

FieldTypeDescription
callstring

The call the link is for.

kindstring

Which audio the link fetches.

One of: recording, voicemail
urlstring

The signed link. Points at Service VIN, never at the storage provider.

expires_atstring

When the link stops working. The authoritative value — read it.

duration_secondsinteger | null

How long the audio runs.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

404not_found

No call with that id in this shop.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

That call has no audio of the requested kind, or this deployment has no public site URL configured.

GET/api/v1/calls/{id}/transcript

Read 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.

Request
curl https://www.servicevin.com/api/v1/calls/{id}/transcript \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
idrequiredstringpath

The call's id.

200 — The transcript, or nulls when the call has none.
{
  "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

FieldTypeDescription
callstring

The call.

transcriptstring | null

What was said. Null when the call was never transcribed.

sourcestring | null

What produced it — a live capture, or a transcription of the recording.

started_atstring

When the call started.

duration_secondsinteger | null

How long the call ran.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

404not_found

No call with that id in this shop.

429rate_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.

GET/api/v1/tasks

List 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.

Request
curl https://www.servicevin.com/api/v1/tasks \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
statusstringquery

done covers completed and cancelled; all is both blocks.

Default: openOne of: open, done, all
assigned_tostringquery

Only this teammate's tasks.

conversation_idstringquery

Only tasks filed against this thread.

overduestringquery

true for open tasks past their due date. Overrides status.

One of: true
limitintegerquery

How many to return, 1-100.

Default: 25
offsetintegerquery

How many to skip. Pass meta.next_offset from the previous response to continue.

Default: 0
200 — A page of tasks, with `meta.total`, `meta.omitted` and `meta.next_offset`. Answer "how much is on our list" from `meta.total`.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the task.

titlestring

What needs doing, in one line.

notesstring | null

Any detail behind the title.

statusstring

canceled means it will not be done — the row stays, so the decision is visible.

One of: open, done, canceled
prioritystring

How urgent.

One of: low, normal, high
due_atstring | null

When it is due. Null for an undated to-do, which sorts last.

overdueboolean

Open and past its due date. Computed at read time, so it is always current.

completed_atstring | null

When it was finished. Cleared on reopen, so a re-completed task reports the real finish.

created_atstring

When it was created.

conversationstring | null

The inbox thread it belongs to.

conversation_labelstring | null

Display name for that thread's contact.

customer_idstring | null

The customer it is about.

job_idstring | null

The job it hangs off.

message_idstring | null

The thread line the task was created from — the customer's own words rather than a paraphrase.

assigned_tostring | null

The teammate who owns it.

assignee_namestring | null

That teammate's display name.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

422validation_error

status was not open, done or all.

429rate_limited

More than 120 requests in 60 seconds on this key.

POST/api/v1/tasksFull-access key

Create 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.

Request
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.

FieldTypeDescription
titlerequiredstringmax 200

What needs doing, in one line. This is what the shop sees.

notesstringmax 4000

Detail behind the title.

prioritystring

How urgent.

Default: normalOne of: low, normal, high
due_atstring

ISO instant it is due. Resolve "tomorrow" against the SHOP's timezone before sending.

conversationstring

The inbox thread it belongs to.

customer_idstring

The customer it is about.

job_idstring

The job it hangs off.

message_idstring

The thread line that caused it.

staff_idstring

Hand it to this teammate. They are notified.

201 — The created task.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the task.

titlestring

What needs doing, in one line.

notesstring | null

Any detail behind the title.

statusstring

canceled means it will not be done — the row stays, so the decision is visible.

One of: open, done, canceled
prioritystring

How urgent.

One of: low, normal, high
due_atstring | null

When it is due. Null for an undated to-do, which sorts last.

overdueboolean

Open and past its due date. Computed at read time, so it is always current.

completed_atstring | null

When it was finished. Cleared on reopen, so a re-completed task reports the real finish.

created_atstring

When it was created.

conversationstring | null

The inbox thread it belongs to.

conversation_labelstring | null

Display name for that thread's contact.

customer_idstring | null

The customer it is about.

job_idstring | null

The job it hangs off.

message_idstring | null

The thread line the task was created from — the customer's own words rather than a paraphrase.

assigned_tostring | null

The teammate who owns it.

assignee_namestring | null

That teammate's display name.

Errors

StatusCodeWhen
400invalid_request

The body was not JSON.

401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

422validation_error

title was missing, empty or over 200 characters.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The named teammate's role does not grant inbox access, so the task would sit somewhere they cannot see it.

GET/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.

Request
curl https://www.servicevin.com/api/v1/tasks/{id} \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
idrequiredstringpath

The task's id.

200 — The task.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the task.

titlestring

What needs doing, in one line.

notesstring | null

Any detail behind the title.

statusstring

canceled means it will not be done — the row stays, so the decision is visible.

One of: open, done, canceled
prioritystring

How urgent.

One of: low, normal, high
due_atstring | null

When it is due. Null for an undated to-do, which sorts last.

overdueboolean

Open and past its due date. Computed at read time, so it is always current.

completed_atstring | null

When it was finished. Cleared on reopen, so a re-completed task reports the real finish.

created_atstring

When it was created.

conversationstring | null

The inbox thread it belongs to.

conversation_labelstring | null

Display name for that thread's contact.

customer_idstring | null

The customer it is about.

job_idstring | null

The job it hangs off.

message_idstring | null

The thread line the task was created from — the customer's own words rather than a paraphrase.

assigned_tostring | null

The teammate who owns it.

assignee_namestring | null

That teammate's display name.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

404not_found

No task with that id in this shop.

429rate_limited

More than 120 requests in 60 seconds on this key.

POST/api/v1/tasks/{id}Full-access key

Change 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.

Request
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

NameInDescription
idrequiredstringpath

The task's id.

Body

Exactly one change.

FieldTypeDescription
titlestringmax 200

Rewrite the one-line title.

notesstringmax 4000

Replace the notes. An empty string clears them.

prioritystring

Set urgency.

One of: low, normal, high
completeboolean

Mark it done.

reopenboolean

Put a done or cancelled task back to open.

cancelboolean

It will not be done. The row stays.

staff_idstring

Hand it to this teammate. They are notified.

unassignboolean

Take it off whoever holds it.

due_atstring

ISO instant it is due, resolved against the shop's timezone.

remove_due_dateboolean

Clear the due date entirely.

link_conversationstring

Re-file it under this thread.

unlink_conversationboolean

Detach it from its thread.

200 — The task as it now stands.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the task.

titlestring

What needs doing, in one line.

notesstring | null

Any detail behind the title.

statusstring

canceled means it will not be done — the row stays, so the decision is visible.

One of: open, done, canceled
prioritystring

How urgent.

One of: low, normal, high
due_atstring | null

When it is due. Null for an undated to-do, which sorts last.

overdueboolean

Open and past its due date. Computed at read time, so it is always current.

completed_atstring | null

When it was finished. Cleared on reopen, so a re-completed task reports the real finish.

created_atstring

When it was created.

conversationstring | null

The inbox thread it belongs to.

conversation_labelstring | null

Display name for that thread's contact.

customer_idstring | null

The customer it is about.

job_idstring | null

The job it hangs off.

message_idstring | null

The thread line the task was created from — the customer's own words rather than a paraphrase.

assigned_tostring | null

The teammate who owns it.

assignee_namestring | null

That teammate's display name.

Errors

StatusCodeWhen
400invalid_request

The body was not JSON.

401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

404not_found

No task with that id in this shop.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_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.

GET/api/v1/phone-numbers

List 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.

Request
curl https://www.servicevin.com/api/v1/phone-numbers \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
limitintegerquery

How many to return, 1-100. A shop holds a handful.

Default: 50
200 — The shop's lines, primary first.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the line.

numberstring

The number itself, E.164.

namestring | null

The owner's own label — "Front desk", "Mobile crew".

providerstring

Who the line is held with.

statusstring

active can send; pending is a reservation that was never bought; suspended is a billing hold. Released lines are not returned at all.

countrystring | null

Two-letter country code.

is_primaryboolean

The number automations, click-to-call and outbound texts use when nothing names a line — the one a customer sees.

is_sendingboolean

Whether the line can actually send. Not the same as holding it: a suspended line is held and cannot carry a text.

purchased_atstring | null

When the shop bought it.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

429rate_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.

GET/api/v1/services

List 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.

Request
curl https://www.servicevin.com/api/v1/services \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
activestringquery

false lists the retired services instead.

Default: trueOne of: true, false
200 — The service menu.
{
  "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

FieldTypeDescription
idstring

Service VIN's id for the service.

namestring

Menu name.

descriptionstring | null

Longer description.

skustring | null

The shop's own code.

categorystring | null

Category name, inlined.

base_pricenumber | null

List price in the shop's currency.

pricing_modelstring

How the price is worked out.

default_duration_minutesinteger | null

Default booking length.

is_activeboolean

Whether the shop still sells it.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The service query failed.

GET/api/v1/staff

List 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.

Request
curl https://www.servicevin.com/api/v1/staff \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
bookablestringquery

Narrow to staff who can (or cannot) be scheduled.

One of: true, false
200 — The roster.
{
  "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

FieldTypeDescription
idstring

Staff-profile id. This is what jobs.assignee_id points at — use it to assign a JOB.

user_idstring | null

The person's login id, and what leads.assigned_to points at — use it to assign a LEAD. Null for a staff profile with no login, which is why the two ids cannot be used interchangeably.

namestring

Display name, falling back to the login's name and then its email.

is_bookableboolean

Whether they can be scheduled.

skillsstring[]

Skill tags. Empty array when none are set.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The staff query failed.

Webhooks

Subscribe a URL to events, list your subscriptions, unsubscribe.

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.

Request
curl https://www.servicevin.com/api/v1/hooks \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

200 — Every endpoint on the shop.
{
  "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

FieldTypeDescription
idstring

Pass this to DELETE /api/v1/hooks/{id} to unsubscribe.

urlstring

Where deliveries are POSTed.

descriptionstring | null

Label shown in the dashboard.

eventsstring[]

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
activeboolean

Endpoints auto-pause after 10 consecutive delivery failures and are resumed from the dashboard.

consecutive_failuresinteger

Failed deliveries in a row. A single success resets it to 0.

created_atstring

When the subscription was made.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The endpoint query failed.

POST/api/v1/hooksFull-access key

Subscribe 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.

Request
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.

FieldTypeDescription
urlrequiredstringmax 2000

Must be https:// and must resolve to a public host.

eventsstring[]

Which events to receive. Omit it, or send [], to subscribe to everything.

Default: [] (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
201 — The subscription, plus the signing secret — the only time it is returned.
{
  "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

FieldTypeDescription
idstring

Pass this to DELETE /api/v1/hooks/{id}.

urlstring

Where deliveries will be POSTed.

eventsstring[]

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
secretstring

The whsec_… signing secret. Shown once. Store it now — it verifies every delivery.

Errors

StatusCodeWhen
400invalid_request

The body was not valid JSON.

401unauthorized

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.

402plan_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.

403plan_required

Webhooks are a Growth-plan feature and this shop is on a lower plan.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

409conflict

The shop already has 10 endpoints. Delete one first.

422validation_error

url is not https, is not a valid URL, names an unknown event, or resolves to a private/loopback host.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The subscription could not be stored.

DELETE/api/v1/hooks/{id}Full-access key

Unsubscribe

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.

Request
curl -X DELETE https://www.servicevin.com/api/v1/hooks/3e7a1c4e-5b92-4f08-a6d3-8c204f7b1e59 \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY"

Parameters

NameInDescription
idrequiredstringpath

The subscription id from the subscribe response or the list.

204 Removed. No body.

Errors

StatusCodeWhen
401unauthorized

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.

402plan_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.

403org_suspended

The Service VIN account behind this key is suspended. Billing or support resolves it — retrying will not.

403insufficient_scope

The key is read-only and this endpoint writes. Mint a full-access key; the scope is fixed when a key is created.

404not_found

No such subscription on this shop.

429rate_limited

More than 120 requests in 60 seconds on this key.

500internal_error

The delete failed.

OpenAPI spec

The machine-readable version of this page.

GET/api/v1/openapi.jsonNo auth

The 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.

Request
curl https://www.servicevin.com/api/v1/openapi.json

200 — An OpenAPI 3.1 document describing every endpoint above.
{
  "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"
      }
    }
  }
}