Collections
Curated, tenant-scoped groupings of listings — hand-picked or map-bounded
Introduction
Collections let a tenant curate named groups of listings and use them anywhere a listing set is needed: a search filter, a landing page for a destination, or a featured section on a booking site. A collection carries a display name, an optional description, a URL slug, and one of two membership modes.
Collection Types
| Type | Membership |
|---|---|
STATIC | Hand-picked: listings are added and removed explicitly via the listings sub-resource |
LOCATION | Geographic: every active listing whose coordinates fall inside the collection's bounds box is a member, automatically |
LOCATION collections require bounds (minLat, minLng, maxLat, maxLng) at create time. Membership is evaluated at query time, so newly synced listings inside the box join the collection with no extra calls.
The Slug Contract
The slug is a consumer-supplied handle (Shopify-style). ZEPL stores it, validates its shape, and enforces per-tenant uniqueness — nothing more. As the consumer, you own URL-naming policy: transliteration, localization, branding.
- Shape: lowercase ASCII segments joined by single hyphens (
^[a-z0-9]+(?:-[a-z0-9]+)*$), max 100 chars. - Supplying a slug that's already taken for the tenant returns
409 Conflict— including on concurrent-create races. Retry with a different handle (e.g. a-2suffix). - If you omit the slug, ZEPL derives a naive ASCII fallback from
name. This fallback does no transliteration — a fully non-Latin name (e.g. Cyrillic) falls back to a generic default — so supplying your own handle is strongly recommended. - The slug is updatable via
PATCH, but changing it breaks any published URL that references the old handle. Treat renames as a deliberate migration, not a side effect of renaming the collection.
Endpoints
GET /api/v1/collections # list (add ?includeListings=true for members)
POST /api/v1/collections # create
GET /api/v1/collections/{id} # fetch one
PATCH /api/v1/collections/{id} # update name/slug/description/bounds
DELETE /api/v1/collections/{id} # delete
POST /api/v1/collections/{id}/listings # add static members
DELETE /api/v1/collections/{id}/listings # remove static membersAll endpoints are tenant-scoped: pass x-calry-tenant-id to operate as a non-default tenant, exactly like the listing endpoints. The listings sub-resource only applies to STATIC collections.
Example: create a location collection
POST /api/v1/collections
{
"name": "South Black Sea Coast",
"slug": "south-black-sea-coast",
"type": "LOCATION",
"bounds": { "minLat": 42.0, "minLng": 27.4, "maxLat": 42.7, "maxLng": 28.1 }
}Filtering Search by Collection
Pass collectionId to Search to restrict results to a collection's members:
GET /api/v1/search?collectionId=<id>&checkInDate=2026-09-01&checkOutDate=2026-09-05If the collection cannot be resolved — unknown ID, deleted, or belonging to another tenant — the search returns an empty result set, not an error. A stale collection reference on a published site degrades to "no results" instead of a broken page.