API Reference

Base URL for all endpoints: /api/* (backend-js, port 3001 in development).

All authenticated endpoints require Authorization: Bearer <access_token> header except where noted. Errors always return { "error": "message string" }.

[!NOTE] Request and response shapes verified from DTO source files, repository types, and frontend client types. Pagination defaults and admin-only endpoint responses marked Needs verification where not covered by frontend clients.

Error response format

All endpoints return structured errors:

{ "error": "Human-readable error message" }

HTTP status codes used: 400 (validation), 401 (unauthenticated), 403 (forbidden role), 404 (not found), 409 (conflict), 500 (server error).

Auth endpoints

POST /api/auth/login

Public. Authenticates user, issues access token and sets refresh token cookie.

Request body

{ "email": "user@example.com", "password": "secret" }

Response 200

{
  "user": {
    "id": 1,
    "name": "Maria García",
    "email": "user@example.com",
    "role": "distributor",
    "is_active": true
  },
  "access_token": "<JWT>"
}

Sets Set-Cookie: refresh_token=...; HttpOnly; SameSite=Lax (dev) / SameSite=None; Secure (prod).


POST /api/auth/refresh

Uses refresh token cookie. Rotates the token and issues new access token.

Request: no body, cookie sent automatically.

Response 200

{ "access_token": "<new JWT>" }

Sets new refresh_token cookie. Old token is revoked.


POST /api/auth/logout

Revokes the current refresh token and clears cookie.

Request: no body.

Response 200

{ "message": "Logged out" }

GET /api/auth/me

Returns the authenticated user’s profile. Requires Bearer token.

Response 200

{
  "id": 1,
  "name": "Maria García",
  "email": "user@example.com",
  "role": "distributor",
  "is_active": true,
  "created_at": "2025-01-15T10:30:00.000Z"
}

POST /api/auth/forgotPassword

Public. Triggers password reset email. Always returns success to avoid email enumeration.

Request body

{ "email": "user@example.com" }

Response 200

{ "message": "If that email is registered, a reset link has been sent." }

PATCH /api/auth/changePwd

Public token-based endpoint. Has two modes depending on body content.

Mode 1 — verify token only (no new_password):

{ "reset_pwd_token": "<token>" }

Response 200: { "valid": true }

Mode 2 — set new password:

{ "reset_pwd_token": "<token>", "new_password": "newSecret123" }

Response 200: { "message": "Password changed successfully" }

new_password must be at least 6 characters. Token is revoked after use.


POST /api/auth/activateAccount

Token-based. Activates a newly created account and sets its password.

Request: ?token=<activate_account_token> (query param) — Needs verification on exact body vs query param split.


User endpoints (admin only)

All endpoints in this group require role: admin.

POST /api/users/create

Creates a new user and sends an activation email.

Request body

{ "name": "Jon Doe", "email": "jon@example.com", "role": "distributor" }

role must be "admin" or "distributor". User is created inactive; activation email is sent to the provided email.

Response 201Needs verification for exact shape; expected to be the created user object.


GET /api/users/list

Query params

ParamTypeDefaultDescription
pagenumber1Page number
limitnumber20Max 100
role"admin" | "distributor"Filter by role
is_active"true" | "false"Filter by active status

Response 200Needs verification for exact pagination envelope shape.


GET /api/users/getById?id=<number>

Response 200 — user object. 404 if not found.


PATCH /api/users/update

Request bodyNeeds verification for allowed fields; expected { name?, email?, role?, is_active? }.


DELETE /api/users/remove?id=<number>

Removes user. A user cannot delete themselves.

Response 200 or 403 if attempting self-deletion.


PATCH /api/users/changeMyPwd

Any authenticated role. Changes own password.

Request body

{ "current_password": "oldSecret", "new_password": "newSecret123" }

Response 200

{ "message": "Password updated successfully" }

Package endpoints

POST /api/packages/create — admin

Creates a package, geocodes the address, generates a tracking token, and sends a tracking email.

Request body

{
  "packageInfo": {
    "recipient_name": "Ane Etxeberria",
    "recipient_email": "ane@example.com",
    "assigned_to": null,
    "created_by": 1,
    "status": "pending",
    "weight_kg": 2.5,
    "description": "Fragile"
  },
  "address_info": {
    "street": "Kale Nagusia 12",
    "city": "Tolosa",
    "postal_code": "20400"
  }
}

assigned_to can be null (unassigned) or a valid user ID. description is optional.

Response 201Needs verification for exact package shape.


GET /api/packages/list — admin

Query paramsNeeds verification for full filter set and pagination envelope.


GET /api/packages/getById?id=<number> — admin

Response 200PackageWithAddress:

{
  "id": 42,
  "tracking_code": "PAK-20260001",
  "recipient_name": "Ane Etxeberria",
  "recipient_email": "ane@example.com",
  "weight_kg": 2.5,
  "description": "Fragile",
  "status": "pending",
  "estimated_delivery": "2026-05-01",
  "address_id": 7,
  "assigned_to": null,
  "created_by": 1,
  "created_at": "2026-04-26T10:00:00.000Z",
  "updated_at": "2026-04-26T10:00:00.000Z",
  "street": "Kale Nagusia 12",
  "city": "Tolosa",
  "postal_code": "20400",
  "latitude": 43.1298,
  "longitude": -2.0773,
  "country": "España"
}

GET /api/packages/getMyPackages — distributor

Returns all packages assigned to the authenticated distributor.

Response 200 — array of PackageWithAddress.


PATCH /api/packages/updateStatus — distributor

Updates the status of one or multiple packages. Exactly one of package_id or package_ids must be provided.

Request body (single)

{ "package_id": 42, "new_status": "in_transit" }

Request body (batch)

{ "package_ids": [42, 43, 44], "new_status": "delivered" }

new_status must be a valid package status: "pending" | "assigned" | "in_transit" | "delivered" | "undelivered" | "failed".

Response 200 — single PackageWithAddress (single update) or PackageWithAddress[] (batch).

Triggers status log insertion and email side effects (see Domain Model).


PATCH /api/packages/update?id=<number> — admin

Updates package info and address. Needs verification for allowed fields.


DELETE /api/packages/delete?id=<number> — admin

Response 200Needs verification.


GET /api/packages/getDailySummary — admin

Returns a count of packages per status for today.

Response 200Needs verification for exact shape; expected { pending: n, assigned: n, ... }.


Route and stop endpoints

POST /api/routes/create — admin

Creates an optimized delivery route for a distributor on a given date.

Request body

{ "user_id": 3, "date": "2026-05-02", "package_ids": [10, 11, 12] }
  • date must be in YYYY-MM-DD format and must be at least tomorrow.
  • package_ids must be non-empty, no duplicates, all valid package IDs.

Response 200Needs verification for exact route+stops envelope shape.


GET /api/routes/getByUserAndDate — admin

Query params: user_id, dateNeeds verification.


GET /api/routes/getMyDaily — distributor

Returns the route and stops for the authenticated distributor for today.

Response 200

{
  "route": { "id": 5, "route_date": "2026-04-26", "status": "in_progress" },
  "stops": [
    {
      "id": 21,
      "stop_order": 1,
      "estimated_arrival": "09:30:00",
      "actual_arrival": "09:28:00",
      "package": {
        "id": 42,
        "recipient_name": "Ane Etxeberria",
        "status": "delivered",
        "address": {
          "street": "Kale Nagusia 12",
          "city": "Tolosa",
          "lat": 43.1298,
          "lng": -2.0773
        }
      }
    }
  ]
}

PATCH /api/routes/updateStatus/:id — admin or distributor (scoped)

Request body

{ "status": "in_progress" }

status must be "in_progress" or "completed".

Response 200

{ "message": "Route status updated" }

GET /api/routes/continueFromPast — distributor

Checks if the distributor has a past route with pending stops.

Response 200

{
  "pending": {
    "route_id": 3,
    "route_date": "2026-04-25",
    "status": "in_progress",
    "pending_count": 2,
    "route_count": 5
  }
}

pending is null if no past pending route exists.


POST /api/routes/continueFromPast — distributor

Migrates pending stops from the last pending past route into today’s route.

Request: no body.

Response 200

{
  "route_id": 6,
  "new_route_date": "2026-04-26",
  "migrated_stops": 2
}

PATCH /api/stops/reorder — admin

Reorders stops within a route. Needs verification for body shape.


PATCH /api/stops/updateArrival — distributor

Records the actual arrival time at a stop.

Request body

{ "stop_id": 21 }

Response 200

{
  "id": 21,
  "route_id": 5,
  "package_id": 42,
  "stop_order": 1,
  "estimated_arrival": "09:30:00",
  "actual_arrival": "09:28:00"
}

Logs endpoints

GET /api/logs/listByPackage — admin

Query params: packageId (required), page, limit.

Response 200

{
  "logs": [
    {
      "id": 1,
      "packageId": 42,
      "oldStatus": "assigned",
      "newStatus": "in_transit",
      "changedBy": 3,
      "notes": null,
      "changedAt": "2026-04-26T09:00:00.000Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 20
}

GET /api/logs/listAll — admin

Lists all status logs across all packages. Needs verification for filters and pagination envelope.


Tracking endpoint (public)

GET /api/tracking/:trackingToken

Public. No auth required. Token is a 32-byte hex string.

Response 200

{
  "tracking_code": "PAK-20260001",
  "recipient_name": "Ane Etxeberria",
  "status": "in_transit",
  "estimated_delivery": "2026-05-01",
  "address": {
    "street": "Kale Nagusia 12",
    "city": "Tolosa",
    "postal_code": "20400"
  },
  "last_update": "2026-04-26T09:00:00.000Z"
}

Token expires after TRACKING_EXPIRES_DAYS days (default 30). Returns 404 if token is unknown or expired.