phpBB API Reference
dmzx/phpbbapi · extension

A REST API for your phpBB board

Full topic, post, poll, attachment and member management, moderation, private messages, search, a live event stream and outgoing webhooks — every request runs through phpBB’s own permission system, exactly as if that member were sitting at the keyboard.

phpBB 3.3.0+ & 4.0.x PHP 8.1+ JSON over HTTPS GPL-2.0-only
Overview

Nothing runs outside your permissions

An API credential is always bound to one real forum member. You grant it a set of scopes — the categories of action it’s allowed to attempt — and phpBB’s own permission system decides, on every single request, whether that action is actually allowed. A scope without the matching phpBB permission does nothing; a phpBB permission without the scope is unreachable. A credential can never do more than the member it’s bound to could do themselves, and every action still shows up in your normal moderation logs.

  • Topics & posts — create, read, edit, delete, bulk-fetch, polls, up to 10 attachments per post
  • Moderation — lock/unlock, soft-delete/restore, approve/disapprove, move, merge, split
  • Members — register, search, manage groups, ban/unban, delete accounts
  • Messaging & reports — private messages, notifications, moderator report queue
  • Real-time — a Server-Sent Events stream and 18 outgoing webhook events, signed with HMAC-SHA256
  • Operability — per-credential rate limits, IP/forum allowlists, expiry, token & secret rotation, a full ACP dashboard
Getting started

Installation

  1. Upload the extension to ext/dmzx/phpbbapi in your phpBB root, then enable it under ACP → Customise → Manage extensions.
  2. Grant the permission. Nobody can manage credentials until you do this: ACP → Permissions → Administrator permissions → “Can manage phpBB API credentials and view the request log” (a_phpbbapi). The founder account already has it.
  3. Check Settings. ACP → phpBB API → Settings — confirm the master switch is on, and decide whether to require HTTPS (on by default).
  4. Create a credential. ACP → phpBB API → API credentials. The token is shown once, immediately after creation — copy it before leaving the page.
Also in the ACP: an Overview tab (credential counts, requests in the last 24h, error rate, most-used routes), a Request log (every request ever made), and a Webhooks tab.
Authentication

Sending your token

Send the token in every request using one of two headers:

header
Authorization: Bearer <token>
header (fallback)
X-API-Key: <token>

Use X-API-Key if your host strips the Authorization header — common on Apache/FastCGI setups.

Entry point: app.php vs index.php

Endpoints are shown below as /app.php/api/v1/... — the version-safe choice, since index.php doesn’t route extension paths on 3.3.x. On a 4.0.x board, app.php still works but is now just a 301-redirect to the equivalent index.php URL.

Careful on 4.0.x: following that redirect can silently drop the request body on POST/PATCH/DELETE calls — many HTTP clients resend the method after a 301 but without the original body. Call /index.php/api/v1/... directly for write requests on a 4.0.x board, or make sure your client preserves the body across redirects.

PATCH/DELETE blocked by your host?

Some Apache/ModSecurity setups reject PATCH/DELETE outright with a bare 403, before phpBB ever sees the request. Send a normal POST to the same path instead, with either header:

header
X-HTTP-Method-Override: PATCH

…or a _method field in the body. It’s handled identically to a real request. A plain POST with no override on a PATCH/DELETE-only path returns method_override_required.

Access control

Scopes

A credential only has the scopes you explicitly grant it — checked on top of, never instead of, the bound member’s own phpBB permissions. Credentials can additionally be restricted to specific forums and/or caller IPs, rate-limited, and given an expiry date.

ScopeGrants
A scope only opens the door — the bound member still needs the matching phpBB permission behind it. members.write needs a_group/a_user for group management/registration and a_userdel to delete a member; messages.write needs u_sendpm; posts.attach needs f_attach; topics.vote needs f_vote (plus f_votechg to change a vote); members.ban needs a_ban; reports.* are further scoped per forum by m_report. phpBB itself refuses to ban, or delete, yourself or a founder account — regardless of permissions.
Reference

All endpoints

Every endpoint lives under /api/v1/ and returns application/json. Bodies may be sent as JSON or form-encoded.

MethodPathRequires
Content

Topics

POST /forums/{forum_id}/topics accepts title, content, and optional type (normal/sticky/announcement). Editing replaces both title and content — send the current value for whichever you’re not changing.

POST/forums/{forum_id}/topicstopics.write

Create a new topic.

request
curl -X POST https://example.com/app.php/api/v1/forums/2/topics \
  -H "X-API-Key: <token>" \
  -H "Content-Type: application/json" \
  -d '{"title": "Test topic via API", "content": "Created through the phpBB API extension."}'
200 response
{"success": true, "topic_id": 303, "post_id": 7572, "url": "https://example.com/viewtopic.php?t=303", "attachment_ids": []}
GET/topics/{topic_id}topics.read

Read a single topic. Bulk variant: GET /topics?ids=1,2,3 — unknown ids are silently omitted, capped at 100 ids per call.

request
curl https://example.com/app.php/api/v1/topics/303 -H "X-API-Key: <token>"
200 response
{"success": true, "topic": {"topic_id": 303, "forum_id": 2, "title": "Pick a colour", "type": "normal", "poster": "dmzx", "replies": 0, "views": 1, "locked": false, "url": "https://example.com/viewtopic.php?t=303", "poll": null}}
PATCH/topics/{topic_id}topics.write

Edit a topic’s title and first-post content. If your host blocks PATCH, send POST with X-HTTP-Method-Override: PATCH.

request
curl -X POST https://example.com/app.php/api/v1/topics/303 \
  -H "X-API-Key: <token>" -H "X-HTTP-Method-Override: PATCH" \
  -H "Content-Type: application/json" \
  -d '{"title": "Pick a colour (updated)", "content": "Vote below - now with more options."}'
200 response
{"success": true, "topic_id": 303, "post_id": 7572, "url": "https://example.com/viewtopic.php?t=303"}
DELETE/topics/{topic_id}topics.delete + m_delete

Permanently delete a topic. For a reversible alternative see soft-delete under Moderation below.

request
curl -X POST https://example.com/app.php/api/v1/topics/303 \
  -H "X-API-Key: <token>" -H "X-HTTP-Method-Override: DELETE"
200 response
{"success": true}
POST/topics/{topic_id}/watchtopics.watch

Watch or unwatch a topic (DELETE to unwatch) — same mechanism as “Watch topic” in the UCP. Calling either state twice is a harmless no-op. The same pattern applies to /forums/{forum_id}/watch.

request
curl -X POST   https://example.com/app.php/api/v1/topics/303/watch -H "X-API-Key: <token>"
curl -X DELETE https://example.com/app.php/api/v1/topics/303/watch -H "X-API-Key: <token>"
Content

Posts

POST/topics/{topic_id}/postsposts.write

Reply to a topic.

request
curl -X POST https://example.com/app.php/api/v1/topics/303/posts \
  -H "X-API-Key: <token>" -H "Content-Type: application/json" \
  -d '{"content": "This is a reply."}'
200 response
{"success": true, "topic_id": 303, "post_id": 7573, "url": "https://example.com/viewtopic.php?t=303&p=7573#p7573", "attachment_ids": []}
GET/posts/{post_id}posts.read

Read a single post. Bulk variant: GET /posts?ids=1,2,3, same rules as bulk topics.

request
curl https://example.com/app.php/api/v1/posts/7573 -H "X-API-Key: <token>"
200 response
{"success": true, "post": {"post_id": 7573, "poster": "dmzx", "subject": "Re: Pick a colour", "content_html": "This is a reply.", "attachments": []}, "topic": {"topic_id": 303, "forum_id": 2, "locked": false}}
PATCH/posts/{post_id}posts.write

Edit a post’s content.

request
curl -X POST https://example.com/app.php/api/v1/posts/7573 \
  -H "X-API-Key: <token>" -H "X-HTTP-Method-Override: PATCH" \
  -H "Content-Type: application/json" -d '{"content": "Edited content."}'
200 response
{"success": true, "topic_id": 303, "post_id": 7573, "url": "https://example.com/viewtopic.php?t=303&p=7573#p7573"}
DELETE/posts/{post_id}posts.delete + m_delete

Permanently delete a post.

request
curl -X POST https://example.com/app.php/api/v1/posts/7573 \
  -H "X-API-Key: <token>" -H "X-HTTP-Method-Override: DELETE"
200 response
{"success": true}
Content

Polls

POST /forums/{forum_id}/topics accepts an optional poll object when creating a topic — polls can’t be added afterwards, and editing the topic never touches an existing poll’s options or votes.

POST/forums/{forum_id}/topicstopics.write + f_poll

Create a topic with a poll.

request
curl -X POST https://example.com/app.php/api/v1/forums/2/topics \
  -H "X-API-Key: <token>" -H "Content-Type: application/json" \
  -d '{"title": "Pick a colour", "content": "Vote below.",
       "poll": {"question": "Favourite colour?", "options": ["Red", "Green", "Blue"],
                 "max_options": 1, "length_days": 7}}'
poll, as returned by a later GET
"poll": {"question": "Favourite colour?", "options": [
  {"option_id": 1, "text": "Red", "votes": 0},
  {"option_id": 2, "text": "Green", "votes": 0},
  {"option_id": 3, "text": "Blue", "votes": 0}
], "max_options": 1, "vote_change": false, "expires": 1756104800, "your_votes": []}
POST/topics/{topic_id}/poll/votetopics.vote + f_vote

Vote — or change your vote. option_ids is the complete set you want selected, same semantics as phpBB’s own poll form.

request
curl -X POST https://example.com/app.php/api/v1/topics/303/poll/vote \
  -H "X-API-Key: <token>" -H "Content-Type: application/json" -d '{"option_ids": [2]}'
200 response
{"success": true, "your_votes": [2]}
Content

Attachments

Up to 10 files per post, sent as multipart/form-data. Each file is its own fieldattachment, attachment2, … attachment10 — not an attachment[] array (PHP would merge that into one nested entry phpBB’s uploader can’t split back apart). Requires posts.attach on top of topics.write/posts.write, plus f_attach in that forum.

POST/forums/{forum_id}/topicsposts.attach

Create a topic with multiple attachments.

request
curl -X POST https://example.com/app.php/api/v1/forums/2/topics \
  -H "X-API-Key: <token>" \
  -F "title=Screenshots attached" -F "content=See attached." \
  -F "attachment=@screenshot1.png" -F "attachment2=@screenshot2.png" -F "attachment3=@notes.pdf"
200 response
{"success": true, "topic_id": 305, "post_id": 7600, "attachment_ids": [12, 13, 14]}
GET/attachments/{attachment_id}posts.read

Download a file — not JSON, streams the file back with the correct headers. Same visibility checks as reading the parent post.

request
curl https://example.com/app.php/api/v1/attachments/12 -H "X-API-Key: <token>" -o downloaded_file
Content

Moderation

Lock/unlock, soft-delete/restore, approve/disapprove, move, merge, split — each still checks the bound member’s own moderator permissions (m_lock, m_softdelete, m_approve, m_move, m_merge, m_split…), same as phpBB’s own moderator UI.

POST/topics/{topic_id}/locktopics.moderate

Lock or unlock a topic (/unlock for the reverse).

request
curl -X POST https://example.com/app.php/api/v1/topics/303/lock -H "X-API-Key: <token>"
curl -X POST https://example.com/app.php/api/v1/topics/303/unlock -H "X-API-Key: <token>"
POST/topics/{topic_id}/soft-deletetopics.delete + m_softdelete

Hide a topic reversibly (optionally with a reason) — undo with /restore. Same pattern exists for posts.

request
curl -X POST https://example.com/app.php/api/v1/topics/303/soft-delete \
  -H "X-API-Key: <token>" -H "Content-Type: application/json" -d '{"reason": "spam"}'
curl -X POST https://example.com/app.php/api/v1/topics/303/restore -H "X-API-Key: <token>"
POST/posts/{post_id}/approveposts.approve + m_approve

Approve a post awaiting moderation, or /disapprove to reject it. Disapproving deletes it outright — same as phpBB’s own moderator queue, a rejected never-published post has nothing to restore from.

request
curl -X POST https://example.com/app.php/api/v1/posts/7573/approve -H "X-API-Key: <token>"
curl -X POST https://example.com/app.php/api/v1/posts/7573/disapprove -H "X-API-Key: <token>"
POST/topics/{topic_id}/movetopics.manage + f_post

Move a topic to another forum.

request
curl -X POST https://example.com/app.php/api/v1/topics/303/move \
  -H "X-API-Key: <token>" -H "Content-Type: application/json" -d '{"to_forum_id": 11}'
POST/topics/{topic_id}/mergetopics.manage + m_merge

Merge into another topic. Omit post_ids to merge the entire source topic, or list specific posts.

request
curl -X POST https://example.com/app.php/api/v1/topics/303/merge \
  -H "X-API-Key: <token>" -H "Content-Type: application/json" \
  -d '{"to_topic_id": 290, "post_ids": [7572, 7573]}'
POST/topics/{topic_id}/splittopics.manage + m_split

Split posts into a genuinely new topic.

request
curl -X POST https://example.com/app.php/api/v1/topics/303/split \
  -H "X-API-Key: <token>" -H "Content-Type: application/json" \
  -d '{"post_ids": [7574, 7575], "subject": "Split-off discussion", "to_forum_id": 2}'
200 response
{"success": true, "topic_id": 304}
People

Members

POST/membersmembers.write + a_user

Register a new member — always active immediately, regardless of the board’s activation policy.

request
curl -X POST https://example.com/app.php/api/v1/members \
  -H "X-API-Key: <token>" -H "Content-Type: application/json" \
  -d '{"username": "newuser", "password": "correct-horse-battery-staple", "email": "newuser@example.com"}'
201 response
{"success": true, "user_id": 15, "username": "newuser"}
GET/members?q=...members.read

List or search members.

request
curl "https://example.com/app.php/api/v1/members?q=dmzx&limit=10" -H "X-API-Key: <token>"
200 response
{"success": true, "members": [{"user_id": 2, "username": "dmzx", "posts": 128, "colour": "AA0000"}]}
POST/members/{user_id}/groupsmembers.write + a_group

Add to a group (group_id in the body) — remove with DELETE /members/{user_id}/groups/{group_id}.

request
curl -X POST https://example.com/app.php/api/v1/members/15/groups \
  -H "X-API-Key: <token>" -H "Content-Type: application/json" -d '{"group_id": 5}'
POST/members/{user_id}/banmembers.ban + a_ban

Ban (length_minutes: 0 = permanent) or /unban. phpBB refuses to ban yourself or the founder, regardless of permissions.

request
curl -X POST https://example.com/app.php/api/v1/members/15/ban \
  -H "X-API-Key: <token>" -H "Content-Type: application/json" \
  -d '{"length_minutes": 0, "reason": "spam (moderator-only)", "public_reason": "Banned for spam"}'
DELETE/members/{user_id}members.write + a_userdel

Delete an account. mode: "remove" also deletes everything they posted; anything else retains their posts under the deleted username. Same self/founder protection as banning.

request
curl -X POST https://example.com/app.php/api/v1/members/15 \
  -H "X-API-Key: <token>" -H "X-HTTP-Method-Override: DELETE" \
  -H "Content-Type: application/json" -d '{"mode": "retain"}'
People

Messages

POST/messagesmessages.write + u_sendpm

Send a private message — to accepts a username or numeric user id.

request
curl -X POST https://example.com/app.php/api/v1/messages \
  -H "X-API-Key: <token>" -H "Content-Type: application/json" \
  -d '{"to": "someuser", "subject": "Welcome", "content": "Hi there!"}'
200 response
{"success": true, "msg_id": 10, "to": {"user_id": 3, "username": "someuser"}}
GET/messages/{msg_id}messages.read

Read a single message — marks it read, same as opening it in the UCP. GET /messages lists your own inbox.

request
curl https://example.com/app.php/api/v1/messages/9 -H "X-API-Key: <token>"
People

Reports

GET/reports?status=open|closed|allreports.read + m_report

List post reports, scoped to forums the bound member moderates. Resolve one with POST /reports/{report_id}/resolve. Creating a report isn’t part of this API — reporting is member-facing, this surface is moderator-facing.

request
curl "https://example.com/app.php/api/v1/reports?status=open" -H "X-API-Key: <token>"
People

Notifications

GET/notificationsnotifications.read

Your own board notifications, same feed as the bell icon. Mark one read with POST /notifications/{id}/read.

request
curl https://example.com/app.php/api/v1/notifications -H "X-API-Key: <token>"
200 response
{"success": true, "unread_count": 1, "notifications": [{"notification_id": 21, "title": "someuser replied to Pick a colour", "unread": true}]}
Real-time

Event stream (SSE)

GET /stream is a Server-Sent Events connection that emits topic.created/post.created as they happen, instead of polling. Only approved, non-deleted content the bound member can read is included.

curl
curl -N -H "X-API-Key: <token>" "https://example.com/index.php/api/v1/stream"
browser
const source = new EventSource('/index.php/api/v1/stream?access_token=<token>');
source.addEventListener('topic.created', e => console.log(JSON.parse(e.data)));
source.addEventListener('post.created',  e => console.log(JSON.parse(e.data)));

Pass ?since=<unix time> to resume, or just let the browser’s native EventSource send Last-Event-ID automatically on reconnect.

Query-string auth, here only. The browser’s EventSource can’t set custom headers, so ?access_token= is accepted as a fallback — only for this endpoint. Prefer a header wherever your client supports one; a token in a URL can leak via logs or browser history.
Real-time

Webhooks

ACP → phpBB API → Webhooks lets you register a URL to receive a signed POST whenever a subscribed event happens through this API. Each webhook has a Fire for forum-UI actions too toggle (off by default) to also fire for the same kind of action done through the normal forum UI, not just the API. Configuration is ACP-only.

Events

CategoryEvents

Delivery

POST to your endpoint
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Webhook-Event: topic.created
X-Webhook-Signature: sha256=5a8f...c3e1

{"event": "topic.created", "time": 1755500000, "data": {"topic_id": 303, "post_id": 7572, "forum_id": 2, "title": "Test topic via API", "url": "https://example.com/viewtopic.php?t=303"}}

Verifying a signature

PHP
$payload = file_get_contents('php://input');
$sig = 'sha256=' . hash_hmac('sha256', $payload, $secret);

if (!hash_equals($sig, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}
Node.js
const crypto = require('crypto');
const expected = 'sha256=' + crypto
  .createHmac('sha256', secret)
  .update(rawBody).digest('hex');

crypto.timingSafeEqual(
  Buffer.from(expected),
  Buffer.from(req.headers['x-webhook-signature'] || '')
);

Retries & rotation

  • Best-effort delivery — a failed attempt never affects the API response that triggered it, and is retried with backoff: 1 min, 5 min, 30 min, 2h, then 12h (5 tries, ~15h total).
  • Retries run opportunistically, on phpBB’s own cron beacon — a quiet board won’t retry until someone loads a page, or system cron polls it.
  • Secret rotation has a 24-hour grace period: deliveries carry both X-Webhook-Signature (new) and X-Webhook-Signature-Previous (old) so you can update your verification without a hard cutover.
Reference

Errors

shape
{"success": false, "error": "<code>"}
HTTPCodeMeaning
Every response from a rate-limited credential carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset — check Remaining to back off before hitting a 429. A credential with rate_limit: 0 (unlimited) sends none of these.
Operations

Security notes

  • Permissions are never bypassed — every request runs phpBB’s own submit_post(), delete functions, and ACL checks exactly as the bound member would trigger them via the web UI.
  • Deletion requires moderator-level m_delete — self-service deletion of your own post isn’t exposed in this version; the destructive-action surface stays moderator-only.
  • Tokens are hashed — only a SHA-256 hash is ever stored.
  • Token rotation — Regenerate issues a new token immediately; the old one keeps working for 24 hours so you can update whichever app uses it.
  • Rate limiting runs through phpBB’s own cache layer — no extra database writes on the hot path.
  • CORS is off by default — only needed if browser JavaScript calls the API directly.
  • Full audit log — every request, successful or not, recorded with time, IP, method, route, status and error code.