Skip to main content

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.

StatusMeaning
400Bad request — invalid input
403Security check failed (inquiry endpoint)
404Dealer or watch not found
429Rate limit exceeded — try again later
500Server error
GET/api/v1/dealers/:id

Dealer 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"
  }
}
GET/api/v1/dealers/:id/inventory

Inventory List

Returns the dealer's public inventory. Sold watches are excluded by default.

Query Parameters

ParamTypeDescription
pagenumberPage number. Default: 1
perPagenumberResults per page. Default: 24, max: 100
brandstringFilter by brand (case-insensitive). E.g. Rolex
includeSoldbooleanInclude 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

FieldTypeDescription
idstringUnique watch identifier
brandstring | nullBrand name
modelstring | nullModel name
referenceNumberstring | nullManufacturer reference number
yearstring | nullYear of production
seriesstring | nullSub-collection name, e.g. Submariner, Speedmaster
nicknamestring | nullPopular nickname, e.g. Hulk, Pepsi, Panda
stylestring | nullDress, Sport, Diver, Chronograph, Pilot, GMT, etc.
genderstring | nullMen's, Women's, or Unisex
caseSizestring | nullCase diameter, e.g. 40mm
caseMaterialstring | nullStainless Steel, Gold, Titanium, Ceramic, etc.
caseShapestring | nullRound, Tonneau, Square, etc.
caseBackstring | nullSolid, Exhibition, Screw-Down, etc.
crownstring | nullScrew-Down, Push-In, etc.
bezelstring | nullFixed, Unidirectional Rotating, Tachymeter, etc.
crystalstring | nullSapphire, Acrylic, Mineral, etc.
waterResistancestring | nulle.g. 100m, 300m
movementstring | nullAutomatic, Manual, Quartz, etc.
calibrestring | nullMovement calibre reference, e.g. Cal. 3135, ETA 2824
powerReservestring | nulle.g. 48h, 72h
dialColorstring | nulle.g. Black, Blue, Silver
dialTypestring | nullSunburst, Matte, Guilloche, etc.
handsstring | nullBaton, Sword, Mercedes, etc.
dialMarkersstring | nullApplied indices, Arabic numerals, etc.
conditionstring | nullnew, bnib, lnib, excellent, good, fair
conditionOverallnumber | nullOverall condition score on a 0.0–10.0 scale
hasBoxbooleanOriginal box included
hasPapersbooleanOriginal papers included
hasWarrantybooleanActive warranty included
warrantyUntilMonthstring | nullMonth warranty expires, e.g. January
warrantyUntilYearstring | nullYear warranty expires, e.g. 2027
comesWithNotesstring | nullFreeform description of included extras, e.g. extra straps, tools
strapLengthnumber | nullTotal strap/bracelet length in inches
maxWristSizenumber | nullMaximum wrist circumference this watch fits comfortably, in inches
descriptionstring | nullDealer's main listing description
thingsToNotestring | nullImportant condition notes or caveats from the dealer
statusesstring[]selling, trading, and/or sold
askingPricenumber | nullRetail/client asking price in USD
dealerAskingPricenumber | nullWholesale/dealer-to-dealer asking price in USD
photosobject[]Array of { id, url }
urlstringDirect link to watch page on wristly.io
createdAtstringISO 8601 timestamp
GET/api/v1/dealers/:id/inventory/:watchId

Single 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.

FieldTypeDescription
conditionOverallnumber | nullOverall condition score (0.0–10.0)
conditionOverallNotestring | nullDealer note for overall condition
conditionCasenumber | nullCase body, lugs, caseback, and crown condition score
conditionCaseNotestring | nullDealer note for case condition
conditionBezelnumber | nullBezel condition score
conditionBezelNotestring | nullDealer note for bezel condition
conditionDialnumber | nullDial surface, indices, hands, and lume condition score
conditionDialNotestring | nullDealer note for dial condition
conditionMovementnumber | nullMovement condition score. null if not tested — check conditionMovementNotTested
conditionMovementNotestring | nullDealer note for movement condition
conditionMovementNotTestedbooleantrue if the dealer did not test the movement
conditionCrystalnumber | nullCrystal condition score
conditionCrystalNotestring | nullDealer note for crystal condition
conditionBraceletnumber | nullBracelet or strap condition score
conditionBraceletNotestring | nullDealer note for bracelet condition
conditionBoxPapersnumber | nullBox & papers condition score (only relevant when hasBox or hasPapers is true)
conditionBoxPapersNotestring | nullDealer note for box & papers condition
POST/api/v1/dealers/:id/inquiry

Submit 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

FieldTypeDescription
watchIdstringRequired. The watch ID from the inventory list
firstNamestringRequired. Buyer's first name
lastNamestringRequired. Buyer's last name
emailstringRequired. Buyer's email address
phonestringRequired. Buyer's phone number
preferencesstringOptional. Buyer's preferences, e.g. "Full set, open to trade"
messagestringOptional. Free-text message from the buyer (max 2000 chars)
honeypotstringOptional. Leave empty — used for bot detection
turnstileTokenstringOptional. 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.

POST/api/v1/dealers/:id/sell-watch

Sell 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-submission with type seller
  • 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

FieldTypeDescription
firstNamestringRequired. Seller's first name
lastNamestringOptional. Seller's last name
emailstringRequired. Seller's email address
phonestringRequired. Seller's phone number
brandstringRequired. Watch brand (e.g. Rolex, Patek Philippe)
modelstringRequired. Watch model (e.g. Submariner, Nautilus)
referenceNumberstringOptional. Reference number (e.g. 126610LN)
yearstringOptional. Year of manufacture
conditionstringOptional. One of: mint, excellent, very_good, good, fair, poor
askingPricenumberOptional. Seller's asking price in USD
accessoriesstringOptional. Box, papers, extra links, etc.
messagestringOptional. Free-text message (max 2000 chars)
photosFile[]Optional. Up to 10 images (JPEG/PNG/WebP, max 10 MB each) — repeat field for multiple
honeypotstringOptional. Leave empty — bot detection
turnstileTokenstringOptional. 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 }
POST/api/v1/dealers/:id/trade-watch

Trade 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

FieldTypeDescription
firstNamestringRequired. Submitter's first name
lastNamestringOptional. Submitter's last name
emailstringRequired. Submitter's email address
phonestringRequired. Submitter's phone number
brandstringRequired. Their watch brand
modelstringRequired. Their watch model
referenceNumberstringOptional. Reference number
yearstringOptional. Year of manufacture
conditionstringOptional. One of: mint, excellent, very_good, good, fair, poor
accessoriesstringOptional. Box, papers, extra links, etc.
desiredBrandstringOptional. Brand of the watch they want in return
desiredModelstringOptional. Model of the watch they want in return
cashDifferencenumberOptional. Cash adjustment — positive means they pay dealer, negative means dealer pays them
messagestringOptional. Free-text message (max 2000 chars)
photosFile[]Optional. Up to 10 images — repeat field for multiple
honeypotstringOptional. Leave empty — bot detection
turnstileTokenstringOptional. 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 }
POST/api/v1/dealers/:id/consign-watch

Consign 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

FieldTypeDescription
firstNamestringRequired. Submitter's first name
lastNamestringOptional. Submitter's last name
emailstringRequired. Submitter's email address
phonestringRequired. Submitter's phone number
brandstringRequired. Watch brand
modelstringRequired. Watch model
referenceNumberstringOptional. Reference number
yearstringOptional. Year of manufacture
conditionstringOptional. One of: mint, excellent, very_good, good, fair, poor
accessoriesstringOptional. Box, papers, extra links, etc.
desiredPricenumberOptional. Minimum price the owner wants (USD)
timeframestringOptional. One of: immediate, 1_3_months, 3_6_months, 6_12_months, flexible
messagestringOptional. Free-text message (max 2000 chars)
photosFile[]Optional. Up to 10 images — repeat field for multiple
honeypotstringOptional. Leave empty — bot detection
turnstileTokenstringOptional. 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.

POST/api/v1/dealers/:id/general-inquiry

General 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)

FieldTypeDescription
firstNamestringRequired (or provide name). Max 50 chars.
lastNamestring?Optional. Max 50 chars.
namestring?Legacy full-name field (split into first/last internally).
emailstringRequired. Submitter email address.
phonestringRequired. Submitter phone number.
preferencesstring?Optional. What the buyer is looking for. Max 1 000 chars.
messagestring?Optional. Free-form message. Max 2 000 chars.
honeypotstring?Leave empty. If filled the request is silently discarded.
turnstileTokenstring?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

ParamValuesDescription
themelight | darkForce 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

ScopeWhat it allows
inventory:readList and read watches in your inventory
inventory:writeCreate watches and update non-financial fields
expenses:writeAdd, update, and delete watch expenses
photos:writeUpload photos (binary or URL) and delete photos
documents:writeUpload 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.

ScopeWhat it allows
inventory:financialRead/write cost of acquisition, asking prices, acquisition channel and contact
inventory:salesRead/write sold amount, sold date, payment method, sale channel, buyer contact
contacts:readRead CRM contacts and their inquiry preference history
contacts:writeCreate and update CRM contacts
GET/api/v1/watches

List 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));
POST/api/v1/watches

Create 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 calls

Response

Returns the full watch object identical to the Get Watch response, including the newly assigned id.

GET/api/v1/watches/:watchId

Get 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));
PATCH/api/v1/watches/:watchId

Update 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.

POST/api/v1/watches/:watchId/expenses

Add 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
  })
})
PATCH/api/v1/watches/:watchId/expenses/:expenseId

Update 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 })
})
DELETE/api/v1/watches/:watchId/expenses/:expenseId

Delete 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.

POST/api/v1/watches/:watchId/photos

Add 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 valueDescription
dialDial face
casebackCase back
two_oclock2 o'clock side
ten_oclock10 o'clock side
crownCrown
case_sideCase side profile
strapStrap or bracelet
claspClasp or deployant
wrist_shotOn-wrist shot
boxBox
papersPapers / warranty card
otherOther
DELETE/api/v1/watches/:watchId/photos/:photoId

Delete 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" }
})