CalryOTA Docs

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

TypeMembership
STATICHand-picked: listings are added and removed explicitly via the listings sub-resource
LOCATIONGeographic: 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 -2 suffix).
  • 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 members

All 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-05

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

API Reference

On this page