Orders

The customer-facing order lifecycle: create an order, track the driver, confirm delivery, and read receipts. Paths are relative to the /api/v1 prefix; all routes require Authorization: Bearer {token}.

Driver bidding (accept / counter-offer) and the client accept/reject of offers live under Order offers (/orders/{id}/offer, /orders/{id}/accept, /counter-offers/{id}/accept). Driver-side listings live under Drivers (/driver/orders, /driver/online). Both are documented separately.

See Common errors and the order lifecycle.

Endpoints

Method URI Role Description
POST /orders CLIENT Create an order (starts broadcasting)
GET /orders CLIENT Paginated order history (cursor)
GET /orders/active CLIENT The caller's current active order
GET /orders/history CLIENT Recent orders (flat list)
POST /orders/{orderId}/start DRIVER Start the assigned trip
POST /orders/{orderId}/location DRIVER Push the driver's live location
POST /orders/{orderId}/arrived DRIVER Mark arrival at the destination
GET /orders/{orderId}/tracking CLIENT Live driver position, distance & ETA
POST /orders/{orderId}/deliver DRIVER Confirm delivery with the PIN
PATCH /orders/{orderId}/payment-method CLIENT Change the order's payment method
POST /orders/{orderId}/cancel any Cancel the order
GET /orders/cancellation-reasons any Cancellation-reason catalogue for the caller's role
GET /orders/{orderId}/receipt CLIENT Accounting receipt (delivered orders)
GET /orders/{orderId} any Order detail (owner or assigned driver)

The order object

Single-order responses wrap the order under an order key. The payload is the order record filtered by role:

client_charged_* is what the client actually pays: client_pays_* - promo_credit_*. With no promotional credit applied, promo_credit_* is 0 and client_charged_* equals client_pays_*.

is_fully_promoted is a boolean flag indicating whether the order was 100% covered by promotional credit (promo_credit_* >= client_pays_* and client_charged_* is 0).

The credit is applied automatically when the order is created, so POST /orders already returns it discounted. The total can still move afterwards (an accepted counter-offer, a payment-method change), and the credit is re-applied over the new one. promo_credit_* never exceeds client_pays_*, so client_charged_* is never negative. When a credit is applied the order also carries a promo_credit breakdown line of type adjustment with a negative amount_cents, hidden from the DRIVER view on non-cash orders.

Cash orders (CASH_USD) charge no solidary at all: client_solidary_*, driver_solidary_* and solidary_fund_* are 0, client_pays_* equals base_price_*, driver_receives_* equals driver_gross_*, and all three solidary breakdown lines are 0 (applied_fee_prc 0). Online methods apply the full 1% + 1% split.

delivery_pin is additionally only present once the order is paid (CONFIRMED/IN_PROGRESS/ARRIVED).

Representative CLIENT view:

{
  "order": {
    "id": 42,
    "status": "CONFIRMED",
    "latitude": 10.487,
    "longitude": -66.88,
    "address": "Av. Principal, Las Mercedes",
    "address_reference": "Frente a la panadería, portón azul",
    "liters_requested": 5000,
    "water_type": "POTABLE",
    "payment_method": "CASH_USD",
    "base_price_usd": 20.0,
    "client_solidary_usd": 0.0,
    "client_pays_usd": 20.0,
    "promo_credit_usd": 0.0,
    "client_charged_usd": 20.0,
    "is_fully_promoted": false,
    "delivery_pin": "1234",
    "notes": null,
    "created_at": "2026-06-26T12:00:00+00:00"
  }
}

POST /orders

Creates an order and starts broadcasting to nearby online drivers. A customer may only have one active order at a time.

Request body

Field Type Required Rules
latitude number yes between −90 and 90
longitude number yes between −180 and 180
address string yes 3–280 chars
addressReference string | null no max 255 chars
litersRequested integer yes 1–50000
basePrice number yes > 0, ≤ 100000
waterType string no POTABLE or INDUSTRIAL
paymentMethod string no an active method code or PAYMENT_FORM; defaults to CASH_USD
paymentFormId integer conditional required when paymentMethod=PAYMENT_FORM; must be an active payment form
notes string no ≤ 500 chars
accessInstructions string no ≤ 500 chars
billingProfileId integer | null conditional must be one of the caller's billing profiles. When omitted, the caller's default profile is used. Required when billing_required is on and the caller has no profile

Response 201 — { "order": { … } } (CLIENT view). Cash orders start at CONFIRMED; online methods start at WAITING_FOR_PAYMENT.

The order carries a billing object with the billing data as it stood when the order was created, or null when there was none:

{
  "billing": {
    "entity_type": "JURIDICA",
    "id_number": "J-123456789",
    "name": "Distribuidora Aguas del Valle, C.A.",
    "address": "Av. Libertador, Torre Norte, Piso 3, Caracas",
    "phone": "+58 212 5551234"
  }
}

billing.address is the billing address and is unrelated to the order's address, which is where the water is delivered. billing is present in the CLIENT and ADMIN views of an order; the DRIVER view never includes it.

Errors

Status Body When
409 { "error": "ActiveOrderExists", "message": "…" } The customer already has an active order
422 { "error": "ExchangeRateUnavailable", "message": "…" } There is no active BCV (VES) rate to snapshot on the order
422 { "error": "ValidationError", "details": { "fieldErrors": { "billingProfileId": [ … ] } } } billingProfileId is unknown or belongs to another customer, or billing is required and the customer has no billing profile

GET /orders

Cursor-paginated history for the authenticated customer. See Pagination → cursor-based.

Query: status, date_from, date_to, limit (1–100, default 25), cursor.

Response 200

{
  "items": [
    {
      "id": 42,
      "status": "DELIVERED",
      "liters_requested": 5000,
      "water_type": "POTABLE",
      "address": "Av. Principal, Las Mercedes",
      "client_pays_usd": 20.0,
      "promo_credit_usd": 0.0,
      "client_charged_usd": 20.0,
      "payment_method": "CASH_USD",
      "driver": { "id": 3, "name": "Carlos Rodríguez" },
      "expires_at": "2026-06-26T12:05:00+00:00",
      "created_at": "2026-06-26T12:00:00+00:00",
      "delivered_at": "2026-06-26T12:40:00+00:00"
    }
  ],
  "next_cursor": "eyJpZCI6NDF9",
  "has_more": true
}

GET /orders/active

Returns the caller's current active order, or null.

Response 200 — { "order": { … } } or { "order": null }. The delivery_pin is stripped unless the order is paid.


GET /orders/history

Recent orders as a flat list.

Query: limit (1–100, default 20).

Response 200 — { "orders": [ { … } ] } (each entry is a CLIENT-view order).


POST /orders/{orderId}/start

Driver starts the assigned trip (CONFIRMED → IN_PROGRESS).

Response 200 — { "order": { … } } (DRIVER view).

Status Body When
409 { "error": "Conflict", "message": "…" } Order not in a startable state

POST /orders/{orderId}/location

Push the driver's live GPS while serving an order (IN_PROGRESS/ARRIVED).

Request body

Field Type Required Rules
latitude number yes between −90 and 90
longitude number yes between −180 and 180

Response 204 No Content.

Status Body When
404 { "error": "OrderNotFound" } Unknown order
403 { "error": "Forbidden" } Caller is not the assigned driver
409 { "error": "WrongStatus", "message": "…" } Order not IN_PROGRESS/ARRIVED

POST /orders/{orderId}/arrived

Marks arrival. The driver must be within 500 m of the destination; passing latitude/longitude updates the live location first. Idempotent once ARRIVED.

Request body

Field Type Required Rules
latitude number no between −90 and 90
longitude number no between −180 and 180

Response 200

{ "ok": true, "status": "ARRIVED", "arrivedAt": "2026-06-26T12:35:00+00:00", "distanceMeters": 120.4 }

Errors

Status Body When
404 { "error": "OrderNotFound" } Unknown order
403 { "error": "Forbidden" } Caller is not the assigned driver
409 { "error": "WrongStatus", "message": "…" } Order not IN_PROGRESS
409 { "error": "NoLocation", "message": "…" } No recent driver location
409 { "error": "TooFarFromDestination", "message": "…", "distanceKm": 1.8 } Over 500 m away

GET /orders/{orderId}/tracking

Live driver position, straight-line distance, and a rough ETA (25 km/h). Only the order owner, only while CONFIRMED/IN_PROGRESS/ARRIVED.

Response 200

{
  "order": { "id": 42, "status": "IN_PROGRESS", "latitude": 10.487, "longitude": -66.88, "address": "Av. Principal", "expires_at": "2026-06-26T12:05:00+00:00" },
  "driver": {
    "id": 3,
    "name": "Carlos Rodríguez",
    "phone": "+584241001001",
    "currentLat": 10.49,
    "currentLng": -66.85,
    "lastLocationAt": "2026-06-26T12:34:00+00:00"
  },
  "distanceKm": 1.2,
  "estimatedMinutes": 3,
  "avgSpeedKmh": 25
}

driver is null (with distanceKm/estimatedMinutes null) until the driver has a known location.

Status Body When
403 { "error": "Forbidden" } Caller is not the order owner
409 { "error": "NoTracking", "message": "…" } Order not in a trackable state

POST /orders/{orderId}/deliver

Driver confirms delivery using the customer's 4-digit PIN.

Request body

Field Type Required Rules
actualLiters integer yes 1–50000
deliveryPin string yes exactly 4 digits

Response 200 — { "ok": true }.

Status Body When
400 { "error": "DeliveryError", "message": "…PIN…" } Wrong PIN
409 { "error": "DeliveryError", "message": "…" } Order not deliverable, or zero-cost order not marked as valid promotion

PATCH /orders/{orderId}/payment-method

Changes the order's payment method. Owner only.

Request body

Field Type Required Rules
paymentMethod string yes an active method code or PAYMENT_FORM
paymentFormId integer conditional required when paymentMethod=PAYMENT_FORM; must be an active payment form; ignored otherwise

Response 200 — { "order": { … } } (CLIENT view) with the updated payment_method; client_pays_usd, promo_credit_usd and client_charged_usd may change.

Errors

Status Body When
404 { "error": "OrderNotFound", "message": "…" } Unknown order or not owned by the caller
409 { "error": "PaymentMethodLocked", "message": "…" } Order past WAITING_FOR_PAYMENT, the client already reported a payment, or switching to cash after acceptance
422 { "error": "…" } Method not available, or missing/invalid paymentFormId

POST /orders/{orderId}/cancel

Cancels the order. Allowed for the order's customer or assigned driver (and admins).

Request body

Field Type Required Rules
reason_code string no a CancellationReason code valid for the caller's role (client or driver)
reason string no free-text note; ≤ 280 chars

Response 200 — { "order": { … } } (filtered for the caller's role).

Status Body When
403 { "error": "CancelError", "message": "…" } Caller may not cancel this order
404 { "error": "CancelError", "message": "…" } Order not found
409 { "error": "CancelError", "message": "…" } Order not cancellable
422 { "error": "ValidationError", … } reason_code not in the caller's role catalogue

GET /orders/cancellation-reasons

Cancellation-reason catalogue for the authenticated caller's active role (client or driver). Feeds the cancel dropdown.

Response 200

{ "reasons": [ { "code": "CLIENT_CHANGED_MIND", "label": "Cambié de opinión" } ] }

Reason codes by role:


GET /orders/{orderId}/receipt

Full accounting receipt for a delivered order, owner only.

Response 200

{
  "receipt": {
    "order_id": 42,
    "status": "DELIVERED",
    "delivered_at": "2026-06-26T12:40:00+00:00",
    "liters_requested": 5000,
    "actual_liters": 5000,
    "water_type": "POTABLE",
    "payment_method": "CASH_USD",
    "is_fully_promoted": false,
    "breakdown": {
      "base_price_usd": 20.0,
      "client_solidary_usd": 0.0,
      "client_pays_usd": 20.0,
      "promo_credit_usd": 0.0,
      "client_charged_usd": 20.0,
      "is_fully_promoted": false,
      "aguita_commission_usd": 3.6,
      "driver_gross_usd": 16.4,
      "driver_solidary_usd": 0.0,
      "driver_receives_usd": 16.4,
      "solidary_fund_usd": 0.0
    },
    "driver": {
      "id": 3,
      "name": "Carlos Rodríguez",
      "phone": "+584241001001",
      "vehicle": { "plate": "ABC-123", "capacity_liters": 5000 }
    },
    "rating": { "score": 5, "comment": "Puntual", "tags": ["fast"], "created_at": "2026-06-26T12:45:00+00:00" }
  }
}

rating is null until the customer rates the order. See Business → money breakdown.

Status Body When
403 { "error": "Forbidden" } Caller is not the order owner
409 { "error": "OrderNotDelivered", "message": "…" } Order not delivered yet

GET /orders/{orderId}

Order detail for the owner or the assigned driver. The payload is role-filtered, and delivery_pin is present only when the order is paid.

For the client, each pending order.offers[] adds distance_km (number|null), eta_minutes (number|null), and driver_busy (boolean). offers[].driver exposes only public fields — raw coordinates are not included. Same shape on GET /orders/active.

Response 200 — { "order": { … } }.

Status Body When
404 { "error": "OrderNotFound" } Unknown order
403 { "error": "Forbidden" } Caller is neither owner nor assigned driver

Examples

# Create (cash) → broadcasting/confirmed
curl -X POST /api/v1/orders \
  -H "Authorization: Bearer {client_token}" \
  -H "Content-Type: application/json" \
  -d '{"latitude": 10.487, "longitude": -66.88, "address": "Av. Principal", "litersRequested": 5000, "basePrice": 20}'

# Driver: live location, then mark arrived
curl -X POST /api/v1/orders/42/location \
  -H "Authorization: Bearer {driver_token}" \
  -H "Content-Type: application/json" \
  -d '{"latitude": 10.4895, "longitude": -66.8805}'

# Driver: confirm delivery with the client's PIN
curl -X POST /api/v1/orders/42/deliver \
  -H "Authorization: Bearer {driver_token}" \
  -H "Content-Type: application/json" \
  -d '{"actualLiters": 5000, "deliveryPin": "1234"}'