Spec Savvy Partner API

Reference for customers integrating their own POS / lab management software with Spec Savvy. Share this page with the customer along with their API key.

Authentication

Every request must include the customer's API key in the X-API-Key header.

X-API-Key: ss_a7f3...e91c

Get the key from the Customer Detail panel (Customers › Details › Generate Key). Regenerating creates a new key and invalidates the old one immediately.

Endpoints at a glance

MethodPathPurpose
GET /api/v1/me Account info + balance + status
GET /api/v1/products Lens catalog with availability matrix
GET /api/v1/products/:idSingle lens detail
GET /api/v1/coatings Master coating catalog
GET /api/v1/colours Master colour / tint catalog
GET /api/v1/frames Frames we supply, with your price per frame
POST /api/v1/lens-orders Submit a new order (optionally with inline shape file)
POST /api/v1/lens-orders/:id/shape Upload a tracer / shape file (.oma or .vca)
GET /api/v1/orders List your orders + tracking info
GET /api/v1/orders/:id Single order detail
POST /api/v1/orders/:id/cancelCancel an order before it enters production

Catalogs (products, coatings, colours) refresh nightly from the lab. Order tracking refreshes every 4 hours.

Email notifications

Two automatic emails go to the address on your account:

If your POS already has its own polling/notification, ask us to turn either off — they're toggled per partner on our side.

GET/api/v1/me

Verify the API key works and pull this customer's account header — name, contact info, current balance, paused state. Useful as the data source for an "Account" widget in the partner's POS.

curl https://specsavvy.ca/api/v1/me \
  -H "X-API-Key: ss_a7f3...e91c"
{
  "username": "fantasticglasses",
  "business_name": "Fantastic Glasses Inc.",
  "email": "[email protected]",
  "phone": "905-392-7656",
  "customer_number": "12345",
  "is_blocked": false,
  "blocked_reason": null,
  "outstanding_balance": 327.50
}

GET/api/v1/products

List all lens products available to order with their Spec Savvy product_id and the availability matrix for each (which indices, coatings, colours, corridors and absorptions can actually be ordered).

Use the matrix to populate dropdowns in your software and show only valid combinations. The server will also reject any order that uses a value not in the matrix (HTTP 400).

curl https://specsavvy.ca/api/v1/products \
  -H "X-API-Key: ss_a7f3...e91c"

The available_options object reflects what's actually orderable for this product — populated from the lab's live catalog (re-synced nightly) and trimmed by Spec Savvy admin. The order endpoint rejects any combo not listed here.

[
  {
    "product_id": 1,
    "display_name": "Peritus Essentials",
    "category": "Progressive",
    "tier": "Everyday",
    "description": "Budget / first-time wearers",
    "available_options": {
      "index": ["1.50", "1.53", "1.56", "1.59 Poly", "1.60", "1.67", "1.74"],
      "coating": [
        "Achromatic Premium AR", "Anti-Fog Pro", "Blue Filter AR",
        "Hard Coat", "Premium AR", "Premium Blue Filter AR", "Standard AR"
      ],
      "colour": [
        "Clear",
        "Nupolar Brown", "Nupolar Gradient Brown", "Nupolar Gradient Grey",
        "Nupolar Green", "Nupolar Grey",
        "Nupolar Mirror Blue", "Nupolar Mirror Gold", "Nupolar Mirror Silver",
        "PhotoX Amethyst", "PhotoX Brown", "PhotoX Green", "PhotoX Grey",
        "PhotoX Pink", "PhotoX Sapphire",
        "Photochromic Brown", "Photochromic Grey",
        "Polar Grey", "Polarized Yellow",
        "Transitions Brown", "Transitions Grey",
        "Transitions Xtractive Brown", "Transitions Xtractive Green",
        "Transitions Xtractive Grey", "Transitions Xtractive Polar Grey",
        "Trivex"
      ]
    }
  },
  {
    "product_id": 17,
    "display_name": "Peritus VisionGuard",
    "category": "Myopia Control",
    "tier": "Specialty",
    "description": "Peripheral defocus design — slows myopia progression in kids & teens.",
    "available_options": {
      "index": ["1.59 Poly"],
      "coating": ["Premium AR", "Standard AR"],
      "colour": ["Clear"]
    }
  },
  ...
]

How to read available_options

Important: index, colour, and coating are convenience aggregates — the union of every value the lens supports across all materials. They are NOT a Cartesian product. Always validate combinations against combos_by_index.

If a key is missing or empty, that dimension is unconstrained for the product. If available_options itself is null, no validation constraints apply (rare).

Example combos_by_index with power_ranges

"combos_by_index": {
  "1.50": {
    "colours": ["Clear", "PhotoX Brown", "PhotoX Grey", "Transitions Brown", ...],
    "coatings": ["Hard Coat", "Standard AR", "Premium AR", ...],
    "power_ranges": {
      "sphere":   { "min": -20.00, "max": 12.00, "step": 0.25, "count": 129 },
      "cylinder": { "min": -6.00,  "max": 0.00,  "step": 0.25, "count": 25 },
      "axis":     { "min": 0,      "max": 180,   "step": 1,    "count": 181 },
      "add":      { "min": 0.75,   "max": 4.00,  "step": 0.25, "count": 14 },
      "prism1":   { "min": 0.5,    "max": 8.0,   "step": 0.25 },
      "prism2":   { "min": 0.5,    "max": 8.0,   "step": 0.25 }
    }
  }
}

Validation happens per-(product, index, power). Sending r_sph: -22 against a product whose 1.67 sphere max is -20 returns a 400 with "sph -22 is below the minimum -20 for index 1.67".

Example combos_by_index (Peritus Pura)

"combos_by_index": {
  "1.50":      { "colours": ["Clear", "Nupolar Brown", ..., "PhotoX Green", ...], "coatings": [...] },
  "1.59 Poly": { "colours": ["Clear", "Nupolar Brown", "PhotoX Green", ...],      "coatings": [...] },
  "1.60":      { "colours": ["Clear", ..., "PhotoX Green", ...],                  "coatings": [...] },
  "1.67":      { "colours": ["Clear", ..., "PhotoX Green", ...],                  "coatings": [...] },
  "1.74":      { "colours": ["Clear", "PhotoX Brown", "PhotoX Grey",
                              "Transitions Brown", "Transitions Grey"],            "coatings": [...] }
}

Note: the aggregate colour list for Pura includes "PhotoX Green" (because it's offered in 1.50/1.59/1.60/1.67) AND the aggregate index list includes "1.74" — but 1.74 + PhotoX Green is NOT in combos_by_index["1.74"], because the lab doesn't actually offer that exact combo. The order endpoint will return 400 if you try.

GET/api/v1/products/:id

Same data as /api/v1/products, but for a single product. Useful if you cache the catalog and just need to refresh one entry, or to render a product detail screen.

curl https://specsavvy.ca/api/v1/products/1 \
  -H "X-API-Key: ss_a7f3...e91c"
Compatibility check examples:
  • "Is PhotoX Green offered on Peritus Pura in 1.74?" — pull /api/v1/products/8, look at available_options.combos_by_index["1.74"].colours. If "PhotoX Green" is in that array, yes. (For Pura specifically, 1.74 only comes in Clear / PhotoX Brown / PhotoX Grey / Transitions Brown / Transitions Grey — so this combo is NOT offered, and the order endpoint will reject it with a clear message.)
  • "Can I order Polarized Yellow on Peritus VisionGuard?" — pull /api/v1/products/17; colour is ["Clear"] only, so no — the order endpoint will return 400.
  • "What materials does Peritus Apex SV come in?" — pull /api/v1/products/{n}, read available_options.index.
The order endpoint enforces all of this server-side — if a combo isn't in available_options, you get a clear 400 response naming the field and listing what IS allowed.

GET/api/v1/coatings

Master coating catalog. Use these for dropdown labels and tooltips in your POS. The exact coatings allowed per product still come from /api/v1/products[*].available_options.coating.

curl https://specsavvy.ca/api/v1/coatings \
  -H "X-API-Key: ss_a7f3...e91c"
[
  {
    "code": "AR_PREM",
    "name": "Premium AR",
    "description": "Premium AR with hydrophobic, oleophobic and anti-static layers...",
    "category": "Anti-Reflective",
    "available_indices": null
  },
  {
    "code": "AR_DRIVE",
    "name": "Driving AR",
    "description": "AR optimised for night driving — reduces oncoming headlight glare.",
    "category": "Anti-Reflective",
    "available_indices": ["1.50","1.59","1.60","1.67"]
  },
  ...
]

If available_indices is non-null, that coating is only offered on those lens materials. The order endpoint enforces this — submitting "Driving AR" with index 1.74 returns 400.

You can submit either the code or the name in r_coating / l_coating; the server normalises to the canonical name.

GET/api/v1/frames

The frames we supply, one row per model and colour, with your price for the frame (its tier). Order one as a frame + lens package with lab_supplied_frame: true and its frame_brand, frame_model and frame_colour.

curl "https://specsavvy.ca/api/v1/frames?limit=500&page=1" \
  -H "X-API-Key: ss_a7f3...e91c"
{
  "frames": [
    {
      "frame_id": 1042,
      "brand": "Merci Merci",
      "model": "MM001",
      "colour": "C4",
      "kind": "optical",
      "tier": "standard",
      "price": 25,
      "eye_size": 55, "bridge": 18, "temple": 140, "b_size": 36, "ed": null,
      "image_url": "https://specsavvy.ca/frames/img/mm001-c4.jpg",
      "available": true,
      "updated_at": "2026-11-02T10:15:00.000Z"
    },
    ...
  ],
  "total": 3712, "page": 1, "pages": 8
}

Without updated_since you get every available frame. With updated_since=2026-11-01 you get every frame changed since then, including frames we no longer supply (available: false) so you can take them off sale. limit is 1 to 2000 (default 500). model=MM001 returns that model's colours, available or not. In each row kind is optical or sun and tier is budget, standard or premium.

GET/api/v1/colours

Master colour / tint catalog — solid tints, gradients, photochromics, polarized, therapeutic FL-41, fashion tints. Submit either code or name in r_colour / l_colour.

[
  { "code": "CLEAR",       "name": "Clear",                "description": "No tint.",                                  "category": "Clear" },
  { "code": "PHOTO_BROWN", "name": "Photox Brown",         "description": "Photochromic — clear indoors, brown in sun.","category": "Photochromic" },
  { "code": "FL41_ROSE_25","name": "FL-41 Rose 25% VLT",   "description": "Therapeutic FL-41 rose tint, 25% transmission.","category": "Therapeutic FL-41" },
  ...
]

POST/api/v1/lens-orders

Submit a new lens order. The order goes straight into the Spec Savvy queue, auto-attributed to your account.

Required field

FieldTypeNotes
wearer_infostringYour reference: last name + tray (e.g. Smith-1234) or your own job number. No full names. Up to 40 characters. Your Spec Savvy customer number is auto-prepended.

Per-eye fields — right (r_) / left (l_)

FieldNotes
r_enabled / l_enabledboolean — set false to skip that eye.
r_product_id / l_product_idSpec Savvy product_id from /api/v1/products.
r_indexLens index (e.g. 1.50, 1.59, 1.67, 1.74).
r_coatingCoating code or name.
r_corridorProgressive corridor (mm).
r_colourTint / photochromic / polarized colour name (must be in GET /api/v1/colours).
r_absorptionTint depth — only valid for solid-tint colours (Grey, Brown, G15, Blue, Pink, Yellow). Allowed values: 15%, 35%, 50%, 65%, 75%, 85%, 95%, 25/0%, 75/30%, 90/40% (gradient tints use the slash form). For Night Driving tint or Custom tint colours, send the same string as the colour. Leave blank for all other colour types.
r_sphSphere (e.g. -2.50).
r_cylCylinder.
r_axisAxis (0–180).
r_addAdd power (progressives / bifocals).
r_prism1 / r_base1 / r_prism2 / r_base2Prism + base direction.
r_pdPupillary distance (mm).
r_fhFitting height (mm). Alias accepted: r_height.
r_diameterLens blank diameter (mm). Auto-calculated server-side when omitted using MBS = (A + DBL − 2·PD) + ED + safety + (B − Ht)/2 — pulls A from lens_width, B from lens_height, DBL from bridge_width, Ht from r_fh, ED from frame_ed (falls back to √(A²+B²)). Safety auto-bumps for high prescriptions.
r_et / r_ctEdge thickness / centre thickness (mm).
r_base_curveBase curve.
r_decentrationDecentration (mm).

Shared fitting fields (apply to both eyes)

pantoscopic_tiltDegrees. Alias: pano_tilt.
wrap_angleFrame wrap angle (degrees).
vertex_distanceVertex distance (mm). Alias: vertex.
reading_distanceReading working distance (cm/mm).
automatic_insetAuto inset toggle / value.

Frame fields

FieldNotes
lab_supplied_frameboolean — set true if the frame should be pulled from Spec Savvy's stocked inventory rather than supplied by you. Defaults to false.
frame_brand / frame_model / frame_colour / frame_sizeFrame identification. When lab_supplied_frame is true, these values are used to locate the frame in Spec Savvy's stock.
frame_skuOptional internal SKU when known. Speeds up the lab-supplied-frame match.
frame_typeFull / Half / Rimless. Default: Full.
lens_width / lens_height / bridge_widthFrame box dimensions (mm). A / B / DBL respectively. Sending all three plus r_pd/l_pd lets us auto-calculate the blank diameter; sending frame_ed too makes the calc more accurate.
frame_edMeasured effective diameter of the frame (mm). Optional — when present, used directly in the blank-size formula; when absent, we fall back to √(A² + B²) (worst-case rectangular bounding diagonal — overestimates for rounded frames).
shapeFrame shape code 1–8 (preset shapes) or ownshape when supplying a custom tracer file (see below).
shape_file_base64 / shape_file_nameOptional inline tracer upload. Base64-encode the .oma or .vca tracer contents, send both fields, and we attach it to the order automatically. Max 1 MB. Setting these also sets shape to ownshape.
Lab-supplied frames: set lab_supplied_frame: true and provide as much frame identification as you can (frame_brand, frame_model, frame_colour, frame_size, plus frame_sku if you know it). The Spec Savvy lab will pull the frame from stock and fit your prescription.

Three order types — pick the one that matches your workflow

TypeFrame sourceTracer needed?What we ship back
packageSpec Savvy stockNoFinished pair, lenses already cut + mounted in the frame.
cut_lensSpec Savvy stock or your frameOnly when frame is yours (.oma/.vca)Lenses already cut to the frame outline. You mount them.
uncut_lensYoursNoRound lens blanks (plus diameter from r_diameter/l_diameter or auto-calculated). You edge / mount yourself.

If you don't send order_type, we infer it from lab_supplied_frame and the presence of a tracer file. Sending it explicitly is recommended so the routing is obvious in your audit log.

Automatic fallback: if you submit a package order for a frame Spec Savvy doesn't stock, we'll fall back to a cut_lens order — but only if you also attached a tracer. If neither matches, the order is rejected with a clear error.

POST/api/v1/lens-orders/:id/shape

Upload a tracer / shape file for an existing order. Use this if you didn't include shape_file_base64 inline at order creation. The order must still be in Pending state (not yet filed with the lab).

Content-Type: multipart/form-data
Field name: shape (the file)
Max size: 1 MB
Accepted extensions: .oma or .vca (industry-standard tracer formats)

curl -X POST https://specsavvy.ca/api/v1/lens-orders/1247/shape \
  -H "X-API-Key: ss_a7f3...e91c" \
  -F "shape=@/path/to/tracer.oma"
{ "ok": true, "filename": "tracer.oma", "size": 18432 }

Misc

serviceService level / turnaround.
order_typeOne of package (Spec Savvy frame + lenses fitted), cut_lens (we cut and edge the lens to your frame using a frame from our catalogue or your uploaded tracer), or uncut_lens (we send blanks back, you cut at your end). Optional — when omitted we infer: lab_supplied_frame=true → package; shape_file_base64/shape_file_path present → cut_lens; otherwise uncut_lens.
order_modediameter (default) or boxed.
important_info / other_infoShort admin notes.
notesFree-text notes — anything the lab should know.

Example — single vision Rx, OD only, customer-supplied frame

curl -X POST https://specsavvy.ca/api/v1/lens-orders \
  -H "X-API-Key: ss_a7f3...e91c" \
  -H "Content-Type: application/json" \
  -d '{
    "wearer_info": "31985-T12",
    "r_enabled": true,
    "r_product_id": 11,
    "r_index": "1.59",
    "r_sph": "-2.25",
    "r_cyl": "-0.75",
    "r_axis": "180",
    "r_pd": "31",
    "r_fh": "22",
    "l_enabled": false,
    "frame_brand": "Ray-Ban",
    "frame_model": "RB5184",
    "lens_width": "54", "bridge_width": "17",
    "notes": "Customer prefers light tint"
  }'
{
  "ok": true,
  "order_id": 1247,
  "submitted_at": "2026-04-29T14:13:55.291Z"
}

Example — full pair, progressive, LAB-SUPPLIED frame

curl -X POST https://specsavvy.ca/api/v1/lens-orders \
  -H "X-API-Key: ss_a7f3...e91c" \
  -H "Content-Type: application/json" \
  -d '{
    "wearer_info": "31986-T14",
    "r_product_id": 1, "r_index": "1.59",
    "r_sph": "+1.50", "r_cyl": "-0.50", "r_axis": "85",
    "r_add": "+2.00", "r_pd": "32", "r_fh": "18",
    "l_product_id": 1, "l_index": "1.59",
    "l_sph": "+1.75", "l_cyl": "-0.25", "l_axis": "95",
    "l_add": "+2.00", "l_pd": "32", "l_fh": "18",
    "lab_supplied_frame": true,
    "frame_brand": "Oakley",
    "frame_model": "OX8156",
    "frame_colour": "Satin Black",
    "frame_size": "54-17-140",
    "frame_sku": "OX8156-SBK-54"
  }'

Validation rules — what gets rejected

Orders are validated strictly. The server returns 400 with a clear message if any of the following fails:

Errors

StatusMeaning
401Missing or invalid X-API-Key.
403Account is paused — contact Spec Savvy.
400Validation failed (see rules above) or required field missing. Message names the offending field and lists what's allowed.
409Order can no longer be cancelled (already in production / shipped).
500Server error. Retry later; if it persists, contact us.

GET/api/v1/orders

List your submitted orders, newest first, with their current production / shipping status. Use this to keep your own system in sync — poll periodically or query when a customer asks "where's my order?".

Query params (all optional):

limitMax rows to return. Default 50, max 200.
sinceISO date string. Only return orders created on or after this date.
curl "https://specsavvy.ca/api/v1/orders?limit=20&since=2026-04-01" \
  -H "X-API-Key: ss_a7f3...e91c"
{
  "count": 2,
  "orders": [
    {
      "order_id": 1247,
      "reference": "31985-T12",
      "order_date": "2026-04-29T14:13:55.291Z",
      "last_updated": "2026-04-30T09:02:11.000Z",
      "status": "Processing",
      "tracking_status": null,
      "supplier_order_number": "SS-A12-44871",
      "tracking_number": null,
      "tracking_url": null,
      "estimated_delivery": "2026-05-05",
      "last_synced_at": "2026-04-30T09:02:11.000Z"
    },
    {
      "order_id": 1239,
      "reference": "31984-T09",
      "order_date": "2026-04-28T16:40:11.111Z",
      "last_updated": "2026-04-30T08:15:00.000Z",
      "status": "Shipped",
      "tracking_status": null,
      "supplier_order_number": "SS-A12-44862",
      "tracking_number": "1Z9999999999999999",
      "tracking_url": "https://www.ups.com/track?tracknum=1Z9999999999999999",
      "estimated_delivery": "2026-05-02",
      "last_synced_at": "2026-04-30T08:15:00.000Z"
    }
  ]
}

POST/api/v1/orders/:id/cancel

Cancel an order — only allowed while status is Pending AND the order hasn't been filed with the lab yet. Returns 409 once production has begun (status advanced to Processing or later). Successful cancellation moves the order to status: "Cancelled".

curl -X POST https://specsavvy.ca/api/v1/orders/1247/cancel \
  -H "X-API-Key: ss_a7f3...e91c"
{ "ok": true, "order_id": 1247, "status": "Cancelled" }

GET/api/v1/orders/:id

Single-order detail. :id is the order_id returned when the order was submitted. 404 if the order doesn't exist or doesn't belong to your account.

curl https://specsavvy.ca/api/v1/orders/1247 \
  -H "X-API-Key: ss_a7f3...e91c"

Order response fields

FieldNotes
order_idThe Spec Savvy order ID. Use this to look up or query later.
referenceYour reference: the wearer_info string you originally submitted.
order_dateISO timestamp when the order was placed.
last_updatedISO timestamp of the most recent change.
statusOne of: Pending, Processing, Shipping, Shipped, Completed, Cancelled, On Hold. Pending means we've received the order but haven't started production; Processing means the lab is working on it; Shipping/Shipped indicate the order is leaving the lab (tracking number populated when set to Shipped); Completed = delivered / closed.
tracking_statusOptional granular fulfillment note. null unless the lab has flagged something specific.
supplier_order_numberThe lab's internal reference number for this job, when assigned. null until the order is filed.
tracking_numberCarrier tracking number once the order ships. null before then.
tracking_urlDirect URL to the carrier's tracking page (if provided alongside the tracking number).
estimated_deliveryDate string (YYYY-MM-DD) when set by the lab. null until quoted.
last_synced_atISO timestamp of the last time Spec Savvy refreshed status from the lab. Updated every 4 hours.
Polling tip: hit /api/v1/orders?since=YYYY-MM-DD with the date of your last sync. Cache last_synced_at per order — when it advances, the order's status may have changed.

Node.js example client

// install: npm install node-fetch
import fetch from 'node-fetch';

const API_BASE = 'https://specsavvy.ca/api/v1';
const API_KEY  = process.env.SPECSAVVY_API_KEY;  // never hardcode

async function submitOrder(orderData) {
  const res = await fetch(`${API_BASE}/lens-orders`, {
    method: 'POST',
    headers: {
      'X-API-Key': API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(orderData),
  });
  const result = await res.json();
  if (!res.ok) throw new Error(result.error || 'Submit failed');
  return result;
}

// Example — single vision OD only, lab-supplied frame
submitOrder({
  wearer_info: '31985-T12',
  r_product_id: 11, r_index: '1.59',
  r_sph: '-2.25', r_pd: '31', r_fh: '22',
  l_enabled: false,
  lab_supplied_frame: true,
  frame_brand: 'Ray-Ban', frame_model: 'RB5184',
  frame_colour: 'Black', frame_size: '54-17-140',
}).then(r => console.log('Spec Savvy order #' + r.order_id))
  .catch(err => console.error(err));

Python example client

import os, requests

API_BASE = 'https://specsavvy.ca/api/v1'
API_KEY  = os.environ['SPECSAVVY_API_KEY']  # never hardcode

def submit_order(order_data):
    r = requests.post(
        f'{API_BASE}/lens-orders',
        headers={'X-API-Key': API_KEY},
        json=order_data,
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

# Example — single vision OD only, lab-supplied frame
result = submit_order({
    'wearer_info': '31985-T12',
    'r_product_id': 11, 'r_index': '1.59',
    'r_sph': '-2.25', 'r_pd': '31', 'r_fh': '22',
    'l_enabled': False,
    'lab_supplied_frame': True,
    'frame_brand': 'Ray-Ban', 'frame_model': 'RB5184',
    'frame_colour': 'Black', 'frame_size': '54-17-140',
})
print('Spec Savvy order #', result['order_id'])

Rate limits & security