Admin stations

Back-office management of filling stations (water and fuel): listing, CRUD, status toggling, report/comment moderation, and bulk CSV import. Paths are relative to the /api/v1 prefix; these are back-office endpoints that require an admin token.

Requires an admin bearer token (see Admin auth). Admin only.

The admin station API speaks Spanish: tipo is AGUA/GASOLINA, estado is ACTIVA/INACTIVA (the list filter accepts lowercase activa/inactiva/todas), and report colors are verde/amarillo/rojo/cerrada.

See Common errors and the public Stations endpoints.

Endpoints

Method URI Access Description
GET /admin/stations/bulk-import/template Admin Download the CSV import template
POST /admin/stations/bulk-import Admin Bulk import stations from a CSV
GET /admin/stations/bulk-import/{jobId} Admin Status of an import job
GET /admin/stations Admin Paginated station list (cursor-based)
POST /admin/stations Admin Create a station
GET /admin/stations/{stationId} Admin Station detail with recent reports
PATCH /admin/stations/{stationId} Admin Update a station
PATCH /admin/stations/{stationId}/status Admin Activate / deactivate a station
DELETE /admin/stations/{stationId} Admin Soft-delete a station
GET /admin/stations/{stationId}/reports Admin Paginated reports (cursor-based)
GET /admin/stations/{stationId}/comments Admin Paginated comments (cursor-based)
DELETE /admin/stations/{stationId}/comments/{commentId} Admin Moderate (delete) a comment

GET /admin/stations/bulk-import/template

Downloads the CSV import template (text/csv, plantilla_estaciones.csv); there is no JSON body. The header row is name, address, latitude, longitude, tipo, description, landmark, city, opening_time, closing_time (the first five are required). Pass con_ejemplos=true to include three sample rows.

Query

Field Type Required Rules
con_ejemplos boolean no include example rows (default false)

Response 200 — a text/csv stream.


POST /admin/stations/bulk-import

Uploads a CSV (multipart/form-data) to create or update stations. Up to 100 rows return a report (200); larger files (up to 2,000 rows) return a job id (202) to poll.

Request body

Field Type Required Rules
file file yes csv/txt, ≤ 5 MB

Response 200 (≤ 100 rows)

{
  "report": {
    "processed": 80,
    "created": 72,
    "updated": 5,
    "skipped": 3,
    "skipped_rows": [
      { "line": 14, "reason": "Coordenadas fuera de Venezuela." }
    ]
  }
}

Response 202 (more than 100 rows)

{ "job_id": 9, "status": "PENDING", "total_rows": 540 }

Errors

Status Body When
422 { "error": "InvalidEncoding", "message": "…" } File is not UTF-8
422 { "error": "EmptyFile", "message": "…" } File is empty
422 { "error": "MissingHeaders", "message": "…" } Required headers missing
422 { "error": "TooManyRows", "message": "…" } More than 2,000 rows

GET /admin/stations/bulk-import/{jobId}

Polls the status of an import job. Only the admin who launched the job may read it.

Response 200

{
  "job_id": 9,
  "status": "COMPLETED",
  "total_rows": 540,
  "report": {
    "processed": 540,
    "created": 500,
    "updated": 30,
    "skipped": 10,
    "skipped_rows": [ { "line": 22, "reason": "Tipo inválido: 'X' (use AGUA o GASOLINA)." } ]
  },
  "error": null,
  "created_at": "2026-06-26T12:00:00+00:00",
  "updated_at": "2026-06-26T12:01:10+00:00"
}

report and error are null until the job finishes.

Errors

Status Body When
404 { "error": "ImportJobNotFound" } Unknown job id
403 { "error": "Forbidden", "message": "…" } The job belongs to another admin

GET /admin/stations

Cursor-paginated station list, newest first. See Pagination → cursor-based.

Query

Field Type Required Rules
tipo string no AGUA or GASOLINA
estado string no activa, inactiva, or todas (default todas)
q string no ≤ 120 chars; matches name or address
limit integer no 1–100, default 25
cursor string no opaque cursor from next_cursor

Response 200

{
  "items": [
    {
      "id": 5,
      "name": "Pozo La Trinidad",
      "address": "Av. Principal, Caracas",
      "tipo": "AGUA",
      "estado": "ACTIVA",
      "last_report_at": "2026-06-26T11:00:00+00:00"
    }
  ],
  "next_cursor": "eyJpZCI6NH0",
  "has_more": true
}

POST /admin/stations

Creates a station. Coordinates must fall within Venezuela, and a station with the same name within 50 m is rejected as a duplicate. New stations start ACTIVA.

Request body

Field Type Required Rules
name string yes ≤ 160 chars
address string yes ≤ 280 chars
latitude number yes between −90 and 90
longitude number yes between −180 and 180
tipo string yes AGUA or GASOLINA
description string no ≤ 1000 chars
landmark string no ≤ 280 chars
city string no ≤ 120 chars; defaults to Caracas
opening_time string no HH:MM
closing_time string no HH:MM

Response 201{ "station": { … } } (the detail object, see below).

Errors

Status Body When
422 { "error": "CoordsOutOfRange", "message": "…" } Coordinates outside Venezuela
422 { "error": "DuplicateStation", "message": "…" } Same name within 50 m exists

GET /admin/stations/{stationId}

Station detail with its reports from the last 30 days and an aggregate summary.

Response 200

{
  "station": {
    "id": 5,
    "name": "Pozo La Trinidad",
    "address": "Av. Principal, Caracas",
    "description": "Pozo comunitario",
    "landmark": "Frente a la plaza",
    "latitude": 10.4806,
    "longitude": -66.9036,
    "city": "Caracas",
    "tipo": "AGUA",
    "estado": "ACTIVA",
    "is_verified": true,
    "opening_time": "06:00",
    "closing_time": "18:00",
    "created_at": "2026-06-01T08:00:00+00:00",
    "updated_at": "2026-06-26T11:00:00+00:00"
  },
  "reports": [
    {
      "id": 31,
      "color": "verde",
      "wait_minutes": 10,
      "comment": "Cola corta",
      "reporter": "Carlos Rodríguez",
      "created_at": "2026-06-26T11:00:00+00:00"
    }
  ],
  "summary": {
    "total_reports_30d": 12,
    "avg_wait_minutes": 18,
    "color_breakdown": { "verde": 7, "amarillo": 3, "rojo": 2, "cerrada": 0 }
  }
}

reporter is null for reports with no identifiable author.

Errors

Status Body When
404 { "error": "StationNotFound" } Unknown station id

PATCH /admin/stations/{stationId}

Partial update. Any omitted field is left unchanged. Moved coordinates must fall within Venezuela.

Request body

Field Type Required Rules
name string no ≤ 160 chars
address string no ≤ 280 chars
latitude number no between −90 and 90
longitude number no between −180 and 180
tipo string no AGUA or GASOLINA
description string|null no ≤ 1000 chars
landmark string|null no ≤ 280 chars
city string no ≤ 120 chars
opening_time string|null no HH:MM
closing_time string|null no HH:MM

Response 200{ "station": { … } } (the detail object).

Errors

Status Body When
404 { "error": "StationNotFound" } Unknown station id
422 { "error": "CoordsOutOfRange", "message": "…" } Moved coordinates outside Venezuela

PATCH /admin/stations/{stationId}/status

Activates or deactivates a station.

Request body

Field Type Required Rules
estado string yes ACTIVA or INACTIVA

Response 200

{ "id": 5, "estado": "INACTIVA", "updated_at": "2026-06-29T10:00:00+00:00" }

Errors

Status Body When
404 { "error": "StationNotFound" } Unknown station id

DELETE /admin/stations/{stationId}

Deletes a station. Blocked when the station has any report in the last 7 days — deactivate it instead.

Response 200

{ "deleted": true }

Errors

Status Body When
404 { "error": "StationNotFound" } Unknown station id
422 { "error": "HasRecentReports", "message": "…" } Station has reports in the last 7 days

GET /admin/stations/{stationId}/reports

Cursor-paginated report history for a station, newest first. See Pagination → cursor-based.

Query

Field Type Required Rules
color string no verde, amarillo, rojo, or cerrada
from date no filter created_at >=
to date no filter created_at <=; >= from
limit integer no 1–100, default 25
cursor string no opaque cursor from next_cursor

Response 200

{
  "items": [
    {
      "id": 31,
      "color": "verde",
      "wait_minutes": 10,
      "comment": "Cola corta",
      "reporter": "Carlos Rodríguez",
      "created_at": "2026-06-26T11:00:00+00:00"
    }
  ],
  "next_cursor": "eyJpZCI6MzB9",
  "has_more": true
}

Errors

Status Body When
404 { "error": "StationNotFound" } Unknown station id

GET /admin/stations/{stationId}/comments

Cursor-paginated comment wall for a station (for moderation), newest first. See Pagination → cursor-based.

Query

Field Type Required Rules
limit integer no 1–100, default 25
cursor string no opaque cursor from next_cursor

Response 200

{
  "items": [
    {
      "id": 12,
      "author": { "id": 3, "name": "Carlos Rodríguez", "role": "DRIVER" },
      "content": "El pozo cerró temprano hoy.",
      "created_at": "2026-06-26T11:30:00+00:00"
    }
  ],
  "next_cursor": "eyJpZCI6MTF9",
  "has_more": true
}

Errors

Status Body When
404 { "error": "StationNotFound" } Unknown station id

DELETE /admin/stations/{stationId}/comments/{commentId}

Moderation: deletes any comment on the station's wall.

Response 200

{ "ok": true }

Errors

Status Body When
404 { "error": "CommentNotFound" } No such comment on this station