Conventions
Learn these once; they hold everywhere.
Catalogue reads and writes
Version 2 reads the catalogue and provides documented write operations for items, images, draft auctions and sessions, and lot activation before bidding opens. See item writes for item JSON bodies, permissions, validation and retry reconciliation. Each operation has its own permissions and limits; use the OpenAPI specification for the supported fields. A write response does not imply publication, enabled bidding or completed image/schedule processing.
IDs
Every object has a prefixed ID: auc_ for auctions, ses_ for sessions, lot_ for lots, itm_ for items, img_ for images, sel_ for sellers, cat_ for categories, mkr_ for makers, dlc_ for delivery classes, inc_ for increment strategies and con_ for consignments.
The prefix means an ID is self-describing in a log line, and passing a session ID where a lot ID belongs is rejected as a 404 rather than quietly returning the wrong object. Treat IDs as opaque strings: the part after the prefix is ours to change.
The prefix is required on the way out and optional on the way in: GET /lots/lot_b48e0a19-… and GET /lots/b48e0a19-… both work, so an integration holding bare identifiers does not have to rewrite them before its first call. A wrong prefix is still refused: GET /lots/ses_b48e0a19-… is a 404, because asking for the wrong kind of thing is a mistake worth being told about.
Money
Money is an object with an integer amount in minor units and an ISO 4217 currency:
"starting_bid": { "amount": 12000, "currency": "EUR" }
That is €120,00. No floats, no locale ambiguity, no separate currency field elsewhere in the response to keep in sync.
An estimate is a range whose bounds are the same object, so one formatting function in your code handles every amount the API returns. Either bound may be absent:
"estimate": { "low": { "amount": 8000, "currency": "EUR" },
"high": { "amount": 12000, "currency": "EUR" } }
Time
Timestamps are RFC 3339 in UTC: 2026-07-01T10:00:00Z.
Text
Catalogue text (item titles and descriptions, auction and session names) is returned as a plain string in the auction house’s own language:
"title": "Delft blue tulip vase, 18th century"
Every house on the platform runs a single language, so per-locale objects would be ceremony wrapped around one value. If a house ever adds a second language we will add a locale parameter to select it; the field shape stays flat either way.
Pagination
Lists are cursor-based:
curl "https://app.bidvise.com/api/v2/lots?session=ses_7c1d5e88-2a34-4f19-9b0c-6d2e8f4a1357&limit=50" \
-H "Authorization: Bearer $BIDVISE_API_KEY"
{ "data": [ … ], "has_more": true, "next_cursor": "cur_MGY4Yz…" }
Pass next_cursor back as cursor for the following page, and stop when has_more is false. limit is 1-100 and defaults to 25.
Each collection has a fixed order, and the cursor is a position in it: lots by lot number, sessions by when bidding opens (newest first), auctions and items by when they were created (newest first). Where a value is unset it sorts last, and the record’s identifier breaks ties so the order is total and a page boundary never lands in an ambiguous place.
Cursors are keyset-based, not offset-based, so a catalogue that grows while you are walking it never shifts rows into a page you already read, and never repeats one. The guarantee is for rows present when the walk began: a row added behind your position (a lot numbered 10 after you have passed 50) is not visible to the rest of that walk. Pick it up with a fresh walk, or by watching updated_at. A cursor is opaque: pass it back unchanged rather than parsing it. Responses also carry an RFC 8288 Link header with rel="next", absent on the last page; cursor pagination walks forward only, so there is no prev or last to follow.
Expansion
Relationships are IDs by default. Inflate one in place with expand[]:
curl "https://app.bidvise.com/api/v2/lots/lot_b48e0a19-5c72-4d83-91af-3e6b2c07d514?expand[]=item&expand[]=item.images" \
-H "Authorization: Bearer $BIDVISE_API_KEY"
Expanding needs the permission of the resource you expand, as well as the one you are reading: a key holding only lots.read is refused ?expand[]=session, because a session is what auctions.read protects. Inlining a resource cannot be a way around not being allowed to read it.
The field you expand keeps its name and its position: item is a string before expansion and an object after it, never a second field somewhere else in the response. This is why there are no parallel “detailed” endpoints: depth is a parameter, not a different URL.
Unknown and malformed parameters
Nothing is guessed on your behalf. A limit that is not a whole number, a cursor this API did not issue, an expand value the resource does not support, a boolean filter that is not true or false, or a filter id of the wrong kind is a 400, and the detail names the parameter and what was expected. For expand, it lists what the resource can expand.
That is deliberate. The alternative is to silently default, ignore or restart: expand[]=iamges returning no images and no error, or a corrupted cursor quietly restarting the walk so a sync processes the whole catalogue twice. A typo should be told about, once, immediately.
Errors
Errors are RFC 9457 problem documents, served as application/problem+json:
{
"type": "https://docs.bidvise.com/api/problems/not-found",
"title": "Resource not found",
"status": 404,
"instance": "/api/v2/lots/lot_b48e0a19-5c72-4d83-91af-3e6b2c07d514",
"detail": "Unknown lot id"
}
4xx means fix the request; 5xx means retry later.
Rate limits
300 requests per minute per key. Every response to a key linked to an auction house carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the window resets); going over returns 429 with Retry-After.
The window is fixed rather than sliding, which is worth knowing at the boundary: a client can spend its whole allowance in the last second of one minute and again in the first second of the next, so the real worst case is twice the limit across two seconds. Fewer, larger calls (limit=100) beat many small ones.
The limit is per key, so one integration cannot exhaust another’s allowance.
Scope and isolation
An API key belongs to exactly one auction house, and every response is scoped to it. There is no cross-house lookup and no parameter that widens the scope: an ID belonging to another house reads as 404, not 403, because the existence of that object is not yours to learn.
Versioning
The major version is in the path (/api/v2) and changes only for breaking changes. Within a version, changes are additive: new fields and new endpoints can appear, so parse responses leniently and ignore fields you do not recognise.