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.
| 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 |
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 |
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.
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 |
Response 200 — { "promotion": {promotion} }
Errors
| Status | When |
|---|---|
404 |
PromotionNotFound — no campaign with that id |
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 |
Response 200 — { "deleted": true }
Errors
| Status | When |
|---|---|
404 |
PromotionNotFound — no campaign with that id |
409 |
PromotionHasGrants — grants_count > 0; deactivate it instead |
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 |
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}"