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
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/me | Account info + balance + status |
| GET | /api/v1/products | Lens catalog with availability matrix |
| GET | /api/v1/products/:id | Single 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/cancel | Cancel 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:
- Daily digest (9am local time): a summary of every open order that hasn't shipped yet — sent only when you actually have open orders.
- On-Shipped: fires the moment we mark an order
Shipped, with the tracking number and URL.
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.
index(aggregate list) — every lens material the lens is available in. Examples:1.50,1.53(Trivex),1.56,1.59 Poly(polycarbonate),1.60,1.67,1.74.colour(aggregate list) — every tint / photochromic / polarized / mirror finish offered for the lens (across all materials).coating(aggregate list) — every coating supported across all materials.combos_by_index★ — the real compatibility map. Keyed by index, each entry lists the colours and coatings actually orderable in that material. This is the source of truth — the order endpoint validates the chosen(index, colour)pair and(index, coating)pair against this map.corridor(progressives / occupationals only) — corridor length in mm.absorption(when set) — photochromic / polarization base options.- Power ranges — each
combos_by_index[index].power_rangesobject hassphere,cylinder,axis,add,prism1,prism2entries, each shaped{ "min": -20.00, "max": 12.00, "step": 0.25, "count": 129 }. Use these to constrain the dropdowns in your own UI so customers can't enter unorderable powers. The server enforces the same ranges and returns 400 on out-of-range values.
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"
- "Is PhotoX Green offered on Peritus Pura in 1.74?" — pull
/api/v1/products/8, look atavailable_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;colouris["Clear"]only, so no — the order endpoint will return 400. - "What materials does Peritus Apex SV come in?" — pull
/api/v1/products/{n}, readavailable_options.index.
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
| Field | Type | Notes |
|---|---|---|
wearer_info | string | Your 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_)
| Field | Notes |
|---|---|
r_enabled / l_enabled | boolean — set false to skip that eye. |
r_product_id / l_product_id | Spec Savvy product_id from /api/v1/products. |
r_index | Lens index (e.g. 1.50, 1.59, 1.67, 1.74). |
r_coating | Coating code or name. |
r_corridor | Progressive corridor (mm). |
r_colour | Tint / photochromic / polarized colour name (must be in GET /api/v1/colours). |
r_absorption | Tint 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_sph | Sphere (e.g. -2.50). |
r_cyl | Cylinder. |
r_axis | Axis (0–180). |
r_add | Add power (progressives / bifocals). |
r_prism1 / r_base1 / r_prism2 / r_base2 | Prism + base direction. |
r_pd | Pupillary distance (mm). |
r_fh | Fitting height (mm). Alias accepted: r_height. |
r_diameter | Lens 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_ct | Edge thickness / centre thickness (mm). |
r_base_curve | Base curve. |
r_decentration | Decentration (mm). |
Shared fitting fields (apply to both eyes)
pantoscopic_tilt | Degrees. Alias: pano_tilt. |
wrap_angle | Frame wrap angle (degrees). |
vertex_distance | Vertex distance (mm). Alias: vertex. |
reading_distance | Reading working distance (cm/mm). |
automatic_inset | Auto inset toggle / value. |
Frame fields
| Field | Notes |
|---|---|
lab_supplied_frame | boolean — 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_size | Frame identification. When lab_supplied_frame is true, these values are used to locate the frame in Spec Savvy's stock. |
frame_sku | Optional internal SKU when known. Speeds up the lab-supplied-frame match. |
frame_type | Full / Half / Rimless. Default: Full. |
lens_width / lens_height / bridge_width | Frame 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_ed | Measured 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). |
shape | Frame shape code 1–8 (preset shapes) or ownshape when supplying a custom tracer file (see below). |
shape_file_base64 / shape_file_name | Optional 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_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
| Type | Frame source | Tracer needed? | What we ship back |
|---|---|---|---|
package | Spec Savvy stock | No | Finished pair, lenses already cut + mounted in the frame. |
cut_lens | Spec Savvy stock or your frame | Only when frame is yours (.oma/.vca) | Lenses already cut to the frame outline. You mount them. |
uncut_lens | Yours | No | Round 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
service | Service level / turnaround. |
order_type | One 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_mode | diameter (default) or boxed. |
important_info / other_info | Short admin notes. |
notes | Free-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:
- Unknown
product_id— value not found or product is not active. - Unknown coating / colour — value not present in the master catalog (
/api/v1/coatings,/api/v1/colours). Tip: submit thecodefor an exact match. - Coating not available in chosen index — coating's
available_indicesdoesn't includer_index. - Value not offered for this product — index / coating / colour / corridor / absorption not in the product's
available_options. - Power out of range —
r_sph,r_cyl, orr_addoutsidepower_ranges[index]for the chosen product.
Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid X-API-Key. |
403 | Account is paused — contact Spec Savvy. |
400 | Validation failed (see rules above) or required field missing. Message names the offending field and lists what's allowed. |
409 | Order can no longer be cancelled (already in production / shipped). |
500 | Server 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):
limit | Max rows to return. Default 50, max 200. |
since | ISO 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
| Field | Notes |
|---|---|
order_id | The Spec Savvy order ID. Use this to look up or query later. |
reference | Your reference: the wearer_info string you originally submitted. |
order_date | ISO timestamp when the order was placed. |
last_updated | ISO timestamp of the most recent change. |
status | One 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_status | Optional granular fulfillment note. null unless the lab has flagged something specific. |
supplier_order_number | The lab's internal reference number for this job, when assigned. null until the order is filed. |
tracking_number | Carrier tracking number once the order ships. null before then. |
tracking_url | Direct URL to the carrier's tracking page (if provided alongside the tracking number). |
estimated_delivery | Date string (YYYY-MM-DD) when set by the lab. null until quoted. |
last_synced_at | ISO timestamp of the last time Spec Savvy refreshed status from the lab. Updated every 4 hours. |
/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
- API keys are scoped per customer — orders submitted with a key are auto-attributed to that customer's account at Spec Savvy.
- Treat the key like a password. Don't commit it to source control. Use environment variables.
- If a key is compromised, log into the admin panel and click Regenerate Key on the customer's detail panel — the old key stops working immediately.
- If your account is paused, all API requests will return
403until reactivated.