Order watches reserved for your business
Bezel reserves inventory for your business at a guaranteed price. The Wholesale API lets your systems see that inventory, place orders paid from your prepaid credit balance, and track every watch through delivery.
valid_until. Its quantity only goes down when you order. Bezel may add units at any time.Endpoints
| Method | Path | Does |
|---|---|---|
| GET | /v1/accounts | List the accounts your key acts for |
| GET | /v1/listings | List listings reserved for you |
| GET | /v1/listings/{id} | Retrieve a listing |
| POST | /v1/shipping_addresses | Save a shipping address |
| GET | /v1/shipping_addresses | List saved addresses |
| DELETE | /v1/shipping_addresses/{id} | Delete a saved address |
| POST | /v1/orders | Place an order |
| GET | /v1/orders | List orders |
| GET | /v1/orders/{id} | Retrieve an order with its items and shipments |
| GET | /v1/balance | Retrieve your credit balance |
Quickstart
Place a sandbox order in four requests. Sandbox has its own listings, orders and balance, and its shipments never move real watches.
Check your key
Bezel issues your sandbox key, which starts with
bzl_test_. Confirm it works by listing your accounts.Requestcurl https://<sandbox-api-host>/v1/accounts \ -H "Authorization: Bearer $BEZEL_API_KEY"See what's reserved for you
List your active listings. Each one shows the watch, its unit price, how many you can order and how long the offer is guaranteed.
Requestcurl "https://<sandbox-api-host>/v1/listings?brand=Rolex" \ -H "Authorization: Bearer $BEZEL_API_KEY"Save a shipping address
Addresses are saved once and referenced by ID on orders.
Requestcurl https://<sandbox-api-host>/v1/shipping_addresses \ -H "Authorization: Bearer $BEZEL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Receiving Dept", "company": "Harbor Watch Co.", "line1": "500 Madison Ave", "city": "New York", "region": "NY", "postal_code": "10022", "country": "US", "phone": "+12125550142" }'Place an order
Send a unique
Idempotency-Keyso a retry never places a second order.Requestcurl https://<sandbox-api-host>/v1/orders \ -H "Authorization: Bearer $BEZEL_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 5f0c7a52-0c4e-4a8e-9d1b-3a6f2e9b7c11" \ -d '{ "customer_reference": "PO-88213", "lines": [ { "listing_id": "lst_9fK2mQ7xR4tB", "quantity": 2, "shipping_address_id": "adr_4Gm7kP2sW9dF" } ] }'Then poll
GET /v1/orders/{id}for shipping updates and tracking links.
Authentication
Every request carries a secret API key as a bearer token. Bezel issues keys to you directly; nobody at your business needs a Bezel login.
GET /v1/listings HTTP/1.1
Host: <sandbox-api-host>
Authorization: Bearer bzl_test_8NNUgqIB734YdZBb_ZgHvsXcOG50EMUT5aMfXfunqhSoafa7m904cMj2CO3u0hjA16- A key acts for exactly one account. An account can have several keys, so you can rotate without downtime.
- Keys look like
bzl_<env>_<key id>_<secret>. The 16 characters after the environment are the key's public ID (key_…), which is safe to log. - A key is shown once when issued and Bezel can't recover it. If you lose one, ask Bezel to revoke it and issue a new one.
- Keys can be issued with an expiry date. A revoked key stops working within a minute.
- Keep keys on your servers. Never put them in a browser, a mobile app or source control.
Environments
| Environment | Base URL | Key prefix |
|---|---|---|
| Sandbox | https://<sandbox-api-host>/v1 | bzl_test_ |
| Production | https://<production-api-host>/v1 | bzl_live_ |
A key only works in its own environment. Hostnames will be confirmed before launch.
Permissions
Each key carries the permissions you and Bezel agree on when it's issued. A request outside them gets 403 insufficient_permission. A higher privilege includes the lower ones on the same resource.
| Permission | Allows |
|---|---|
wholesale_listings:read | List and retrieve listings |
wholesale_shipping-addresses:read | List and retrieve shipping addresses |
wholesale_shipping-addresses:create | Save shipping addresses, plus read |
wholesale_shipping-addresses:delete | Delete shipping addresses, plus create and read |
wholesale_orders:read | List and retrieve orders |
wholesale_orders:create | Place orders, plus read |
wholesale_balance:read | Retrieve the balance |
Account endpoints work with any valid key.
Conventions
The API speaks JSON over HTTPS with snake_case field names. These rules hold for every endpoint.
IDs
IDs are opaque strings with a type prefix: acc_, lst_, adr_, ord_, itm_, shp_ and key_. Treat them as case-sensitive and allow up to 64 characters.
Money
Amounts are objects with an integer amount in minor units and an ISO 4217 currency. v1 is USD only, so a $14,250.00 watch is:
{ "amount": 1425000, "currency": "USD" }Timestamps
RFC 3339 strings in UTC, for example 2026-11-11T23:59:59Z.
Pagination
List endpoints return newest first. Pass limit (1 to 100, default 25) and, for the next page, the previous response's next_cursor as cursor. next_cursor is null on the last page.
{
"data": [ … ],
"has_more": true,
"next_cursor": "eyJjIjoiMjAyNi0xMS0wNFQyMjoyMDowMFoifQ"
}Idempotency
POST /v1/orders requires an Idempotency-Key header: any unique string up to 255 characters, such as a UUID. Retrying with the same key and body within 24 hours returns the original response and never places a second order. Reusing a key with a different body returns 409 idempotency_key_reused.
Polling for changes
There are no webhooks in v1. To pick up changes, pass updated_after to GET /v1/listings or GET /v1/orders with the time of your last successful poll.
Rate limits
Limits are per account and will be published before launch. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds). Over the limit, you get 429 rate_limited with a Retry-After header.
Request IDs
Every response carries a Bezel-Request-Id header. Include it when you contact Bezel about a request.
Versioning
The major version is in the path (/v1). Within v1, Bezel may add endpoints, optional request fields, response fields and enum values without notice, so ignore fields you don't recognize and handle unknown enum values. Removing or renaming anything means a new major version.
Errors
Every error uses the same envelope. Branch on code; message is written for people and may change.
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid",
"message": "lines[0].quantity must be at least 1.",
"param": "lines[0].quantity",
"request_id": "req_8Hq2LkVw0pXa"
}
}| Status | Code | Meaning | Retry |
|---|---|---|---|
| 400 | parameter_missing | A required field or header is missing. param names it. | No |
| 400 | parameter_invalid | A value is malformed or out of range. param names it. | No |
| 400 | listing_not_found | An order line names a listing that doesn't exist or isn't reserved for you. | No |
| 400 | shipping_address_not_found | An order line names an address that doesn't exist or isn't yours. | No |
| 400 | address_unverified | Bezel couldn't verify a new address, so nothing was saved. | With allow_unverified |
| 401 | invalid_api_key | The key is missing, malformed, unknown, revoked, expired or for the other environment. | No |
| 402 | insufficient_balance | The order costs more than your balance. Nothing was placed. | After credits are added |
| 403 | insufficient_permission | The key lacks the permission this endpoint needs. | No |
| 404 | resource_not_found | No such account, listing, order or address for you. | No |
| 409 | idempotency_key_reused | The idempotency key was used with a different body. | With a new key |
| 409 | idempotency_request_in_progress | The first request with this key is still running. | Yes, after a pause |
| 409 | no_units_available | No line of the order could be filled. Nothing was placed. | No |
| 429 | rate_limited | Too many requests. | After Retry-After |
| 500 | internal_error | Something failed at Bezel. | Yes, with backoff |
| 503 | service_unavailable | Bezel is temporarily unavailable, or couldn't check your key. Don't treat your key as invalid. | Yes, after Retry-After |
The type field groups codes into invalid_request_error, authentication_error, permission_error, balance_error, rate_limit_error and api_error. Retrying POST /v1/orders with the same Idempotency-Key is always safe.
Accounts
An account is your business as a buyer. Listings, shipping addresses, orders and the balance all belong to an account, and your key acts for one account. Most businesses have one account today; every resource carries account_id so a business can hold several later, for example one per storefront.
The account object
| Field | Description |
|---|---|
id string | acc_ ID. |
name string | Display name, usually your trading name. |
created_at timestamp | When Bezel opened the account. |
{
"id": "acc_Qm4Tz8Wc1Lr6Hn2V",
"name": "Harbor Watch Co.",
"created_at": "2026-11-02T17:45:00Z"
}List accounts
Returns the accounts your key acts for, newest first, paginated. Today that's always one account, which makes this a quick way to check a new key.
Retrieve an account
Returns one account, or 404 resource_not_found for an account your key doesn't act for.
Listings
A listing is a quantity of one watch reference, in one condition, at one unit price, reserved for your account until valid_until. Before then Bezel never raises the price or takes units back, though it may add units. A new price always arrives as a new listing.
The listing object
| Field | Description |
|---|---|
id string | lst_ ID. |
account_id string | The account the listing is reserved for. |
status enum | active, sold_out or expired. See below. |
brand string | Brand name, e.g. Rolex. |
reference_number string | Manufacturer reference, e.g. 126610LN. |
model_name string or null | Common model name, e.g. Submariner Date, when Bezel has one. |
condition enum | unworn (new, never worn) or pre_owned. |
box enum | Whether the watch comes with its original box: yes, no or unknown (not confirmed either way). |
papers enum | Whether it comes with its original papers, such as the warranty card: yes, no or unknown. |
images array | Photos as {url, width, height}. v1 returns one; accept several. URLs may expire, so re-fetch the listing rather than storing them. |
unit_price Money | Price per watch, including shipping and watch authentication, excluding tax. |
quantity_available integer | Watches you can still order. Goes up when Bezel adds units and down only when you order. 0 once sold out or expired. |
valid_until timestamp | End of the guarantee. Orders placed after it aren't filled. |
created_at timestamp | When Bezel posted the listing. |
updated_at timestamp | Last change, such as an order or Bezel adding units. |
| Status | Meaning |
|---|---|
active | Before valid_until with at least one watch left. You can order it. |
sold_out | You ordered every watch before valid_until. Returns to active if Bezel adds units. |
expired | valid_until has passed, and any unordered units are released. |
{
"id": "lst_9fK2mQ7xR4tB",
"account_id": "acc_Qm4Tz8Wc1Lr6Hn2V",
"status": "active",
"brand": "Rolex",
"reference_number": "126610LN",
"model_name": "Submariner Date",
"condition": "unworn",
"box": "yes",
"papers": "unknown",
"images": [
{ "url": "https://<image-host>/lst_9fK2mQ7xR4tB/1.jpg", "width": 1600, "height": 1600 }
],
"unit_price": { "amount": 1425000, "currency": "USD" },
"quantity_available": 4,
"valid_until": "2026-11-11T23:59:59Z",
"created_at": "2026-11-04T17:00:00Z",
"updated_at": "2026-11-04T21:12:40Z"
}List listings
wholesale_listings:readReturns your listings, newest first, paginated. Without a status filter, only active listings are returned.
| Parameter | Description |
|---|---|
status enum, repeatable | active, sold_out or expired. Default active. |
brand string | Exact brand name, case-insensitive. |
reference_number string | Exact reference, case-insensitive. |
condition enum | unworn or pre_owned. |
box, papers enum | yes, no or unknown. |
min_price, max_price integer | Unit price bounds in minor units, inclusive. |
updated_after timestamp | Only listings changed after this time. |
limit, cursor | Pagination. |
Retrieve a listing
wholesale_listings:readReturns one listing in any status. A listing that isn't reserved for you returns 404 resource_not_found, the same as one that doesn't exist.
Shipping addresses
Save each shipping address once and reference it by ID on orders. Bezel validates every new address. If it can't verify one, you can still save it as unverified, and orders to it are still filled. Addresses can't be edited; to change one, save a new address and delete the old one.
The shipping address object
| Field | Description |
|---|---|
id string | adr_ ID. |
account_id string | The account that saved the address. |
name string | Recipient name. |
company string or null | Company name. |
line1 string | Street address. |
line2 string or null | Suite, unit or floor. |
city string | City. |
region string | Two-letter state code, e.g. NY. |
postal_code string | ZIP code. |
country string | Must be US in v1. Anything else returns 400 parameter_invalid. |
phone string | Recipient phone in E.164 format, for the carrier. |
verification_status enum | verified or unverified. See below. |
created_at timestamp | When it was saved. |
| Status | Meaning |
|---|---|
verified | Bezel recognized the address. It may standardize fields such as the ZIP code, so the saved address can differ slightly from what you sent. |
unverified | Bezel couldn't identify the address and you chose to save it anyway. Bezel ships to it as given and may contact you before shipping. Bezel can later mark it verified. |
Save a shipping address
wholesale_shipping-addresses:createSend the address fields above, without id, account_id, verification_status or created_at, plus an optional allow_unverified boolean that defaults to false.
- If Bezel recognizes the address, it's saved as
verifiedand you get201. - If not, and
allow_unverifiedisfalse, you get400 address_unverifiedand nothing is saved. Show the address to the person entering it and ask whether to save it anyway. - If they confirm, send it again with
allow_unverified: true. It's saved asunverifiedand you get201.
{
"name": "Receiving Dept",
"company": "Harbor Watch Co.",
"line1": "500 Madison Ave",
"city": "New York",
"region": "NY",
"postal_code": "10022",
"country": "US",
"phone": "+12125550142",
"allow_unverified": false
}List shipping addresses
wholesale_shipping-addresses:readReturns your saved addresses, newest first, paginated. Filter with verification_status.
Retrieve a shipping address
wholesale_shipping-addresses:readDelete a shipping address
wholesale_shipping-addresses:deleteReturns 204. The address no longer appears when you list or retrieve addresses and can't be used on new orders. Orders that already use it keep it and still ship to it.
Orders
An order buys watches from one or more of your listings and debits your balance in the same step. Each watch becomes its own order item, so items move through shipping independently and can go to different addresses.
The order object
| Field | Description |
|---|---|
id string | ord_ ID. |
account_id string | The account that placed the order. |
status enum | open, or closed once every item is delivered or cancelled. |
customer_reference string or null | Your own reference, such as a PO number. Up to 100 characters. |
items array | One order item per watch filled. |
unfilled array | Lines, or parts of lines, that couldn't be filled when the order was placed, as {line_index, listing_id, quantity, reason}. Usually empty. |
shipments array | Packages sent so far. Empty until the first item ships. |
amount_charged Money | Total debited when the order was placed: the sum of every item's unit_price. |
amount_refunded Money | Total credited back for cancelled items. |
created_at timestamp | When the order was placed. |
updated_at timestamp | Last change to the order or any of its items or shipments. |
closed_at timestamp or null | When the order closed. |
The order item object
| Field | Description |
|---|---|
id string | itm_ ID. |
line_index integer | Position of the request line this item came from, starting at 0. |
listing_id string | The listing it was bought from. |
brand, reference_number, model_name, condition, box, papers | Copied from the listing at purchase, so they stay readable after the listing expires. |
unit_price Money | Price paid for this watch. |
status enum | awaiting_shipment, shipped, delivered or cancelled. |
shipping_address Shipping address | The saved address this watch ships to. |
shipment_id string or null | The shipment carrying it, once shipped. |
cancellation_reason enum or null | unfulfillable if Bezel couldn't ship the watch, or lost_in_transit if the carrier lost it. |
shipped_at, delivered_at, cancelled_at timestamp or null | When the item reached each status. |
The shipment object
| Field | Description |
|---|---|
id string | shp_ ID. |
carrier string | Carrier code, e.g. fedex. |
tracking_number string | Carrier tracking number. |
tracking_url string | Link to the carrier's tracking page. |
item_ids array | Items in this package. They all share one address. |
shipped_at timestamp | When the package was handed to the carrier. |
delivered_at timestamp or null | When the carrier reported delivery. |
Place an order
wholesale_orders:create · requires Idempotency-KeyThe body lists up to 50 lines. Each line names a listing, a quantity and the shipping_address_id those watches ship to. Use two lines for the same listing to split it across addresses.
{
"customer_reference": "PO-88213",
"lines": [
{ "listing_id": "lst_9fK2mQ7xR4tB", "quantity": 2, "shipping_address_id": "adr_4Gm7kP2sW9dF" },
{ "listing_id": "lst_3Wd8pZ1nE6yH", "quantity": 1, "shipping_address_id": "adr_8Rt1yN5vB3hJ" }
]
}Bezel places the order in one atomic step:
- It validates the request. A malformed body, an unknown listing or an unknown address rejects the whole request with
400, and nothing is placed. Unverified addresses are accepted. - It fills each line in order with as many watches as the listing has left, up to the quantity asked. An expired or sold-out listing fills nothing.
- If nothing could be filled, it returns
409 no_units_availableand places nothing. - If the filled total is more than your balance, it returns
402 insufficient_balanceand places nothing. Your balance never causes a partial fill. - Otherwise it debits the filled total, reduces each listing's quantity and returns
201with the order.
Because listings are reserved for you, a line only falls short when your own concurrent orders took the units first or the listing expired. Any shortfall is recorded in unfilled with a reason of insufficient_quantity or listing_expired. Always check unfilled before treating an order as complete.
{
"id": "ord_7Tn4cX2qL9vA",
"account_id": "acc_Qm4Tz8Wc1Lr6Hn2V",
"status": "open",
"customer_reference": "PO-88213",
"items": [
{
"id": "itm_1aB2cD3eF4gH",
"line_index": 0,
"listing_id": "lst_9fK2mQ7xR4tB",
"brand": "Rolex",
"reference_number": "126610LN",
"model_name": "Submariner Date",
"condition": "unworn",
"box": "yes",
"papers": "yes",
"unit_price": { "amount": 1425000, "currency": "USD" },
"status": "awaiting_shipment",
"shipping_address": { "id": "adr_4Gm7kP2sW9dF", "verification_status": "verified", "…": "…" },
"shipment_id": null,
"cancellation_reason": null,
"shipped_at": null,
"delivered_at": null,
"cancelled_at": null
}
],
"unfilled": [
{ "line_index": 0, "listing_id": "lst_9fK2mQ7xR4tB", "quantity": 1, "reason": "insufficient_quantity" },
{ "line_index": 1, "listing_id": "lst_3Wd8pZ1nE6yH", "quantity": 1, "reason": "listing_expired" }
],
"shipments": [],
"amount_charged": { "amount": 1425000, "currency": "USD" },
"amount_refunded": { "amount": 0, "currency": "USD" },
"created_at": "2026-11-04T22:31:05Z",
"updated_at": "2026-11-04T22:31:05Z",
"closed_at": null
}There's no cancel endpoint in v1. To cancel an order, contact Bezel.
Order item lifecycle
Each watch moves through shipping on its own. The order stays open until every item is delivered or cancelled.
When an item is cancelled, its unit_price is refunded to your balance and added to the order's amount_refunded. A watch that arrives damaged stays delivered; contact Bezel to resolve it.
List orders
wholesale_orders:readReturns your orders, newest first, paginated.
| Parameter | Description |
|---|---|
status enum | open or closed. Both by default. |
customer_reference string | Exact match. |
created_after, created_before timestamp | Bounds on created_at. |
updated_after timestamp | Only orders changed after this time. Use it to poll for shipping updates. |
limit, cursor | Pagination. |
Retrieve an order
wholesale_orders:readReturns one order with its items, unfilled lines and shipments.
Balance
Your balance is prepaid credit in USD. Bezel adds credits for you; there's no top-up endpoint. Orders debit it in full when placed, cancelled items are refunded to it, and it never goes negative.
wholesale_balance:read{
"account_id": "acc_Qm4Tz8Wc1Lr6Hn2V",
"available": { "amount": 8575000, "currency": "USD" },
"updated_at": "2026-11-04T22:31:05Z"
}Share feedback
The Wholesale API is still taking shape, and we're building it with the partners who will use it. If something here doesn't fit how your business buys, is missing, or is harder than it should be, we want to hear about it.
Send your thoughts to your Bezel contact at any point. No feedback is too small, and anything in this preview can change before launch.