تخطي إلى المحتوى

Public REST API

The /api/v1 facade lets merchants integrate programmatically instead of using the portal. It's a thin REST wrapper around the same service functions the merchant tRPC router calls — see DOMAIN.md § API Access for the business rules (key format, access gating, request logging). This doc is the endpoint reference, kept up to date in the same PR as any route change (per STANDARDS.md § Documentation).

Routes live under apps/web/src/app/api/v1/. There is no OpenAPI generation for this API yet — this file is hand-maintained.

Authentication

Every request needs:

Authorization: Bearer dk_live_<key>

Keys are created and revoked by the merchant in the portal (Settings → API Keys), capped at 5 active keys per merchant. A key inherits its merchant's subscription: if the merchant's plan doesn't include apiAccess, every request 403s regardless of key validity.

StatusMeaning
401 UNAUTHORIZEDMissing, malformed, invalid, or revoked key
403 FORBIDDENKey is valid but the merchant's plan lacks apiAccess

Errors

Non-2xx responses always return this envelope:

{ "error": { "code": "BAD_REQUEST", "message": "human-readable detail" } }

code is a stable machine-readable string (matches the HTTP status), suitable for switching on in client code. message is for logs/debugging, not for parsing.

Endpoints

POST /api/v1/deliveries

Create a delivery. Identical validation and behavior (pricing, ETA, quota, dispatch) to the merchant portal's create flow — both call the same createDeliveryForMerchant() function.

Body (all fields required unless marked optional):

FieldTypeNotes
customerNamestring
customerPhonestring
pickupNeighborhoodIdstring
pickupLat, pickupLngnumber
pickupStreet, pickupBuilding, pickupFloor, pickupLandmark, pickupNotesstringoptional
dropoffNeighborhoodIdstring
dropoffLat, dropoffLngnumber
dropoffStreet, dropoffBuilding, dropoffFloor, dropoffLandmark, dropoffNotesstringoptional
packageDescriptionstring
packageSize"SMALL" | "MEDIUM" | "LARGE"optional
priority"STANDARD" | "URGENT"optional, default STANDARD
requiredVehicleType"BICYCLE" | "MOTORCYCLE" | "CAR" | "KEI_TRUCK" | "VAN"optional, default CAR
prepTimeMinutesintegeroptional, default 5
merchantAmountinteger (minor units)optional, default 0. Cash-on-delivery goods value
currencyCodestring (ISO 4217)optional, defaults to the merchant's account currency
pricingType"PAID" | "FREE"optional, default PAID. FREE zeroes the customer fee and consumes the plan's free-delivery quota; the real distance-based driver cost is still computed either way

201 — returns the created Delivery row (id, trackingCode, status, merchantId, customerName, customerPhone, pickup/dropoff fields, packageDescription, packageSize, priority, requiredVehicleType, prepTimeMinutes, estimatedDeliveryAt, driverId, dispatchRound, createdAt, updatedAt). Pricing/financial fields are not on this row — fetch GET /deliveries/:id for those.

Errors:

StatusCodeCause
400BAD_REQUESTSchema validation failed, or the neighborhood id is invalid
403FORBIDDENMerchant is suspended, or has an overdue invoice
429TOO_MANY_REQUESTSSubscription's delivery quota, or free-delivery quota (when pricingType: "FREE"), is exhausted for this period
500INTERNAL_SERVER_ERRORPricing/ETA misconfiguration, or dispatch failed to start

GET /api/v1/deliveries

List the merchant's deliveries, newest first, cursor-paginated.

Query params:

ParamTypeNotes
statusDeliveryStatusoptional — one of CREATED, SEARCHING_DRIVER, DRIVER_ASSIGNED, PICKUP_STARTED, PICKED_UP, IN_TRANSIT, DELIVERED, COMPLETED, CANCELLED, FAILED, EXPIRED
searchstringoptional — matches tracking code or customer name
cursorstringoptional — from a previous response's nextCursor
limitintegeroptional, 1–100, default 20

200:

{
  "items": [
    {
      "id": "...",
      "trackingCode": "...",
      "customerName": "...",
      "status": "SEARCHING_DRIVER",
      "createdAt": "2026-08-13T10:00:00.000Z",
      "customerFee": 15000,
      "currencyCode": "SYP",
      "driverName": null
    }
  ],
  "nextCursor": "cly...xyz"
}

nextCursor is null on the last page.

GET /api/v1/deliveries/:id

Fetch one delivery, with financial snapshot, status history, assigned driver, and live dispatch offers — the same detail payload the merchant portal's delivery detail page uses.

200 — the delivery row plus financial, statusHistory (chronological), assignedDriver, offers, city, pickupNeighborhood, dropoffNeighborhood.

404NOT_FOUND if the id doesn't exist or belongs to a different merchant.

GET /api/v1/pricing/quote

Calculate the price for a delivery before creating it — same pricing engine POST /deliveries uses internally, so a quote never drifts from what creation actually charges. No delivery is created; this is a pure calculation, safe to call as often as needed (e.g. to reprice as the customer edits pickup/dropoff in your own UI).

Query params:

ParamTypeNotes
vehicleType"BICYCLE" | "MOTORCYCLE" | "CAR" | "KEI_TRUCK" | "VAN"required
priority"STANDARD" | "URGENT"required
packageSize"SMALL" | "MEDIUM" | "LARGE"optional
pickupLat, pickupLngnumberrequired
dropoffLat, dropoffLngnumberrequired
currencyCodestring (ISO 4217)optional, defaults to the merchant's account currency

200:

{
  "customerFee": 15000,
  "driverAmount": 12000,
  "platformAmount": 3000,
  "currencyCode": "SYP",
  "pricingRuleRef": "fee:cly...,commission:cly...",
  "distanceKm": 4.21
}

Errors:

StatusCodeCause
400BAD_REQUESTMissing or invalid query params
500INTERNAL_SERVER_ERRORNo active pricing/commission rule is configured for this merchant and currency

Adding a New Endpoint

Follow this shape so new endpoints stay consistent and REST/tRPC can't drift apart:

  1. Reuse the service layer. If a tRPC procedure already does this, extract or reuse its underlying service function (see createDeliveryForMerchant() in packages/api/src/domains/delivery/service.ts) instead of writing parallel logic in the route handler. Share the zod input schema too if one exists (see create-input.ts).
  2. Wrap the route in withApiAuth. Add route.ts under apps/web/src/app/api/v1/<resource>/, using withApiAuth(req, handler) from ../_auth.ts for auth + request logging — don't reimplement key checking.
  3. Map service errors to HTTP status explicitly, one if (error instanceof X) per typed error class, falling through to a rethrow for anything unexpected (see deliveries/route.ts).
  4. Add route tests mocking the service/db module (see deliveries/route.test.ts) — auth failure cases (401/403) plus the endpoint's own success/error paths.
  5. Document it here: one ### section per route, same shape as above (body/query table, response shape, error table).
  6. Update DOMAIN.md if the change affects business rules, and mark the roadmap slice shipped in ROADMAP.md.

tRPC procedures used by the mobile driver app

These live on the tRPC surface (/api/trpc), not /api/v1 — the driver app authenticates with Better Auth bearer sessions (ADR-0007), not merchant API keys. They are documented here because they are API surface.

driver.reportLocation (authenticated — driverProcedure)

Body: { "points": [{ "lat", "lng", "accuracyMeters"?, "speedMps"?, "headingDeg"?, "recordedAt" }] } — max 20 points per batch. Server-gated on the driver being ONLINE/BUSY and rate-limited per driver. Returns { "received": number }. Append-only writes to DriverLocation.

delivery.getDriverPosition (public — publicProcedure)

Input: { "trackingCode" }. Returns the delivery's assigned driver's latest position { "lat", "lng", "updatedAt" } or null. Redis rate-limited per ip+trackingCode. Position is hidden when the delivery is CANCELLED/FAILED/EXPIRED or the latest fix is > 5 minutes old. Never returns the trail or driver identity.