Documentation
Warranties
warranties.issue
REST
ShippingPOST /api/v1/warranties/issueMCP tool
Livewarranties.issueExposed on: REST API · Shop MCP server · Claude connector · Dash (in-app copilot) · Zapier. Part of the Warranties domain.
Operating contract
Records cover the shop is promising, against a job it has finished or an invoice it has raised. This is a COMMITMENT IN WRITING: once issued, the certificate says what it says, and the shop is held to it.
THE SOURCE IS WHAT AUTHORISES THE WRITE. The customer is resolved FROM the job or invoice you name, which is why there is no customer_id parameter — a warranty cannot be issued against somebody who was not the customer on the work.
coverage is the promise. Write the shop's own words for what is covered, not a generic sentence: this is what a claim is later judged against, and a vague scope is a dispute waiting to happen. If you do not know what this shop covers, read an existing warranty with warranties.certificate first rather than inventing wording.
term_months is how long. ZERO OR OMITTED MEANS A LIFETIME WARRANTY WITH NO EXPIRY — that is a much bigger promise than a short term, so never leave it out because you were unsure.
starts_on defaults to today in the shop's own timezone. expires_on is derived and comes back on the result.
A vehicle, when given, must be that customer's car; a material, when given, must be a SKU in the shop's own catalogue. Both are refused rather than silently dropped.
The warranty number is allocated LAST, after every check, so a refused call never burns a number out of the shop's gap-free counter — which means a retry after a refusal is safe.
Nothing is sent to the customer by this call. The certificate is rendered and delivered separately, by a person.
Who may call it
- Permission
customers.accessThe caller must hold Customers at the ACT level. A read-only dashboard grant on the same section is refused.- Plan
- Every planNo plan gate. Available on every Service VIN plan.
- Retries
noneA repeat genuinely does it twice and no key can make it not. Read before you write; the operating contract says what to read.- Rate class
writeCounted against the write budget, which is tighter than a read.
Input
| Field | Type | Description |
|---|---|---|
| sourcerequired | string | Whether this cover is being issued off a job or off an invoice. One of:job, invoice |
| job_id | stringuuid | The finished job, as returned by jobs.list. Required when source is job. |
| invoice_id | stringuuid | The invoice, as returned by invoices.list. Required when source is invoice. |
| coveragerequired | stringmax 300 chars, min 1 chars | What is covered, in the shop's own words. This is what a claim is judged against. |
| type | string | manufacturer, shop or extended — whose promise this is. Default:"shop"One of: manufacturer, shop, extended |
| term_months | integer0–600 | Months of cover. 0 or omitted means a LIFETIME warranty with no expiry. |
| starts_on | string | When cover begins, YYYY-MM-DD shop-local. Defaults to today. |
| vehicle_id | stringuuid | The car covered, as returned by vehicles.list. Must be this customer's. |
Output
| Field | Type | Description |
|---|---|---|
| warranty_id | string | — |
| warranty_number | string | — |
| customer_id | string | — |
| starts_on | string | — |
| expires_on | string | null | — |
| job_id | string | null | — |
| invoice_id | string | null | — |
Examples
Built from this capability's own schema — required fields and the ones carrying a default, and nothing invented. Paste one and it validates.
export SERVICEVIN_API_KEY=svk_live_…
curl -X POST https://www.servicevin.com/api/v1/warranties/issue \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"source":"job","coverage":"…","type":"shop"}'const res = await fetch("https://www.servicevin.com/api/v1/warranties/issue", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SERVICEVIN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"source": "job",
"coverage": "…",
"type": "shop"
}),
});
// Success and failure are both envelopes. Switch on error.code, never
// on error.message — the codes are stable, the messages are for people.
const payload = await res.json();
if (!res.ok) throw new Error(payload.error.code);
const data = payload.data;import os, requests
res = requests.post(
"https://www.servicevin.com/api/v1/warranties/issue",
headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
json={
"source": "job",
"coverage": "…",
"type": "shop"
},
timeout=30,
)
payload = res.json()
if not res.ok:
raise RuntimeError(payload["error"]["code"])
data = payload["data"]{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "warranties.issue",
"arguments": {
"source": "job",
"coverage": "…",
"type": "shop"
}
}
}Refusals
The four gates run in this order on every surface, and the order is not arbitrary — see Authentication.
| Status | Code | When |
|---|---|---|
| 404 | not_found | The id is unknown, or the feature is not enabled for this account. Deliberately the same answer for both. |
| 403 | forbidden | This login does not hold customers.access. |
| 403 | insufficient_scope | The credential is read-only and this capability writes. |
| 422 | validation_error | An argument was wrong. The message names the field. |
| 429 | rate_limited | Too many write calls. Back off and retry. |
| 500 | internal_error | Something failed on our side. Nothing was changed. |