A WordPress custom REST API endpoint can work in a local demo and still be unsafe or expensive to maintain in production. The route may accept the wrong input, expose data to the wrong user, return an unstable shape, or become impossible to change without breaking a mobile app or business integration.
This guide turns endpoint development into a reviewable contract. It covers the decisions that should be written down before implementation, a minimal registration example, and an acceptance-test matrix for release. It assumes you are extending a site you own or are authorised to maintain.
Start with an endpoint contract, not a callback
In WordPress terminology, a route is the URI and an endpoint is a route paired with an HTTP method. Decide what the resource means before choosing PHP details. A useful contract answers seven questions:
| Decision | Example | Review question |
|---|---|---|
| Namespace and version | ict/v1 |
Can a future incompatible response live under v2? |
| Route and method | GET /reports/(?P<id>d+) |
Does the method match a read, creation, replacement or deletion? |
| Caller | Authenticated staff member | Which capability represents the business permission? |
| Inputs | Positive integer id |
What is valid, required and safe to normalize? |
| Success response | 200 with a stable resource shape |
Which fields are contractual and which are internal? |
| Error response | 404 when the resource is absent |
Can a client act on the status and error code? |
| Operational limits | Maximum page size or request rate | What prevents an expensive query or retry storm? |
Version your own namespace rather than placing a route under another plugin's namespace. Keep a resource-oriented route stable and use HTTP methods to express the operation. If the requirement is only to expose an existing post type or registered meta field, check whether core REST support can satisfy it before creating another route.
Register the route with explicit arguments
Register routes on rest_api_init. WordPress expects a permission_callback; public access should be an explicit decision, not an omitted check. The following example is deliberately small and read-only:
add_action( 'rest_api_init', function () {
register_rest_route( 'ict/v1', '/reports/(?P<id>d+)', [
'methods' => WP_REST_Server::READABLE,
'callback' => 'ict_get_report',
'permission_callback' => function ( WP_REST_Request $request ) {
return current_user_can( 'view_ict_reports' );
},
'args' => [
'id' => [
'required' => true,
'validate_callback' => function ( $value ) {
return is_numeric( $value ) && (int) $value > 0;
},
'sanitize_callback' => 'absint',
],
],
] );
} );
function ict_get_report( WP_REST_Request $request ) {
$report = ict_find_report( (int) $request['id'] );
if ( ! $report ) {
return new WP_Error(
'ict_report_not_found',
'Report not found.',
[ 'status' => 404 ]
);
}
return rest_ensure_response( [
'id' => (int) $report->id,
'status' => (string) $report->status,
'updated' => mysql_to_rfc3339( $report->updated_at ),
] );
}
This is a pattern, not a drop-in plugin. The capability, storage lookup and response fields must match the real business rule. Put production code in a version-controlled plugin or must-use plugin, not in a theme file or an ad-hoc snippet that disappears when presentation code changes.
Separate authentication from authorization
Authentication establishes who the caller is. Authorization decides whether that identity may perform this operation. A valid application password, cookie nonce or OAuth token does not automatically grant access to every custom route.
Make the permission callback test a capability tied to the action. Avoid authorizing by role name because roles can be customized and copied. For object-specific access, combine the capability check with ownership, tenant or record rules. Return the same denial behaviour consistently so a rejected caller cannot infer sensitive resource details.
Use __return_true only for data intentionally available to anonymous visitors. Public does not mean unlimited: consider pagination, caching and abuse controls for expensive endpoints. The official REST API authentication guide explains the difference between cookie authentication for in-site requests and application passwords for external clients.
Validate first, then sanitize
Validation asks whether a value is allowed. Sanitization transforms an allowed value into the representation your code will use. WordPress runs registered validation before sanitization, so do not rely on sanitization to decide whether an operation is legitimate.
- Declare required arguments, types, formats, ranges, enum values and reasonable length limits.
- Reject unexpected structures instead of letting PHP coerce them silently.
- Use parameterized database APIs for queries; sanitization is not a substitute for safe query construction.
- Do not accept an account ID, price, capability or status from the client when the server can derive it.
- For bulk operations, cap the item count and total work per request.
The official routes and endpoints documentation details registered arguments, validation and sanitization. If your resource is complex, add a JSON schema. Schema improves discovery, documents field types and gives the controller a consistent definition of its data.
Design a stable response contract
Return only fields a client needs. Leaking internal database columns creates both security risk and a compatibility promise you may not intend to keep. Normalize identifiers and dates, and use a consistent representation for an absent optional field.
Use standard HTTP meanings: 200 for a successful read, 201 for a created resource, 204 when success intentionally has no body, 400 for invalid input, 401 when authentication is required, 403 when an identified caller is not allowed, 404 when the resource is unavailable, and 409 for an applicable state conflict. The exact choice depends on the contract; consistency matters more than creative status codes.
Use WP_Error with a stable machine-readable code and a safe human message. Do not send stack traces, SQL errors, file paths or secrets. For lists, define pagination, ordering and filters before clients depend on accidental defaults. For writes that may be retried after a timeout, decide whether idempotency or a client request key is needed to avoid duplicate work.
Budget database work and payload size
An authenticated endpoint can still be a denial-of-service path if each request scans a large table or performs repeated remote calls. Set maximum page sizes, select only required columns, avoid query-per-item loops and measure the slow path with realistic data.
Cache only when the invalidation rule and authorization boundary are clear. A shared cache key that ignores user, tenant or capability can disclose private data. If the response varies by identity, include the relevant scope in the key or keep the result private. Never cache a permission decision longer than its underlying policy permits.
Run an acceptance-test matrix before release
| Test | Expected result | Evidence to retain |
|---|---|---|
| Anonymous request | Public response or deliberate denial | Status, safe response body |
| Authenticated user without capability | 403 or agreed denial |
User role/capability fixture |
| Authorized user | Documented success schema | Status, headers, representative body |
| Missing, malformed and boundary input | Predictable 4xx; no PHP warning |
Cases and stable error codes |
| Absent resource | Contracted not-found response | Nonexistent fixture ID |
| Large allowed request | Within response-time and memory budget | Dataset size and timing environment |
| Duplicate write or retry | No unintended duplicate side effect | Repeated request result |
| Schema or snapshot test | No accidental field/type change | Versioned expected shape |
Test the permission callback directly and through the REST server. Include negative tests; a suite that proves only the happy path does not prove access control. Run acceptance tests in staging with representative permissions and data volume, then repeat a safe smoke test after deployment. ICT's staging-versus-production guide provides a broader release checklist.
Log enough to diagnose, not enough to leak
Record a request or correlation ID, route, method, outcome, elapsed time and an internal error category. Do not log application passwords, authorization headers, session cookies or full sensitive payloads. Where personal data is involved, define who can see logs and how long they are retained.
Monitor error rate, latency and resource pressure after release. A client-visible 500 should map to an internal event without exposing its details. Agree on rollback criteria before deployment: for example, elevated error rate, unexpected authorization failures or database load beyond the tested budget.
Use a go-live gate
- The route, method, version, caller and resource are documented.
- The permission callback enforces a capability and any object-level rule.
- Every client-controlled argument has a declared boundary.
- The response and error shapes are stable and contain no internal-only fields.
- Pagination, retry behaviour and performance limits are explicit.
- Negative, boundary and authorization tests pass in staging.
- Logs exclude secrets and include a traceable request identifier.
- The deployment has monitoring, rollback ownership and a compatibility plan.
If the endpoint will support a paid integration or business workflow, ask for the contract and acceptance matrix during estimation. Innovative Code Tech provides custom WordPress plugin development for scoped endpoint and extension work. For systems that must exchange data across multiple applications, review the broader API integration and software services before committing to a one-off route.
Frequently asked questions
What is the difference between a WordPress REST route and endpoint?
A route is the URI pattern. An endpoint is that route paired with an HTTP method and its callbacks. One route can therefore have separate read and write endpoints.
Does every custom endpoint need a permission callback?
Yes. Use a capability or object-level authorization check for protected operations. If the data is intentionally public, make that decision explicit and still control expensive use.
Should I use a nonce or an application password?
Cookie authentication with a REST nonce fits requests made inside the logged-in WordPress interface. Application passwords are designed for authenticated external clients over HTTPS. Use the method supported by the actual client and threat model.
Do I need JSON Schema for a small endpoint?
Not always, but schema becomes valuable when fields are reused, clients need discoverability or compatibility matters. At minimum, declare and validate request arguments and document the response shape.
How should a custom endpoint be versioned?
Put a version in your namespace, such as ict/v1. Keep backward-compatible changes within that contract and introduce a new version when clients cannot safely consume the change.
A working callback is only the beginning
A production-ready WordPress custom REST API endpoint is a small public contract inside your system. Define the caller, input, output, failure modes and operating limits first. Then make permission, validation, schema and acceptance tests prove that contract before a client depends on it.