Admin promotions

CRUD for milestone promotion campaigns: on the Nth completed delivery, the audience receives an amount — spendable credit for a client, wallet money for a driver. milestone_basis says whether that Nth delivery is counted over the party's whole history or only from the campaign's start. Paths are relative to the /api/v1 prefix; these are back-office endpoints requiring a super-admin token.

Requires a super-admin bearer token (see Admin auth). Super-admin only.

See Common errors.

Endpoints

Method URI Access Description
GET /admin/promotions Super admin List campaigns
POST /admin/promotions Super admin Create a campaign
GET /admin/promotions/{promotionId} Super admin Show one campaign
PATCH /admin/promotions/{promotionId} Super admin Update a campaign
DELETE /admin/promotions/{promotionId} Super admin Delete a campaign
GET /admin/promotions/{promotionId}/grants Super admin List who received it

The promotion object

index, show, store and update present each campaign the same way:

{
  "id": 1,
  "name": "Bienvenida",
  "description": null,
  "audience": "CLIENT",
  "credit_scope": "SURPLUS",
  "milestone_orders": 2,
  "milestone_basis": "LIFETIME",
  "amount_usd": 10,
  "city_id": null,
  "starts_at": null,
  "ends_at": null,
  "max_grants": null,
  "grants_count": 0,
  "granted_amount_usd": 0,
  "active": true,
  "is_running": true,
  "created_at": "2026-08-27T12:00:00+00:00"
}
Field Type Description
audience string CLIENT or DRIVER
credit_scope string|null Where the granted credit can be spent: ANY, ORDERS or SURPLUS. Always null for DRIVER
milestone_orders integer Completed deliveries that trigger the grant
milestone_basis string Which deliveries the milestone counts: LIFETIME (the party's all-time deliveries) or CAMPAIGN (only those made since the campaign opened)
amount_usd number Amount granted
city_id integer|null Restricts the campaign to one city; null is platform-wide
starts_at / ends_at string|null ISO-8601 campaign window; null is open-ended
max_grants integer|null Beneficiary cap; null is uncapped
grants_count integer How many people already received it
granted_amount_usd number Total granted so far
active boolean The admin's on/off switch
is_running boolean active, inside the window, and under the cap

GET /admin/promotions

Query parameters

Field Type Required Rules
audience string No CLIENT or DRIVER
credit_scope string No ANY, ORDERS or SURPLUS
active boolean No —
city_id integer No —

Response 200 — { "items": [ {promotion}, … ] }, newest first.


POST /admin/promotions

Request body

Field Type Required Rules
name string Yes max:120
description string|null No max:255
audience string Yes CLIENT or DRIVER
credit_scope string Conditional Required when audience is CLIENT; prohibited when DRIVER. ANY, ORDERS or SURPLUS
milestone_orders integer Yes min:1, max:1000
milestone_basis string No LIFETIME or CAMPAIGN. Defaults to LIFETIME
amount_usd number Yes min:0.01, max:10000
city_id integer|null No Must exist in cities
starts_at date|null No —
ends_at date|null No after:starts_at
max_grants integer|null No min:1
active boolean No Defaults to true

Response 201 — { "promotion": {promotion} }

Errors

Status When
401 Missing or invalid bearer token
403 The admin is not a super admin
422 Validation failed, including a DRIVER campaign carrying credit_scope or a CLIENT campaign missing it

GET /admin/promotions/{promotionId}

Response 200 — { "promotion": {promotion} }

Errors

Status When
404 PromotionNotFound — no campaign with that id

PATCH /admin/promotions/{promotionId}

Merge-PATCH: only the fields sent are changed.

Request body

Field Type Required Rules
name string No max:120
description string|null No max:255
audience string No CLIENT or DRIVER
credit_scope string|null No ANY, ORDERS or SURPLUS. Must be null when the effective audience is DRIVER, and non-null when it is CLIENT (the effective value is the sent one, or the stored one if omitted)
milestone_orders integer No min:1, max:1000
milestone_basis string No LIFETIME or CAMPAIGN
amount_usd number No min:0.01, max:10000
city_id integer|null No Must exist in cities
starts_at date|null No —
ends_at date|null No Must be after starts_at (the sent value, or the stored one if starts_at is omitted)
max_grants integer|null No min:1
active boolean No —

Response 200 — { "promotion": {promotion} }

Errors

Status When
404 PromotionNotFound — no campaign with that id
409 PromotionAlreadyGranted — grants_count > 0 and the body carries audience, credit_scope, milestone_orders, milestone_basis or amount_usd. The response lists the offending fields
422 Validation failed, including an effective DRIVER campaign carrying credit_scope or an effective CLIENT campaign left without it

DELETE /admin/promotions/{promotionId}

Response 200 — { "deleted": true }

Errors

Status When
404 PromotionNotFound — no campaign with that id
409 PromotionHasGrants — grants_count > 0; deactivate it instead

GET /admin/promotions/{promotionId}/grants

Cursor-paginated list of who received the campaign, newest first.

Query parameters

Field Type Required Rules
limit integer No min:1, max:100 (default 25)
cursor string No Cursor from a previous response

Response 200

{
  "items": [
    {
      "id": 8,
      "user": { "id": 3, "name": "Ana", "phone": "+58414…" },
      "order_id": 91,
      "audience": "CLIENT",
      "credit_scope": "SURPLUS",
      "amount_usd": 10,
      "created_at": "2026-08-27T12:00:00+00:00"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
Field Type Description
items[].user object Beneficiary: id, name, phone
items[].order_id integer|null The delivery that hit the milestone
items[].audience string Frozen at grant time
items[].credit_scope string|null Frozen at grant time
items[].amount_usd number Frozen at grant time; unaffected by later edits

Errors

Status When
404 PromotionNotFound — no campaign with that id
422 Validation failed

Examples

curl -X POST /api/v1/admin/promotions \
  -H "Authorization: Bearer {superAdminToken}" \
  -H "Content-Type: application/json" \
  -d '{"name":"Bienvenida","audience":"CLIENT","credit_scope":"ANY","milestone_orders":2,"amount_usd":10}'

curl -X POST /api/v1/admin/promotions \
  -H "Authorization: Bearer {superAdminToken}" \
  -H "Content-Type: application/json" \
  -d '{"name":"5 pedidos","audience":"CLIENT","credit_scope":"ANY","milestone_orders":5,"milestone_basis":"CAMPAIGN","amount_usd":10}'

curl "/api/v1/admin/promotions?audience=DRIVER&active=true" \
  -H "Authorization: Bearer {superAdminToken}"

curl /api/v1/admin/promotions/1/grants -H "Authorization: Bearer {superAdminToken}"