API Reference
Fetch live dealer inventory and integrate it natively into any website.
Introduction
The Wristly REST API lets you pull a dealer's live inventory and render it as native HTML on any website. Because the content lives on your domain — not inside an iframe — search engines crawl and index it as your content, which is significantly better for SEO.
All responses are JSON. The base URL for every endpoint is:
https://www.wristly.io/api/v1
Your dealer ID is available on the My Storefront page inside your Wristly dealer dashboard under the API Access section.
Authentication
The read endpoints (GET) are fully public — no API key or token required. Inventory data is already public on the Wristly platform, so there is nothing to protect. Just make the request.
CORS
All endpoints return the following header, so you can call the API directly from a browser on any domain:
Access-Control-Allow-Origin: *
This means you can use fetch() from client-side JavaScript on your dealer's website without any proxy or server-side workaround.
Errors
Errors return a JSON object with an error field and a standard HTTP status code.
| Status | Meaning |
|---|---|
| 400 | Bad request — invalid input |
| 403 | Security check failed (inquiry endpoint) |
| 404 | Dealer or watch not found |
| 429 | Rate limit exceeded — try again later |
| 500 | Server error |
/api/v1/dealers/:idDealer Profile
Returns public profile information for a dealer.
Example Request
fetch("https://www.wristly.io/api/v1/dealers/DEALER_ID")
.then(res => res.json())
.then(data => console.log(data));Example Response
{
"dealer": {
"id": "cm9abc123...",
"name": "5D Watches",
"bio": "Specialist in vintage and modern luxury timepieces.",
"logo": "https://cdn.wristly.io/logos/5d-watches.jpg",
"banner": "https://cdn.wristly.io/banners/5d-watches.jpg",
"website": "https://5dwatches.com",
"phone": "+1 (555) 000-1234",
"isVerified": true,
"createdAt": "2024-03-15T12:00:00.000Z"
}
}/api/v1/dealers/:id/inventoryInventory List
Returns the dealer's public inventory. Sold watches are excluded by default.
Query Parameters
| Param | Type | Description |
|---|---|---|
| page | number | Page number. Default: 1 |
| perPage | number | Results per page. Default: 24, max: 100 |
| brand | string | Filter by brand (case-insensitive). E.g. Rolex |
| includeSold | boolean | Include sold watches. Default: false |
Example Request
fetch("https://www.wristly.io/api/v1/dealers/DEALER_ID/inventory?page=1&perPage=12&brand=Rolex")
.then(res => res.json())
.then(({ watches, pagination }) => {
watches.forEach(watch => {
console.log(watch.brand, watch.model, watch.askingPrice);
});
});Example Response
{
"dealer": {
"id": "cm9abc123...",
"name": "5D Watches",
"logo": "https://cdn.wristly.io/logos/5d-watches.jpg",
"isVerified": true
},
"watches": [
{
"id": "cm9xyz789...",
"brand": "Rolex",
"model": "Daytona",
"referenceNumber": "116500LN",
"year": "2021",
"series": null,
"nickname": "Panda",
"style": "Chronograph",
"gender": "Men's",
"caseSize": "40mm",
"caseMaterial": "Stainless Steel",
"caseShape": "Round",
"caseBack": "Solid",
"crown": "Screw-Down",
"bezel": "Tachymeter",
"crystal": "Sapphire",
"waterResistance": "100m",
"movement": "Automatic",
"calibre": "Cal. 4130",
"powerReserve": "72h",
"dialColor": "White",
"dialType": "Sunburst",
"hands": "Baton",
"dialMarkers": "Applied indices",
"condition": "excellent",
"conditionOverall": 8.5,
"hasBox": true,
"hasPapers": true,
"hasWarranty": false,
"warrantyUntilMonth": null,
"warrantyUntilYear": null,
"comesWithNotes": "Two aftermarket rubber straps included",
"strapLength": 7.5,
"maxWristSize": 7.0,
"description": "Full set, unworn. White dial.",
"thingsToNote": null,
"statuses": ["selling"],
"askingPrice": 35000,
"dealerAskingPrice": 33000,
"photos": [
{ "id": "ph_01...", "url": "https://cdn.wristly.io/watches/photo.jpg" }
],
"url": "https://www.wristly.io/watch/cm9xyz789...",
"createdAt": "2025-01-10T09:30:00.000Z"
}
],
"pagination": {
"total": 47,
"page": 1,
"perPage": 12,
"totalPages": 4
}
}Watch Fields
| Field | Type | Description |
|---|---|---|
| id | string | Unique watch identifier |
| brand | string | null | Brand name |
| model | string | null | Model name |
| referenceNumber | string | null | Manufacturer reference number |
| year | string | null | Year of production |
| series | string | null | Sub-collection name, e.g. Submariner, Speedmaster |
| nickname | string | null | Popular nickname, e.g. Hulk, Pepsi, Panda |
| style | string | null | Dress, Sport, Diver, Chronograph, Pilot, GMT, etc. |
| gender | string | null | Men's, Women's, or Unisex |
| caseSize | string | null | Case diameter, e.g. 40mm |
| caseMaterial | string | null | Stainless Steel, Gold, Titanium, Ceramic, etc. |
| caseShape | string | null | Round, Tonneau, Square, etc. |
| caseBack | string | null | Solid, Exhibition, Screw-Down, etc. |
| crown | string | null | Screw-Down, Push-In, etc. |
| bezel | string | null | Fixed, Unidirectional Rotating, Tachymeter, etc. |
| crystal | string | null | Sapphire, Acrylic, Mineral, etc. |
| waterResistance | string | null | e.g. 100m, 300m |
| movement | string | null | Automatic, Manual, Quartz, etc. |
| calibre | string | null | Movement calibre reference, e.g. Cal. 3135, ETA 2824 |
| powerReserve | string | null | e.g. 48h, 72h |
| dialColor | string | null | e.g. Black, Blue, Silver |
| dialType | string | null | Sunburst, Matte, Guilloche, etc. |
| hands | string | null | Baton, Sword, Mercedes, etc. |
| dialMarkers | string | null | Applied indices, Arabic numerals, etc. |
| condition | string | null | new, bnib, lnib, excellent, good, fair |
| conditionOverall | number | null | Overall condition score on a 0.0–10.0 scale |
| hasBox | boolean | Original box included |
| hasPapers | boolean | Original papers included |
| hasWarranty | boolean | Active warranty included |
| warrantyUntilMonth | string | null | Month warranty expires, e.g. January |
| warrantyUntilYear | string | null | Year warranty expires, e.g. 2027 |
| comesWithNotes | string | null | Freeform description of included extras, e.g. extra straps, tools |
| strapLength | number | null | Total strap/bracelet length in inches |
| maxWristSize | number | null | Maximum wrist circumference this watch fits comfortably, in inches |
| description | string | null | Dealer's main listing description |
| thingsToNote | string | null | Important condition notes or caveats from the dealer |
| statuses | string[] | selling, trading, and/or sold |
| askingPrice | number | null | Retail/client asking price in USD |
| dealerAskingPrice | number | null | Wholesale/dealer-to-dealer asking price in USD |
| photos | object[] | Array of { id, url } |
| url | string | Direct link to watch page on wristly.io |
| createdAt | string | ISO 8601 timestamp |
/api/v1/dealers/:id/inventory/:watchIdSingle Watch
Returns full details for a single watch including all photos and additional specifications. The watch must be public and belong to the specified dealer.
Example Request
fetch("https://www.wristly.io/api/v1/dealers/DEALER_ID/inventory/WATCH_ID")
.then(res => res.json())
.then(watch => console.log(watch));Example Response
{
"id": "cm9xyz789...",
"brand": "Rolex",
"model": "Daytona",
"referenceNumber": "116500LN",
"year": "2021",
"series": null,
"nickname": "Panda",
"style": "Chronograph",
"gender": "Men's",
"caseSize": "40mm",
"caseMaterial": "Stainless Steel",
"caseShape": "Round",
"caseBack": "Solid",
"crown": "Screw-Down",
"bezel": "Tachymeter",
"crystal": "Sapphire",
"waterResistance": "100m",
"movement": "Automatic",
"calibre": "Cal. 4130",
"powerReserve": "72h",
"dialColor": "White",
"dialType": "Sunburst",
"hands": "Baton",
"dialMarkers": "Applied indices",
"condition": "excellent",
"conditionOverall": 8.5,
"conditionOverallNote": null,
"conditionCase": 8.0,
"conditionCaseNote": "Minor hairlines on the case sides",
"conditionBezel": 9.0,
"conditionBezelNote": null,
"conditionDial": 9.5,
"conditionDialNote": null,
"conditionMovement": 9.0,
"conditionMovementNote": null,
"conditionMovementNotTested": false,
"conditionCrystal": 9.0,
"conditionCrystalNote": null,
"conditionBracelet": 8.5,
"conditionBraceletNote": "Slight stretch on center links",
"conditionBoxPapers": null,
"conditionBoxPapersNote": null,
"hasBox": true,
"hasPapers": true,
"hasWarranty": false,
"warrantyUntilMonth": null,
"warrantyUntilYear": null,
"comesWithNotes": "Two aftermarket rubber straps included",
"strapLength": 7.5,
"maxWristSize": 7.0,
"description": "Full set, unworn. White dial. Comes with all original packaging.",
"thingsToNote": null,
"statuses": ["selling"],
"askingPrice": 35000,
"dealerAskingPrice": 33000,
"photos": [
{ "id": "ph_01...", "url": "https://cdn.wristly.io/watches/photo1.jpg", "order": 0 },
{ "id": "ph_02...", "url": "https://cdn.wristly.io/watches/photo2.jpg", "order": 1 }
],
"dealer": {
"id": "cm9abc123...",
"name": "5D Watches",
"logo": "https://cdn.wristly.io/logos/5d-watches.jpg",
"isVerified": true
},
"url": "https://www.wristly.io/watch/cm9xyz789...",
"createdAt": "2025-01-10T09:30:00.000Z"
}Condition Breakdown Fields
When a dealer has graded a watch using the condition scale, the following fields are included. All scores are number | null on a 0.0–10.0 scale (stored as floats). A null value means the dealer has not graded that component.
| Field | Type | Description |
|---|---|---|
| conditionOverall | number | null | Overall condition score (0.0–10.0) |
| conditionOverallNote | string | null | Dealer note for overall condition |
| conditionCase | number | null | Case body, lugs, caseback, and crown condition score |
| conditionCaseNote | string | null | Dealer note for case condition |
| conditionBezel | number | null | Bezel condition score |
| conditionBezelNote | string | null | Dealer note for bezel condition |
| conditionDial | number | null | Dial surface, indices, hands, and lume condition score |
| conditionDialNote | string | null | Dealer note for dial condition |
| conditionMovement | number | null | Movement condition score. null if not tested — check conditionMovementNotTested |
| conditionMovementNote | string | null | Dealer note for movement condition |
| conditionMovementNotTested | boolean | true if the dealer did not test the movement |
| conditionCrystal | number | null | Crystal condition score |
| conditionCrystalNote | string | null | Dealer note for crystal condition |
| conditionBracelet | number | null | Bracelet or strap condition score |
| conditionBraceletNote | string | null | Dealer note for bracelet condition |
| conditionBoxPapers | number | null | Box & papers condition score (only relevant when hasBox or hasPapers is true) |
| conditionBoxPapersNote | string | null | Dealer note for box & papers condition |
/api/v1/dealers/:id/inquirySubmit Inquiry
Submits a buyer inquiry for a specific watch. When successful, Wristly automatically:
- Sends an email notification to the dealer
- Creates or updates a contact in the dealer's CRM with the buyer's name, email, and phone — tagged with source
embed_inquiry - Saves a structured inquiry record on the contact capturing the watch's brand, model, series, reference number, asking price, and an auto-bucketed price range (
under_5k,5k_10k,10k_20k,20k_50k,50k_plus). Watch details are preserved even if the watch is later sold or removed, building a durable buyer preference history for CRM segmentation - Sends an in-app notification to the dealer with a direct link to their CRM
Rate limited to 5 requests per IP per hour.
Request Body
| Field | Type | Description |
|---|---|---|
| watchId | string | Required. The watch ID from the inventory list |
| firstName | string | Required. Buyer's first name |
| lastName | string | Required. Buyer's last name |
| string | Required. Buyer's email address | |
| phone | string | Required. Buyer's phone number |
| preferences | string | Optional. Buyer's preferences, e.g. "Full set, open to trade" |
| message | string | Optional. Free-text message from the buyer (max 2000 chars) |
| honeypot | string | Optional. Leave empty — used for bot detection |
| turnstileToken | string | Optional. Cloudflare Turnstile token if implementing CAPTCHA |
Example Request
fetch("https://www.wristly.io/api/v1/dealers/DEALER_ID/inquiry", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
watchId: "cm9xyz789...",
firstName: "John",
lastName: "Smith",
email: "john@example.com",
phone: "+1 (555) 000-5678",
preferences: "Full set preferred, open to trade", // optional
message: "Is the price negotiable?", // optional
honeypot: "" // always send empty — bots fill this
})
})
.then(res => res.json())
.then(data => console.log(data)); // { "success": true }Success Response
{ "success": true }Spam protection: Always include a hidden honeypot input in your form and leave it empty. Real users never fill it; bots do. The server silently discards submissions where this field is populated, so never pre-fill it.
/api/v1/dealers/:id/sell-watchSell Watch Form
Submits a “Sell My Watch” form from someone who wants to sell their watch to the dealer. Accepts multipart/form-data so you can attach up to 10 photos alongside the form fields. When successful, Wristly automatically:
- Converts every photo to WebP (quality 88, max 3000×3000px) and stores them in Supabase
- Sends a branded email to the dealer with watch details, contact info, and photo thumbnails
- Creates or updates a CRM contact tagged
sell-submissionwith typeseller - Saves a timestamped contact note with all submission details and photo URLs
- Sends an in-app notification to the dealer linking to their CRM
Rate limited to 3 requests per IP per hour.
Request Body — multipart/form-data
| Field | Type | Description |
|---|---|---|
| firstName | string | Required. Seller's first name |
| lastName | string | Optional. Seller's last name |
| string | Required. Seller's email address | |
| phone | string | Required. Seller's phone number |
| brand | string | Required. Watch brand (e.g. Rolex, Patek Philippe) |
| model | string | Required. Watch model (e.g. Submariner, Nautilus) |
| referenceNumber | string | Optional. Reference number (e.g. 126610LN) |
| year | string | Optional. Year of manufacture |
| condition | string | Optional. One of: mint, excellent, very_good, good, fair, poor |
| askingPrice | number | Optional. Seller's asking price in USD |
| accessories | string | Optional. Box, papers, extra links, etc. |
| message | string | Optional. Free-text message (max 2000 chars) |
| photos | File[] | Optional. Up to 10 images (JPEG/PNG/WebP, max 10 MB each) — repeat field for multiple |
| honeypot | string | Optional. Leave empty — bot detection |
| turnstileToken | string | Optional. Cloudflare Turnstile token |
Example Request
const formData = new FormData();
formData.append("firstName", "Jane");
formData.append("lastName", "Smith");
formData.append("email", "jane@example.com");
formData.append("phone", "+1 (555) 000-1234");
formData.append("brand", "Rolex");
formData.append("model", "Daytona");
formData.append("referenceNumber", "116500LN");
formData.append("year", "2021");
formData.append("condition", "excellent");
formData.append("askingPrice", "29000");
formData.append("accessories", "Full set — box, papers, extra link");
formData.append("message", "Purchased new, worn maybe 10 times.");
formData.append("honeypot", ""); // always send empty
// Attach photos (repeat for multiple)
formData.append("photos", photoFile1);
formData.append("photos", photoFile2);
fetch("https://www.wristly.io/api/v1/dealers/DEALER_ID/sell-watch", {
method: "POST",
body: formData // DO NOT set Content-Type header — browser sets it with boundary
})
.then(res => res.json())
.then(data => console.log(data)); // { "success": true }Success Response
{ "success": true }/api/v1/dealers/:id/trade-watchTrade Watch Form
Submits a “Trade My Watch” form from someone who wants to trade their watch, optionally specifying a watch from the dealer's inventory they're interested in and any cash difference. Accepts multipart/form-data with up to 10 photos. Behaves identically to the Sell Watch form on the CRM side — creates/updates a contact tagged trade-submission with type seller + buyer, saves a note, sends email and in-app notification.
Rate limited to 3 requests per IP per hour.
Request Body — multipart/form-data
| Field | Type | Description |
|---|---|---|
| firstName | string | Required. Submitter's first name |
| lastName | string | Optional. Submitter's last name |
| string | Required. Submitter's email address | |
| phone | string | Required. Submitter's phone number |
| brand | string | Required. Their watch brand |
| model | string | Required. Their watch model |
| referenceNumber | string | Optional. Reference number |
| year | string | Optional. Year of manufacture |
| condition | string | Optional. One of: mint, excellent, very_good, good, fair, poor |
| accessories | string | Optional. Box, papers, extra links, etc. |
| desiredBrand | string | Optional. Brand of the watch they want in return |
| desiredModel | string | Optional. Model of the watch they want in return |
| cashDifference | number | Optional. Cash adjustment — positive means they pay dealer, negative means dealer pays them |
| message | string | Optional. Free-text message (max 2000 chars) |
| photos | File[] | Optional. Up to 10 images — repeat field for multiple |
| honeypot | string | Optional. Leave empty — bot detection |
| turnstileToken | string | Optional. Cloudflare Turnstile token |
Example Request
const formData = new FormData();
formData.append("firstName", "Mark");
formData.append("email", "mark@example.com");
formData.append("phone", "+1 (555) 000-9876");
formData.append("brand", "Omega");
formData.append("model", "Speedmaster");
formData.append("referenceNumber", "310.30.42.50.01.001");
formData.append("condition", "very_good");
formData.append("desiredBrand", "Rolex"); // optional — what they want
formData.append("desiredModel", "GMT-Master II");
formData.append("cashDifference", "5000"); // optional — they'd pay $5k on top
formData.append("honeypot", "");
formData.append("photos", photoFile1);
fetch("https://www.wristly.io/api/v1/dealers/DEALER_ID/trade-watch", {
method: "POST",
body: formData
})
.then(res => res.json())
.then(data => console.log(data)); // { "success": true }Success Response
{ "success": true }/api/v1/dealers/:id/consign-watchConsign Watch Form
Submits a “Consign My Watch” form from someone who wants the dealer to sell their watch on their behalf. Accepts multipart/form-data with up to 10 photos. Creates/updates a contact tagged consign-submission with type seller, saves a note with all details, and notifies the dealer via email and in-app notification.
Rate limited to 3 requests per IP per hour.
Request Body — multipart/form-data
| Field | Type | Description |
|---|---|---|
| firstName | string | Required. Submitter's first name |
| lastName | string | Optional. Submitter's last name |
| string | Required. Submitter's email address | |
| phone | string | Required. Submitter's phone number |
| brand | string | Required. Watch brand |
| model | string | Required. Watch model |
| referenceNumber | string | Optional. Reference number |
| year | string | Optional. Year of manufacture |
| condition | string | Optional. One of: mint, excellent, very_good, good, fair, poor |
| accessories | string | Optional. Box, papers, extra links, etc. |
| desiredPrice | number | Optional. Minimum price the owner wants (USD) |
| timeframe | string | Optional. One of: immediate, 1_3_months, 3_6_months, 6_12_months, flexible |
| message | string | Optional. Free-text message (max 2000 chars) |
| photos | File[] | Optional. Up to 10 images — repeat field for multiple |
| honeypot | string | Optional. Leave empty — bot detection |
| turnstileToken | string | Optional. Cloudflare Turnstile token |
Example Request
const formData = new FormData();
formData.append("firstName", "Sarah");
formData.append("lastName", "Lee");
formData.append("email", "sarah@example.com");
formData.append("phone", "+1 (555) 000-4321");
formData.append("brand", "Patek Philippe");
formData.append("model", "Aquanaut");
formData.append("referenceNumber", "5167A-001");
formData.append("year", "2019");
formData.append("condition", "mint");
formData.append("accessories", "Full set");
formData.append("desiredPrice", "35000"); // optional — minimum they'll accept
formData.append("timeframe", "3_6_months"); // optional
formData.append("message", "Purchased directly from AD. All original.");
formData.append("honeypot", "");
formData.append("photos", photoFile1);
formData.append("photos", photoFile2);
formData.append("photos", photoFile3);
fetch("https://www.wristly.io/api/v1/dealers/DEALER_ID/consign-watch", {
method: "POST",
body: formData
})
.then(res => res.json())
.then(data => console.log(data)); // { "success": true }Success Response
{ "success": true }Photo uploads: Do not set a Content-Type header when sending FormData — the browser sets it automatically with the correct multipart boundary. Photos are compressed server-side to WebP before storage, so raw phone camera shots (HEIC converted to JPEG, PNG screenshots, etc.) are all fine to send as-is.
/api/v1/dealers/:id/general-inquiryGeneral Inquiry
Submit a general (non-watch-specific) contact form to a dealer. Accepts JSON. Creates or updates a CRM contact tagged general-inquiry, saves a note, and sends an email notification to the dealer.
Request Body (JSON)
| Field | Type | Description |
|---|---|---|
| firstName | string | Required (or provide name). Max 50 chars. |
| lastName | string? | Optional. Max 50 chars. |
| name | string? | Legacy full-name field (split into first/last internally). |
| string | Required. Submitter email address. | |
| phone | string | Required. Submitter phone number. |
| preferences | string? | Optional. What the buyer is looking for. Max 1 000 chars. |
| message | string? | Optional. Free-form message. Max 2 000 chars. |
| honeypot | string? | Leave empty. If filled the request is silently discarded. |
| turnstileToken | string? | Cloudflare Turnstile token (required when site key is configured). |
Example
fetch("https://www.wristly.io/api/v1/dealers/DEALER_ID/general-inquiry", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
firstName: "Sarah",
lastName: "Kim",
email: "sarah@example.com",
phone: "+1 555-234-5678",
preferences: "Looking for a Rolex Submariner, budget around $15k",
message: "Please reach out via text if possible."
})
})
.then(res => res.json())
.then(data => console.log(data)); // { "success": true }Success Response
{ "success": true }Embeddable Forms
Drop a ready-made, mobile-friendly form onto any website with a single <iframe> tag. No JavaScript setup required — just copy, paste, and replace DEALER_ID with your own.
Overview
Each embed form is served at:
https://www.wristly.io/dealers/DEALER_ID/forms/TYPE
Replace TYPE with one of: inquiry, sell, trade, consign.
Query Parameters
| Param | Values | Description |
|---|---|---|
| theme | light | dark | Force a colour theme. Omit to follow the visitor's system preference. |
Sizing tip: Set the iframe height to at least 620px for sell/trade/consign forms (which include a photo uploader) and 480px for the inquiry form. Width responds fully down to 320 px.
General Inquiry Embed
A simple contact form — name, email, phone, and an optional message. Ideal for sidebar widgets or contact pages.
<iframe src="https://www.wristly.io/dealers/DEALER_ID/forms/inquiry" width="100%" height="480" style="border:none;border-radius:12px;" loading="lazy" title="Contact Us" ></iframe>
Force dark mode:
<iframe src="https://www.wristly.io/dealers/DEALER_ID/forms/inquiry?theme=dark" ... ></iframe>
Sell a Watch Embed
Lets a visitor submit a watch they want to sell: contact details, watch specs, asking price, condition, and up to 10 photos. Calls POST /api/v1/dealers/:id/sell-watch under the hood.
<iframe src="https://www.wristly.io/dealers/DEALER_ID/forms/sell" width="100%" height="680" style="border:none;border-radius:12px;" loading="lazy" title="Sell Your Watch" ></iframe>
Trade a Watch Embed
Captures the visitor's watch details plus the watch they want in return and any cash difference. Calls POST /api/v1/dealers/:id/trade-watch under the hood.
<iframe src="https://www.wristly.io/dealers/DEALER_ID/forms/trade" width="100%" height="780" style="border:none;border-radius:12px;" loading="lazy" title="Trade Your Watch" ></iframe>
Consign a Watch Embed
Captures consignment details — watch specs, desired price, timeframe, and photos. Calls POST /api/v1/dealers/:id/consign-watch under the hood.
<iframe src="https://www.wristly.io/dealers/DEALER_ID/forms/consign" width="100%" height="720" style="border:none;border-radius:12px;" loading="lazy" title="Consign Your Watch" ></iframe>
Authenticated API
The authenticated API lets you read and write your own dealer inventory programmatically — create watches, update specs and condition scores, add expenses, upload photos, and more. It is designed for automation, AI agents, and integrations with external systems.
API Keys
Authenticated endpoints require a secret API key passed as a Bearer token in the Authorization header. Keys are created and managed in your Dealer Settings under the API Keys section.
Authorization: Bearer wristly_your_api_key_here
When creating a key you can configure:
- Name — a label so you know what this key is used for (e.g. Claude Agent, Website Integration)
- Expiry — never, 30 days, 90 days, 1 year, or a custom date
- Scopes — which operations the key is allowed to perform (see below)
Your key is shown only once at creation time. Copy it immediately and store it in a secure secrets manager. Wristly cannot recover or display it again. If a key is compromised, revoke it immediately from your settings and create a new one.
Scopes
Each key is granted a set of scopes that control what it can do. A request that requires a scope the key does not have returns 403 Forbidden. Use the minimum scopes needed for each integration — do not grant sensitive scopes unless the use case requires them.
Standard Scopes
| Scope | What it allows |
|---|---|
| inventory:read | List and read watches in your inventory |
| inventory:write | Create watches and update non-financial fields |
| expenses:write | Add, update, and delete watch expenses |
| photos:write | Upload photos (binary or URL) and delete photos |
| documents:write | Upload internal documents to a watch |
Sensitive Scopes
These scopes expose or write financial and sales data. Only add them when the integration explicitly requires it.
| Scope | What it allows |
|---|---|
| inventory:financial | Read/write cost of acquisition, asking prices, acquisition channel and contact |
| inventory:sales | Read/write sold amount, sold date, payment method, sale channel, buyer contact |
| contacts:read | Read CRM contacts and their inquiry preference history |
| contacts:write | Create and update CRM contacts |
/api/v1/watchesList Watches
Returns your dealer inventory. Requires inventory:read. Supports the same page, perPage, brand, and includeSold params as the public endpoint, plus a search param for full-text search across brand, model, and reference number.
Financial fields (costOfAcquisition, askingPrice, etc.) are included only when the key has inventory:financial. Sales fields are included only with inventory:sales.
fetch("https://www.wristly.io/api/v1/watches", {
headers: { "Authorization": "Bearer wristly_your_api_key_here" }
})
.then(res => res.json())
.then(({ watches, pagination }) => console.log(watches));/api/v1/watchesCreate Watch
Adds a new watch to your inventory. Requires inventory:write. Financial fields require inventory:financial; sales fields require inventory:sales. Any fields not provided are left null — you can fill them in later with a PATCH request.
Example Request
fetch("https://www.wristly.io/api/v1/watches", {
method: "POST",
headers: {
"Authorization": "Bearer wristly_your_api_key_here",
"Content-Type": "application/json"
},
body: JSON.stringify({
brand: "Rolex",
model: "Submariner Date",
referenceNumber: "126610LN",
year: "2023",
series: "Submariner",
caseSize: "41mm",
caseMaterial: "Stainless Steel",
movement: "Automatic",
calibre: "Cal. 3235",
condition: "lnib",
conditionOverall: 9.5,
hasBox: true,
hasPapers: true,
description: "Full set, unworn.",
statuses: ["selling"],
isPublic: true,
// Requires inventory:financial scope:
askingPrice: 14500,
costOfAcquisition: 13200
})
})
.then(res => res.json())
.then(watch => console.log(watch.id)); // use this ID for all future callsResponse
Returns the full watch object identical to the Get Watch response, including the newly assigned id.
/api/v1/watches/:watchIdGet Watch
Returns full details for a single watch including all photos and expenses. Requires inventory:read. The watch must belong to your dealer account.
fetch("https://www.wristly.io/api/v1/watches/WATCH_ID", {
headers: { "Authorization": "Bearer wristly_your_api_key_here" }
})
.then(res => res.json())
.then(watch => console.log(watch));/api/v1/watches/:watchIdUpdate Watch
Updates any fields on a watch. Requires inventory:write. Only the fields you include in the request body are changed — omitted fields are left untouched. Scope rules are the same as Create Watch.
This is the primary endpoint for AI agents filling in missing specs — send only the fields that need to be updated.
// Example: fill in specs researched by an AI agent
fetch("https://www.wristly.io/api/v1/watches/WATCH_ID", {
method: "PATCH",
headers: {
"Authorization": "Bearer wristly_your_api_key_here",
"Content-Type": "application/json"
},
body: JSON.stringify({
calibre: "Cal. 3235",
bezel: "Unidirectional Rotating",
crystal: "Sapphire",
powerReserve: "70h",
waterResistance: "300m",
dialColor: "Black",
description: "Updated description with full provenance details."
})
})Expenses
Manage acquisition and holding costs for a watch. All expense endpoints require the expenses:write scope.
/api/v1/watches/:watchId/expensesAdd an expense to a watch.
fetch("https://www.wristly.io/api/v1/watches/WATCH_ID/expenses", {
method: "POST",
headers: {
"Authorization": "Bearer wristly_your_api_key_here",
"Content-Type": "application/json"
},
body: JSON.stringify({
description: "Full service",
amount: 850
})
})/api/v1/watches/:watchId/expenses/:expenseIdUpdate an expense description or amount.
fetch("https://www.wristly.io/api/v1/watches/WATCH_ID/expenses/EXPENSE_ID", {
method: "PATCH",
headers: {
"Authorization": "Bearer wristly_your_api_key_here",
"Content-Type": "application/json"
},
body: JSON.stringify({ amount: 950 })
})/api/v1/watches/:watchId/expenses/:expenseIdDelete an expense.
fetch("https://www.wristly.io/api/v1/watches/WATCH_ID/expenses/EXPENSE_ID", {
method: "DELETE",
headers: { "Authorization": "Bearer wristly_your_api_key_here" }
})Photos
Upload and manage photos for a watch. Requires photos:write. All uploaded photos are automatically converted to WebP format (quality 88, max 3000×3000px) before storage — the same pipeline used by the Wristly web interface.
/api/v1/watches/:watchId/photosAdd a photo via URL or binary file upload. Both are processed identically — the URL is fetched server-side, validated as an image, and stored in Wristly's CDN.
Option A — URL
fetch("https://www.wristly.io/api/v1/watches/WATCH_ID/photos", {
method: "POST",
headers: {
"Authorization": "Bearer wristly_your_api_key_here",
"Content-Type": "application/json"
},
body: JSON.stringify({
url: "https://example.com/watch-photo.jpg",
photoType: "dial" // optional
})
})Option B — Binary upload
const form = new FormData();
form.append("photo", fileBlob, "photo.jpg");
form.append("photoType", "dial"); // optional
fetch("https://www.wristly.io/api/v1/watches/WATCH_ID/photos", {
method: "POST",
headers: { "Authorization": "Bearer wristly_your_api_key_here" },
body: form
})| photoType value | Description |
|---|---|
| dial | Dial face |
| caseback | Case back |
| two_oclock | 2 o'clock side |
| ten_oclock | 10 o'clock side |
| crown | Crown |
| case_side | Case side profile |
| strap | Strap or bracelet |
| clasp | Clasp or deployant |
| wrist_shot | On-wrist shot |
| box | Box |
| papers | Papers / warranty card |
| other | Other |
/api/v1/watches/:watchId/photos/:photoIdDelete a photo. Removes it from storage and the watch listing.
fetch("https://www.wristly.io/api/v1/watches/WATCH_ID/photos/PHOTO_ID", {
method: "DELETE",
headers: { "Authorization": "Bearer wristly_your_api_key_here" }
})Questions? support@wristly.io