Skip to content

API Conventions

All clients (Next.js web, Flutter apps, admin) consume the same REST API. JSON only, UTF-8, UTC timestamps in ISO-8601 (2026-07-05T14:30:00Z).

Base URL & Versioning

https://api.yam3at.com/api/v1
  • URI versioning (/api/v1). v1 is stable for the life of MVP + Scale.
  • Additive changes (new fields, new endpoints, new enum values on output) are non-breaking and happen within v1 — clients must tolerate unknown fields and unknown enum values.
  • Breaking changes (removing/renaming fields, changing semantics, new required inputs) require v2, run side-by-side with v1 for a minimum 12-month deprecation window, announced via Deprecation and Sunset response headers on v1.
  • Mobile reality check: Flutter apps can't be force-upgraded instantly; nothing ships that breaks the previous two released app versions.

Authentication

Authorization: Bearer <sanctum-token>
  • Tokens issued by POST /auth/login, POST /auth/otp/verify, POST /auth/social. Missing/invalid token → 401 UNAUTHENTICATED; valid token, insufficient rights → 403 FORBIDDEN.
  • Public endpoints (search, vendor profiles, categories, CMS) require no token but are rate-limited harder.

Standard Response Envelope

Success (single resource):

{
  "data": { "id": "01J9ZK...", "...": "..." },
  "meta": {}
}

Success (collection):

{
  "data": [ { "...": "..." } ],
  "meta": {
    "pagination": { "page": 2, "per_page": 20, "total": 143, "total_pages": 8 }
  },
  "links": { "next": "...?page=3", "prev": "...?page=1" }
}

Error:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The given data was invalid.",
    "details": {
      "budget_max": ["must be greater than or equal to budget_min"]
    },
    "request_id": "req_01J9ZKQ8MZ"
  }
}

Rules:

  • data XOR error — never both. message is a developer-facing English string; clients render their own localized user messages keyed on code.
  • details appears only for VALIDATION_FAILED (field → array of rule messages, localized per Accept-Language).
  • request_id is always present and echoed in the X-Request-Id header — quote it in bug reports; it joins server logs.
  • Resource IDs in the API are always the ULID public_id (id in payloads). Internal integers never leak.
  • Money is serialized as a string with the currency's exact decimal places, plus currency: {"amount": "150.000", "currency": "KWD"}. Never a float.

HTTP Status Usage

Status Meaning here
200 Read / update success
201 Resource created (body contains it; Location header set)
202 Accepted for async processing (e.g., media confirm)
204 Success, no body (deletes, mark-read)
400 Malformed request (bad JSON, bad param type)
401 / 403 Unauthenticated / unauthorized
404 Not found or not yours (no existence oracle across tenants)
409 State conflict (illegal transition, duplicate)
422 Validation failed
429 Rate limited (Retry-After header)
500 / 503 Server error / dependency down (SEARCH_UNAVAILABLE, PAYMENT_PROVIDER_UNAVAILABLE)

Error-Code Catalog

Stable, SCREAMING_SNAKE, grouped by prefix. Clients map codes → localized strings.

Code HTTP Meaning
UNAUTHENTICATED 401 Missing/expired token
FORBIDDEN 403 Role/ownership check failed
NOT_FOUND 404 Unknown or unowned resource
VALIDATION_FAILED 422 See details
RATE_LIMITED 429 See Retry-After
IDEMPOTENCY_KEY_REUSED 409 Same key, different payload
AUTH_INVALID_CREDENTIALS 401 Bad email/phone + password
AUTH_OTP_INVALID / AUTH_OTP_EXPIRED / AUTH_OTP_MAX_ATTEMPTS 422 OTP failures
AUTH_INVALID_SOCIAL_TOKEN 401 Google/Apple token rejected
AUTH_PHONE_UNVERIFIED / AUTH_EMAIL_UNVERIFIED 403 Verification gate (booking / RFQ publish)
VENDOR_NOT_APPROVED 403 Vendor action before approval
SUBSCRIPTION_REQUIRED / SUBSCRIPTION_QUOTA_EXCEEDED 403 Plan gate / monthly quotation quota spent
RFQ_INVALID_STATE / QUOTATION_INVALID_STATE / BOOKING_INVALID_STATE 409 Illegal state-machine transition
QUOTATION_EXPIRED 409 valid_until passed
PAYMENT_PROVIDER_UNAVAILABLE 503 PSP session creation failed
PAYMENT_AMOUNT_MISMATCH 409 Webhook/redirect amount disagreement (flagged)
REVIEW_ALREADY_EXISTS / REVIEW_BOOKING_NOT_COMPLETED 409 Review gates
MEDIA_TYPE_NOT_ALLOWED / MEDIA_TOO_LARGE 422 Upload policy
SEARCH_UNAVAILABLE 503 Meilisearch down, degraded mode
OTP_DELIVERY_FAILED 502 SMS/email provider failure

Pagination, Filtering, Sorting

  • Page-based for most collections: ?page=1&per_page=20 (per_page max 50, default 20).
  • Cursor-based for messages and notifications (append-heavy): ?cursor=<opaque>&limit=30; response meta.next_cursor.
  • Filtering: flat query params named after fields — ?status=confirmed&city_id=...&category_id=...&price_min=100&price_max=500&rating_min=4&date_from=2026-09-01. Unknown filter params are rejected (422), not ignored — silent typos hide bugs.
  • Sorting: ?sort=-created_at (leading - = desc); allowed sort fields are per-endpoint allow-lists (e.g., search: rating, price_min, distance, created_at).
  • Sparse includes: ?include=vendor,items for optional relations (per-endpoint allow-list).

Idempotency Keys (Payments & Money)

Required on: POST /payments, POST /subscriptions, POST /admin/refunds.

Idempotency-Key: 3f7c2e9a-4b1d-4c68-9a55-8f0d2f1a6b3e
  • Client-generated UUID v4, unique per logical attempt; retries of the same attempt reuse the key.
  • Server stores key + request hash + response for 24 h: same key + same payload → replay the stored response (with Idempotency-Replayed: true); same key + different payload → 409 IDEMPOTENCY_KEY_REUSED.
  • Independently, webhooks are idempotent by provider reference — double protection on the money path.

Rate Limits

Returned on every response: X-RateLimit-Limit, X-RateLimit-Remaining; on 429: Retry-After (seconds).

Bucket Limit
Unauthenticated (per IP) 60/min
Authenticated (per user) 240/min
Login attempts 5/min per account+IP
OTP request 1/60 s and 5/h per phone
Search 60/min per user or IP
RFQ create 10/day per customer
Messages 30/min per user
Media pre-sign 60/h per user

Full abuse-control design: Security — rate limiting.

Localization

Accept-Language: ar   (or en; default: ar)
  • Affects: validation details messages, localized display fields, and error message stays English (developer-facing).
  • Bilingual entities return both languages plus a resolved display field, so clients can switch language without refetching:
{ "name": "قاعة الياسمين", "name_ar": "قاعة الياسمين", "name_en": "Al Yasmin Hall" }
  • Dates are ISO-8601 UTC; clients localize formatting and calendars. Money strings keep Western digits; clients render Eastern Arabic numerals if desired.

Webhook Signing (Outbound — Scale)

Inbound PSP webhook verification is specified in Integrations. When Yam3at itself emits webhooks (Scale: partner/enterprise APIs), the contract is:

X-Yam3at-Signature: t=1751721000,v1=hex(hmac_sha256(secret, t + "." + raw_body))
X-Yam3at-Event: booking.confirmed
X-Yam3at-Delivery: 01J9ZKQ8MZ...
  • Receivers must verify HMAC over the raw body, reject timestamp skew > 5 min, and respond 2xx within 10 s.
  • Retries: 5 attempts, exponential backoff over 24 h; deliveries are idempotent by X-Yam3at-Delivery id.
  • Secrets are per-endpoint and rotatable (two active secrets during rotation).

Miscellaneous

  • Clock skew: clients must not compute expiry locally from durations; the API always returns absolute timestamps (expires_at).
  • Compression: gzip/br supported; responses > 1 KB compressed.
  • CORS: allow-list of first-party origins only; the API is not a public CORS API in MVP.
  • OpenAPI: the spec is generated from code annotations and published per environment; client SDK types (TS/Dart) are generated from it in CI.

Endpoint catalog with examples: endpoints.md.