Warrantora API, as shipped in the download
Warrantora API
A warranty and RMA register, over HTTP and over MCP. Everything runs against your installation — no Warrantora cloud, no relay, no telemetry.
The one thing to know before you call it
Expiry is computed, never stored. There is no expiry column on the items table, and no endpoint accepts one. You send a sale date and a term; expires and days_left come back derived. Correct the sale date, or edit the term, and every affected item's expiry moves with it — which is the point. A register whose stored expiry disagreed with its own inputs would be worse than no register.
Whether a claim was in warranty is decided on the day it was RAISED and frozen onto the claim. A claim raised inside the term stays inside the term however long the assessment takes, because letting the clock run out during your own assessment would be a way of rejecting claims by being slow.
Authentication
Mint a key in Settings → API & webhooks. It is shown once and stored as a SHA-256 hash.
Authorization: Bearer apk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Key scope — read or full
Every write checks the key holder's role, exactly as the screen does: a viewer's key cannot register an item, raise a claim or move one on (403 forbidden).
| Scope | What it can do |
|---|---|
read | GET, HEAD and OPTIONS. Any other method is refused with 403 at the door, before a route runs. |
full | Everything the key's user role allows. |
Enforced once, in Api::requireKey(), rather than in each route — the route that forgets is the one that would leak. On /mcp the same scope hides write tools from tools/list and refuses them by name on tools/call. A key never exceeds its user's role: **approving or rejecting a claim needs an administrator key**, because it changes what a customer is entitled to.
curl -H "Authorization: Bearer apk_xxxx" https://warrantora.example.com/api/ping
# {"ok":true,"user":"Ruth","app":"warrantora","version":"1.0.5"}
Endpoints
The machine-readable spec is at /api/openapi.json.
| Method | Path | What it does | ||||
|---|---|---|---|---|---|---|
| GET | /api/ping | Verify a key | ||||
| GET | /api/warranties | The terms you offer | ||||
| GET | /api/items | The register. `?status=active\ | expiring\ | expired\ | void\ | unknown` |
| POST | /api/items | Register a sold item (product, sold_date required) | ||||
| GET | /api/items/{id} | One item, its computed expiry, and its claims | ||||
| GET | /api/expiring | What lapses within 30, 60 and 90 days | ||||
| GET | /api/claims | Claims. `?status=raised\ | assessed\ | approved\ | rejected\ | resolved` |
| POST | /api/claims | Raise a claim (item_id, fault required) | ||||
| GET | /api/claims/{id} | One claim with its item and its return authorization | ||||
| POST | /api/claims/{id}/advance | Move it along — a reason is required | ||||
| POST | /api/sales | (1.1) Register what was sold at the till: {"order_ref":"V-10231","sold_date":"2026-09-20","customer_name":"…","customer_email":"…","lines":[{"product":"…","model":"…","serials":["…"]},{"product":"…","quantity":2}]}. One item per serial, else per quantity (up to 500 units); the term is the line's warranty_id, else the first sale rule matching the product or model, else the default. Sending the same order_ref again registers nothing twice. Needs item.write | ||||
| GET | /api/reports/faults | (1.1) Claims per 100 registered units: `?by=product\ | batch\ | model&from=YYYY-MM-DD&to=YYYY-MM-DD` |
Claims a customer raised on the warranty page (1.1) appear here like any other, with raised_by ending "via the warranty page". The page itself (/warranty) is not part of the API: it identifies an item by serial and registered email, never by key.
A decision without a reason is refused
curl -X POST https://warrantora.example.com/api/claims/12/advance \
-H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
-d '{"to":"rejected","reason":""}'
# 409 {"error":"conflict","detail":"Write what you found, and why. A decision with no reason
# on it is the one that cannot be defended later."}
That sentence comes from Warranty::advanceRefusal() — the same function the browser form, the API and the MCP tool all call. One rule, three doors.
When the customer asked for text updates on the warranty page and you have switched texts on, an approval, a rejection or closing the claim also sends the customer one text, whichever of the three doors moved it (outside your sending hours, or past the day's limit of texts, it waits). The response does not change; what was sent, waited or failed is in the outbox under Settings.
days_left can be null, and null is not zero
An item with no sale date has expires: "" and days_left: null, and its status is unknown. That means the register cannot answer, not that the cover ends today. Treat null as unknown.
Connect it to an assistant
Mint the key in the app first: Settings → API & webhooks. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.
Every client needs the same three facts, and nothing in the handshake is vendor-specific:
| The address | https://warrantora.example.com/mcp |
| The key | header Authorization: Bearer apk_xxxx |
| The transport | MCP over streamable HTTP, stateless |
Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:
claude mcp add --transport http warrantora https://warrantora.example.com/mcp \
--header "Authorization: Bearer apk_xxxx"
ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):
{
"type": "mcp",
"server_label": "warrantora",
"server_url": "https://warrantora.example.com/mcp",
"authorization": "apk_xxxx",
"require_approval": "never"
}
**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"*. Pasted as-is, a block written for another client will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.
A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:
curl -X POST https://warrantora.example.com/mcp \
-H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Warrantora implements the protocol, not an integration with a particular vendor.
Tools
| Tool | Writes? | What it does |
|---|---|---|
list_items | no | The register, with computed expiry and days left |
expiring_report | no | Counts by 30/60/90 band, plus the next ten to lapse |
list_claims | no | Claims with status, in-warranty flag and outcome |
claim_detail | no | One claim with its item, RMA and evidence count |
register_item | yes | Put a sold item on the register |
raise_claim | yes | Raise a claim; in-warranty is frozen from the raised date |
advance_claim | yes | Move a claim along; a reason is required, and deciding needs an admin key |
There is deliberately no void tool, no delete tool and no terms tool. Voiding a registration and deleting records are how a register stops being evidence; changing the terms you offer re-dates every item on them. Those stay human clicks. An agent can tell you what is lapsing and draft an assessment — it cannot rewrite your cover.
Webhooks
Add them in Settings → API & webhooks. Each delivery carries X-Warrantora-Signature: sha256=<hmac> over the raw body.
| Event | Fires when |
|---|---|
item.registered | something is added to the register, from any door |
claim.raised | a claim is opened |
claim.assessed | the bench assessment is recorded |
claim.approved / claim.rejected | a decision is recorded |
claim.resolved | a claim is closed out |
Calendar feed
GET /feed/expiring.ics?t=<token> — the next year of warranty end dates, token-guarded and revocable from the feeds page. It carries the product and the serial and **never the customer's name or email**, because a calendar subscription ends up on somebody's phone.
Errors
| Code | Means |
|---|---|
| 401 | missing or unknown key |
| 403 | the key is read-only and this is a write, or the role may not decide |
| 404 | no such record |
| 409 | refused: not a legal move, or a decision with no reason |
| 422 | bad input (an impossible date, a missing field) |
Limits, honestly
- No pagination beyond the caps. Lists return the newest 500. A cursor nobody needs is a cursor nobody tests.
- No rate limiting on the API. It is your server; the login has one, the API does not.
- No webhook retries. One attempt, recorded either way. Re-read the record rather than expecting a replay.
- No endpoint issues an RMA or uploads evidence. Both are desk work in the app.
Connect it to Zapier, Make or n8n
Warrantora talks to other tools without a plugin: it sends a webhook (an HTTPS message) the moment something happens, and it accepts calls to the API above. Zapier, Make and n8n can use both.
Before you start
- In Warrantora, open Settings → API & webhooks. Mint an API key (choose Read only if the other tool only reads), and add a webhook when you have its URL (below).
- Use your install's full address, for example
https://warrantora.yourcompany.com.
When something happens in Warrantora: send it to another tool
- In the other tool, create the trigger that gives you a URL to receive webhooks: - Zapier: the "Webhooks by Zapier" app, trigger Catch Hook (Webhooks by Zapier needs a paid Zapier plan). - Make: module Webhooks → Custom webhook. - n8n: the Webhook node.
- Copy that URL into Warrantora under Settings → API & webhooks, and choose the event or events below.
- Make the event happen once in Warrantora, so the other tool receives a sample and you can map its fields.
| Event | Fires when |
|---|---|
item.registered | Something is added to the register, from any door |
claim.raised | A claim is opened |
claim.assessed | The bench assessment is recorded |
claim.approved | A decision is recorded |
claim.rejected | A decision is recorded |
claim.resolved | A claim is closed out |
Every delivery carries X-Warrantora-Signature, an HMAC-SHA256 signature over the raw body. Zapier and Make accept the delivery without checking it, so keep the webhook URL private. In n8n you can check it in a Code node before using the data. Warrantora makes one delivery attempt and records the result either way; if the other tool was down, re-read the record through the API.
When something happens elsewhere: act in Warrantora
- Zapier: "Webhooks by Zapier", action POST (or Custom Request).
- Make: module HTTP → Make a request.
- n8n: the HTTP Request node.
Send the request to your install's address plus the path from the Endpoints table above, with the header Authorization: Bearer <your API key> and a JSON body. A read-only key cannot write.
The endpoints that change something:
POST /api/items: Register a sold item (product,sold_daterequired)POST /api/claims: Raise a claim (item_id,faultrequired)POST /api/claims/{id}/advance: Move it along — a reason is requiredPOST /api/sales: (1.1) Register what was sold at the till:{"order_ref":"V-10231","sold_date":"2026-09-20","customer_name":"…","customer_email":"…","lines":[{"product":"…","model":"…","serials":["…"]},{"product":"…","quantity":2}]}. One item per serial, else per quantity (up to 500 units); the term is the line'swarranty_id, else the first sale rule matching the product or model, else the default. Sending the sameorder_refagain registers nothing twice. Needsitem.write
Example recipes
- item.registered → a row in Google Sheets: webhook trigger on
item.registered(Something is added to the register, from any door), then the Google Sheets "Create row" action with the fields you want. - claim.raised → a message in Slack or Teams: webhook trigger on
claim.raised(A claim is opened), then the Slack or Microsoft Teams "Send message" action. - A new form entry or row elsewhere → Warrantora: the other tool's trigger (for example a new Google Sheets row or a form submission), then an HTTP action calling
POST /api/items(Register a sold item (product,sold_daterequired)).
These recipes use only the events and endpoints documented above. We have not run them inside Zapier, Make or n8n ourselves; Warrantora's own automated tests check its API.