narra

REST API · v1

Narra Warranty API

Issue warranties from your POS or inventory system, and read them back. What comes out is the document your counter issues: sealed at the moment it is made, sent to the customer, and verifiable by anyone holding the link.

BASE URLhttps://app.narrawarranty.com/api/v1

Introduction

The API is organised around REST: predictable URLs, JSON bodies, standard verbs and conventional status codes. Every request is made over HTTPS and carries an API key.

Everything it can do, a person can do in the app. It is a second door into the same rooms and never a shortcut past a rule: issuing goes through the same code the counter uses, so a warranty type's required serial, its period and its terms apply exactly as they do to a staff member standing at a till.

The API is on the Shop+ plan. Keys are made in the app under My account → API keys, and the plan is checked on every request rather than when the key was made.

Quickstart

Three calls: find a warranty type, issue against it, read it back.

REQUEST
curl https://app.narrawarranty.com/api/v1/warranty-types \
  -H "Authorization: Bearer nw_your_key"

curl -X POST https://app.narrawarranty.com/api/v1/warranties \
  -H "Authorization: Bearer nw_your_key" \
  -H "Content-Type: application/json" \
  -d '{"warranty_type_id": 77, "product": "Carrier 1.5HP Split Type Aircon",
       "customer": {"name": "Maria Santos", "email": "[email protected]"}}'

curl https://app.narrawarranty.com/api/v1/warranties/MZ7X-X1JQ \
  -H "Authorization: Bearer nw_your_key"

Authentication

Create a key in the app under My account → API keys → New key. It is shown once, so copy it then; if it is lost, revoke it and make another. A key is the whole shop's access and does not expire, so keep it on your server and never in a browser, a mobile app or a repository.

Send it as a bearer token on every request. There is no cookie and no session.

REQUEST
curl https://app.narrawarranty.com/api/v1/warranties \
  -H "Authorization: Bearer nw_your_key"
401 UNAUTHORIZED
{
  "error": {
    "code": "bad_key",
    "message": "That key is not valid."
  }
}

Wrong, revoked and never-existed all answer the same way. Telling them apart would tell somebody holding a stolen key which one they were holding.

Requests & responses

  • Bodies are JSON and want Content-Type: application/json. A body over 1 MB is refused.
  • Unknown fields are rejected, not ignored. A typo in a field name comes back as bad_json rather than silently doing nothing.
  • Dates are YYYY-MM-DD in Manila time. issued_at is RFC 3339 in UTC.
  • Money is in pesos, as a number: 32500 or 32500.50.
  • A reference may be written with or without its dash, in any case: MZ7X-X1JQ and mz7xx1jq reach the same warranty.

Errors

Every failure takes one shape, so you can branch on code instead of reading prose.

RESPONSE
{
  "error": {
    "code": "rejected",
    "message": "Customer name is required."
  }
}
StatusCodeMeans
400bad_jsonThe body did not read as the documented JSON, or named a field that does not exist
401no_keyNo Authorization header
401bad_keyWrong, revoked, or never existed
402plan_requiredThe shop is not on Shop+
404not_foundNo warranty here with that reference
422rejectedThe warranty was refused; the message says what to fix
500server_errorA fault here. Nothing was issued

Limits

  • No rate limit today. There is no request ceiling and no 429. If one is ever needed it will be announced before it is switched on.
  • No idempotency key yet. A call that times out may still have issued the warranty. Look it up by reference or serial before sending it again.
  • limit on the list endpoint defaults to 50 and caps at 200.
  • A plan that lapses stops the key. The shop keeps everything it issued, but calls answer plan_required until it is back on Shop+.

Warranty types

A warranty type carries the terms, the period and what a warranty of that kind must record. Issuing needs one, and its id is not guessable, so this is the call to make first. Types are set up by the shop in the app and their ids are stable.

Endpoints

GET /api/v1/warranty-types

Every active type this shop can issue against.

RESPONSE
{
  "warranty_types": [
    {
      "id": 77,
      "name": "Aircon Unit (Split Type)",
      "category": "product",
      "period_months": 12,
      "period_days": 0,
      "needs_serial": true,
      "needs_amount": true
    }
  ]
}

needs_serial and needs_amount are the type's own rules. Issue without what they ask for and the call comes back rejected.

Warranties

A warranty is signed when it is created and can never be edited afterwards: that is the whole point of it. Validate before you post; a mistake means voiding it in the app and issuing another.

Endpoints

POST /api/v1/warranties

Issue a warranty. Returns 201 with the customer's link.

REQUEST
curl -X POST https://app.narrawarranty.com/api/v1/warranties \
  -H "Authorization: Bearer nw_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "warranty_type_id": 77,
    "product": "Carrier 1.5HP Split Type Aircon",
    "serial": "CAR-2026-88231",
    "amount": 32500,
    "receipt_no": "0012345",
    "note": "Includes one free cleaning after 6 months.",
    "customer": {
      "name": "Maria Santos",
      "email": "[email protected]",
      "mobile": "0917 555 1234"
    }
  }'
FieldRequiredNotes
warranty_type_idyesFrom /warranty-types
productyesWhat was sold
customer.nameyesAs it should read on the document
customer.email
customer.mobile
one of the twoWithout either there is no way to send the warranty, or to tell two customers apart
serialthe type decidesRequired when the type says needs_serial
amountthe type decidesIn pesos. Required when the type says needs_amount
period_months
period_days
noLeft out, the warranty runs for the type's own period
purchased_onnoYYYY-MM-DD. Defaults to today in Manila
receipt_nonoYour own reference, printed on the warranty
notenoThe customer reads it, and it is sealed with the rest
send_emailnoDefaults true. Set false to deliver the link yourself
201 CREATED
{
  "ref": "MZ7X-X1JQ",
  "link": "https://narrawarranty.com/w/HRY525JNQGQXfTj2BZdZrQevDzXG6zQD",
  "product": "Carrier 1.5HP Split Type Aircon",
  "serial": "CAR-2026-88231",
  "customer": "Maria Santos",
  "email": "[email protected]",
  "amount": 32500,
  "purchased_on": "2026-08-18",
  "expires_on": "2027-08-18",
  "note": "Includes one free cleaning after 6 months.",
  "receipt_no": "0012345",
  "status": "active",
  "verify": "https://narrawarranty.com/verify/7f21-9066-94a2",
  "fingerprint": "7f21-9066-94a2",
  "issued_at": "2026-08-18T13:07:31Z"
}
GET /api/v1/warranties

The shop's warranties, newest first, across every branch. Takes limit (default 50, max 200) and offset.

REQUEST
curl "https://app.narrawarranty.com/api/v1/warranties?limit=50&offset=0" \
  -H "Authorization: Bearer nw_your_key"
RESPONSE
{
  "warranties": [
    { "ref": "MZ7X-X1JQ", "product": "Carrier 1.5HP Split Type Aircon", "status": "active" }
  ],
  "limit": 50,
  "offset": 0
}
GET /api/v1/warranties/MZ7X-X1JQ

One warranty by the reference printed on it. The dash is optional and case does not matter.

REQUEST
curl https://app.narrawarranty.com/api/v1/warranties/MZ7X-X1JQ \
  -H "Authorization: Bearer nw_your_key"

The warranty object

FieldNotes
refThe short reference printed on the warranty
linkThe customer's copy. Print it on a receipt or send it yourself
verifyWhere anyone can check the warranty against the shop's record
fingerprintThe seal, in three groups. Present once the warranty is sealed
statusWhere the warranty stands today, computed rather than stored
purchased_on, expires_onCalendar dates, Manila
issued_atWhen it was created. RFC 3339, UTC

status is one of

active expiring expired used voided

A warranty type's category is one of

product service

expiring means inside its last 30 days, and used means the claims it allowed are spent. Both are worked out when you ask, so a warranty read twice a month apart can answer differently without anything having changed.

Before you build

  • A warranty cannot be edited. It is signed at issue and any change would break the seal that makes it worth having.
  • Voiding is not exposed. It cannot be undone and asks for a reason the customer reads, so it stays a decision a person makes in the app.
  • Nobody is recorded as the issuer. A warranty made here belongs to the shop; the counter records the staff member who was standing there, and there was nobody standing here.
  • The email is ours to send. Leave send_email alone and the customer gets the warranty from the shop's name, with your reply-to. Set it false and delivery is entirely yours.
  • No webhooks yet. Nothing is sent to you when a claim is recorded or a warranty expires. Ask if you need it: it is a small thing to add for a real use.

Something you need that is not here? Write to barrera.vincentlloyd@gmail.com and say what you are building.