Bezel Wholesale API · v1

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.

Preview for partner feedback. The API isn't live yet. Hostnames and rate limits are placeholders, and details may change before launch based on what we hear from you.
Guaranteed listingsA listing's price holds until its valid_until. Its quantity only goes down when you order. Bezel may add units at any time.
Inventory reserved for youYou see only the listings Bezel has reserved for your account. Nobody else can buy those units.
Pay from creditsThe full price of each watch is debited from your prepaid balance when you order. Bezel adds credits to your balance for you.
Shipping and authentication includedListing prices include shipping and Bezel's watch authentication. Each watch on an order can ship to a different address.
Prices exclude taxIf you resell, send your resale certificate to Bezel directly; it isn't handled through the API.
US addresses onlyv1 ships to addresses in the United States. There are no webhooks yet; poll for listing and order changes.

Endpoints

MethodPathDoes
GET/v1/accountsList the accounts your key acts for
GET/v1/listingsList listings reserved for you
GET/v1/listings/{id}Retrieve a listing
POST/v1/shipping_addressesSave a shipping address
GET/v1/shipping_addressesList saved addresses
DELETE/v1/shipping_addresses/{id}Delete a saved address
POST/v1/ordersPlace an order
GET/v1/ordersList orders
GET/v1/orders/{id}Retrieve an order with its items and shipments
GET/v1/balanceRetrieve 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.

  1. Check your key

    Bezel issues your sandbox key, which starts with bzl_test_. Confirm it works by listing your accounts.

    Request
    curl https://<sandbox-api-host>/v1/accounts \
      -H "Authorization: Bearer $BEZEL_API_KEY"
  2. 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.

    Request
    curl "https://<sandbox-api-host>/v1/listings?brand=Rolex" \
      -H "Authorization: Bearer $BEZEL_API_KEY"
  3. Save a shipping address

    Addresses are saved once and referenced by ID on orders.

    Request
    curl 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"
      }'
  4. Place an order

    Send a unique Idempotency-Key so a retry never places a second order.

    Request
    curl 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.

HTTP
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

EnvironmentBase URLKey prefix
Sandboxhttps://<sandbox-api-host>/v1bzl_test_
Productionhttps://<production-api-host>/v1bzl_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.

PermissionAllows
wholesale_listings:readList and retrieve listings
wholesale_shipping-addresses:readList and retrieve shipping addresses
wholesale_shipping-addresses:createSave shipping addresses, plus read
wholesale_shipping-addresses:deleteDelete shipping addresses, plus create and read
wholesale_orders:readList and retrieve orders
wholesale_orders:createPlace orders, plus read
wholesale_balance:readRetrieve 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:

JSON
{ "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.

Response
{
  "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.

Response 400
{
  "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"
  }
}
StatusCodeMeaningRetry
400parameter_missingA required field or header is missing. param names it.No
400parameter_invalidA value is malformed or out of range. param names it.No
400listing_not_foundAn order line names a listing that doesn't exist or isn't reserved for you.No
400shipping_address_not_foundAn order line names an address that doesn't exist or isn't yours.No
400address_unverifiedBezel couldn't verify a new address, so nothing was saved.With allow_unverified
401invalid_api_keyThe key is missing, malformed, unknown, revoked, expired or for the other environment.No
402insufficient_balanceThe order costs more than your balance. Nothing was placed.After credits are added
403insufficient_permissionThe key lacks the permission this endpoint needs.No
404resource_not_foundNo such account, listing, order or address for you.No
409idempotency_key_reusedThe idempotency key was used with a different body.With a new key
409idempotency_request_in_progressThe first request with this key is still running.Yes, after a pause
409no_units_availableNo line of the order could be filled. Nothing was placed.No
429rate_limitedToo many requests.After Retry-After
500internal_errorSomething failed at Bezel.Yes, with backoff
503service_unavailableBezel 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

FieldDescription
id
string
acc_ ID.
name
string
Display name, usually your trading name.
created_at
timestamp
When Bezel opened the account.
Account
{
  "id": "acc_Qm4Tz8Wc1Lr6Hn2V",
  "name": "Harbor Watch Co.",
  "created_at": "2026-11-02T17:45:00Z"
}

List accounts

GET/v1/accountsany valid key

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

GET/v1/accounts/{account_id}any valid key

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

FieldDescription
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.
StatusMeaning
activeBefore valid_until with at least one watch left. You can order it.
sold_outYou ordered every watch before valid_until. Returns to active if Bezel adds units.
expiredvalid_until has passed, and any unordered units are released.
Listing
{
  "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

GET/v1/listingspermission wholesale_listings:read

Returns your listings, newest first, paginated. Without a status filter, only active listings are returned.

ParameterDescription
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, cursorPagination.

Retrieve a listing

GET/v1/listings/{listing_id}permission wholesale_listings:read

Returns 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

FieldDescription
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.
StatusMeaning
verifiedBezel recognized the address. It may standardize fields such as the ZIP code, so the saved address can differ slightly from what you sent.
unverifiedBezel 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

POST/v1/shipping_addressespermission wholesale_shipping-addresses:create

Send the address fields above, without id, account_id, verification_status or created_at, plus an optional allow_unverified boolean that defaults to false.

  1. If Bezel recognizes the address, it's saved as verified and you get 201.
  2. If not, and allow_unverified is false, you get 400 address_unverified and nothing is saved. Show the address to the person entering it and ask whether to save it anyway.
  3. If they confirm, send it again with allow_unverified: true. It's saved as unverified and you get 201.
Request body
{
  "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

GET/v1/shipping_addressespermission wholesale_shipping-addresses:read

Returns your saved addresses, newest first, paginated. Filter with verification_status.

Retrieve a shipping address

GET/v1/shipping_addresses/{shipping_address_id}permission wholesale_shipping-addresses:read

Delete a shipping address

DELETE/v1/shipping_addresses/{shipping_address_id}permission wholesale_shipping-addresses:delete

Returns 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

FieldDescription
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

FieldDescription
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, papersCopied 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

FieldDescription
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

POST/v1/orderspermission wholesale_orders:create · requires Idempotency-Key

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

Request body
{
  "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:

  1. 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.
  2. 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.
  3. If nothing could be filled, it returns 409 no_units_available and places nothing.
  4. If the filled total is more than your balance, it returns 402 insufficient_balance and places nothing. Your balance never causes a partial fill.
  5. Otherwise it debits the filled total, reduces each listing's quantity and returns 201 with 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.

Response 201 · partial fill shown for illustration
{
  "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

GET/v1/orderspermission wholesale_orders:read

Returns your orders, newest first, paginated.

ParameterDescription
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, cursorPagination.

Retrieve an order

GET/v1/orders/{order_id}permission wholesale_orders:read

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

GET/v1/balancepermission wholesale_balance:read
Response 200
{
  "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.