POST
/
api
/
v1
/
admin
/
reports
/
:id
/
warn
Warn user (from report)
curl --request POST \
  --url https://api.example.com/api/v1/admin/reports/:id/warn
import requests

url = "https://api.example.com/api/v1/admin/reports/:id/warn"

response = requests.post(url)

print(response.text)
const options = {method: 'POST'};

fetch('https://api.example.com/api/v1/admin/reports/:id/warn', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/admin/reports/:id/warn",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
package main

import (
"fmt"
"net/http"
"io"
)

func main() {

url := "https://api.example.com/api/v1/admin/reports/:id/warn"

req, _ := http.NewRequest("POST", url, nil)

res, _ := http.DefaultClient.Do(req)

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.post("https://api.example.com/api/v1/admin/reports/:id/warn")
.asString();
require 'uri'
require 'net/http'

url = URI("https://api.example.com/api/v1/admin/reports/:id/warn")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)

response = http.request(request)
puts response.read_body

Overview

Issues a formal warning to the user the report names as the accused. Transitions the report from OPENWARNED and stamps resolvedBy, resolvedAt, resolutionReason = reason. The warning is delivered out-of-band via:
  1. A critical-kind push notification — per push notification fan-out, the critical kind bypasses notificationsMuted because a moderation action is a safety signal the user must see (QUESTIONS.md §7.4).
  2. A warning email to the accused’s primary email, in parallel with the push.
Idempotent: re-warning a WARNED report returns the row without re-firing either side-effect. Side-effect failures (FCM down, mail provider down) are logged and swallowed — they never roll back the DB transition (matches the Phase 5/6 push/notification producer policy). See Admin reports feed.

Authentication

Bearer <accessToken> with scope: 'admin' required.
Role-gated: SUPER or MODERATOR. FINANCE admins are read-only on the reports surface.

Path parameters

FieldTypeRequiredNotesExample
idstringyesThe report id from the list endpoint. 1..64 chars.665a3f1e9c2b0a0001a4d201

Query parameters

None.

Request body

FieldTypeRequiredNotesExample
reasonstringyes1..1000 chars. Stored on reports.resolutionReason AND surfaced in the warning email + push payload to the accused user. Write this from the user’s perspective — they will read it.Please stop sending messages after another user asks you to.

Example payload

{
  "reason": "Please stop sending messages after another user asks you to."
}

Response — 200 OK

Returns the full updated report row.
FieldTypeNotes
idstringReport id.
statusstringAlways "WARNED".
resolvedBystringThe admin id (from the bearer token).
resolvedAtstring (ISO 8601)Server clock at the moment of resolution.
resolutionReasonstringEcho of the request reason.
{
  "id": "665a3f1e9c2b0a0001a4d201",
  "status": "WARNED",
  "resolvedBy": "664f99...",
  "resolvedAt": "2026-05-23T09:14:11.412Z",
  "resolutionReason": "Please stop sending messages after another user asks you to.",
  "...": "...remaining report fields per /api-reference/admin/reports/list..."
}

Error responses

StatusCodeMeaning
400VALIDATION_FAILEDreason missing, empty, or longer than 1000 chars.
401UNAUTHENTICATEDMissing / malformed / expired admin token, or non-admin scope.
403FORBIDDENCaller is a FINANCE admin. SUPER or MODERATOR required.
404NOT_FOUNDNo report exists with that id.
409STATE_CONFLICTReport is in DISMISSED or BANNED — terminal states never transition.

Example error — 400 VALIDATION_FAILED

{
  "type": "https://api.swappr.co.uk/errors/validation-failed",
  "title": "Validation failed",
  "status": 400,
  "code": "VALIDATION_FAILED",
  "detail": "Request body failed validation",
  "errors": [{ "path": "reason", "message": "String must contain at least 1 character(s)" }],
  "instance": "/api/v1/admin/reports/665a.../warn",
  "requestId": "01HZQ7K3M4N5P6Q7R8S9T0V1W2"
}

Side effects

EffectNotes
Report row → WARNEDresolvedBy, resolvedAt, resolutionReason set.
Push notification to accusedkind: 'critical'. Bypasses notificationsMuted. Payload includes the moderator’s reason.
Email to accusedSent in parallel to push. Subject + body include the moderator’s reason.
Audit rowaction: report.warn, targetType: report, targetId: <reportId>.
Push and email are best-effort — if either fails the failure is logged and the warning still counts (the DB row is WARNED). The accused user is not silently let off the hook by a flaky FCM token or email provider.

See also

curl

curl -X POST https://api.swappr.co.uk/api/v1/admin/reports/665a3f1e9c2b0a0001a4d201/warn \
  -H "Authorization: Bearer $ADMIN_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Please stop sending messages after another user asks you to."}'

Postman

See docs/postman/swappr.postman_collection.jsonAdmin Reports → Warn.