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

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

response = requests.post(url)

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

fetch('https://api.example.com/api/v1/admin/reports/:id/ban', 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/ban",
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/ban"

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/ban")
.asString();
require 'uri'
require 'net/http'

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

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

Terminally bans the user named as the accused on this report. Internally this delegates to the same code path as POST /admin/users/:id/ban — there is exactly one ban implementation:
  1. Sets users.bannedAt = now, users.bannedReason = banReason, users.status = BANNED.
  2. Revokes all active refresh tokens for the user (the user is logged out of every device on next API call).
  3. Transitions the report from OPENBANNED with resolvedBy, resolvedAt, resolutionReason = banReason.
Idempotent: re-calling on a BANNED report returns the row unchanged. Re-banning a user who is already banned is a no-op (no double-stamp). See Ban user for the user-side ban semantics and admin auth and MFA for how revoked refresh tokens propagate.

Authentication

Bearer <accessToken> with scope: 'admin' required.
Role-gated: SUPER or MODERATOR. FINANCE admins are read-only on the reports surface. Ban is destructive and immediately locks the user out — handle with care.

Path parameters

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

Query parameters

None.

Request body

FieldTypeRequiredNotesExample
banReasonstringyes1..500 chars. Stored on both users.bannedReason AND reports.resolutionReason. Surfaced to the user as ACCOUNT_BANNED detail on their next API call.Repeated harassment after prior warning.

Example payload

{
  "banReason": "Repeated harassment after prior warning."
}

Response — 200 OK

Returns the full updated report row.
FieldTypeNotes
idstringReport id.
statusstringAlways "BANNED".
resolvedBystringThe admin id (from the bearer token).
resolvedAtstring (ISO 8601)Server clock at the moment of resolution.
resolutionReasonstringEcho of banReason.
{
  "id": "665a3f1e9c2b0a0001a4d201",
  "status": "BANNED",
  "resolvedBy": "664f99...",
  "resolvedAt": "2026-05-23T09:14:11.412Z",
  "resolutionReason": "Repeated harassment after prior warning.",
  "...": "...remaining report fields per /api-reference/admin/reports/list..."
}

Error responses

StatusCodeMeaning
400VALIDATION_FAILEDbanReason missing, empty, or longer than 500 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, or the accused user no longer exists.
409STATE_CONFLICTReport is in DISMISSED or WARNED — terminal states never transition.

Example error — 403 FORBIDDEN

{
  "type": "https://api.swappr.co.uk/errors/forbidden",
  "title": "Forbidden",
  "status": 403,
  "code": "FORBIDDEN",
  "detail": "Admin role FINANCE is not permitted for this action",
  "instance": "/api/v1/admin/reports/665a.../ban",
  "requestId": "01HZQ7K3M4N5P6Q7R8S9T0V1W2"
}

Side effects

EffectNotes
User rowbannedAt, bannedReason, status = BANNED set.
Refresh tokensAll active refresh tokens revoked. The user is logged out of every device on next API call.
Report rowstatus = BANNED, resolvedBy, resolvedAt, resolutionReason set.
Audit rowaction: report.ban, targetType: report, targetId: <reportId>. The downstream ban path also writes its own user.ban audit row.
Unlike warn, ban does not send a push or email to the banned user. They will discover the ban on their next API call (which returns 403 ACCOUNT_BANNED with the banReason in detail).

See also

curl

curl -X POST https://api.swappr.co.uk/api/v1/admin/reports/665a3f1e9c2b0a0001a4d201/ban \
  -H "Authorization: Bearer $ADMIN_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"banReason":"Repeated harassment after prior warning."}'

Postman

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