How to Protect Custom Post Types in WordPress: Capability, Template, and REST API Methods
Custom post types (CPTs) power everything from member resources and downloadable courses to internal documentation and client portals. But a CPT created with the defaults is, by design, public. Unless you deliberately restrict it, anyone can read it on the front end, and in many cases, fetch it through the REST API without ever logging in.
This guide walks through how to protect custom post types in WordPress using layered controls: custom capabilities, front-end redirects, REST API hardening, and membership gating. Each layer closes a different gap, and you usually need more than one.
Key Takeaways
• Capabilities decide who can view, edit, or delete a CPT inside the admin area; map them with `capability_type` and `map_meta_cap`.
• `template_redirect` protects the front-end single and archive views by redirecting unauthorized visitors.
• A CPT with `show_in_rest => true` can leak “protected” content through `/wp-json` even when the front end is locked. Restrict the endpoint separately.
• Membership plugins are the right tool for selling or drip-feeding gated content to large audiences.
• Server-level security (firewall, SSL, hardened hosting) backs up every application-level control.
Why is a default custom post type not private?
When you call `register_post_type()` without restricting it, WordPress assumes the content is meant to be seen. The `public` argument is the master switch: setting `public => true` (or leaving related arguments at their permissive defaults) makes the type queryable on the front end, visible in search, and accessible to logged-out users.
Privacy is not a single setting. A CPT can be hidden from the front end yet still exposed through the REST API. It can be locked in the admin for low-level roles yet readable by anyone with a direct URL. Treat front-end access, admin capabilities, and API access as three separate doors, each needing its own lock.
How do you restrict a custom post type by capability?
Capabilities control what a user can do inside WordPress. By default, custom post types borrow the capabilities of the standard `post` type, which means any author or editor can manage them. To restrict a CPT to specific roles, define custom capabilities at registration.
Use `capability_type` to name a custom capability set, and set `map_meta_cap => true` so WordPress correctly translates meta capabilities (like editing a *specific* post) into your custom primitives.
“`php add_action( ‘init’, ‘darazhost_register_secure_doc’ );
function darazhost_register_secure_doc() { register_post_type( ‘secure_doc’, array( ‘label’ => ‘Secure Documents’, ‘public’ => false, // not public on the front end ‘show_ui’ => true, // still editable in wp-admin ‘show_in_menu’ => true, ‘show_in_rest’ => false, // keep it out of /wp-json (see below) ‘capability_type’ => array( ‘secure_doc’, ‘secure_docs’ ), ‘map_meta_cap’ => true, ‘capabilities’ => array( ‘edit_post’ => ‘edit_secure_doc’, ‘read_post’ => ‘read_secure_doc’, ‘delete_post’ => ‘delete_secure_doc’, ‘edit_posts’ => ‘edit_secure_docs’, ‘edit_others_posts’ => ‘edit_others_secure_docs’, ‘publish_posts’ => ‘publish_secure_docs’, ‘read_private_posts’ => ‘read_private_secure_docs’, ), ‘supports’ => array( ‘title’, ‘editor’, ‘author’ ), ) ); } “`
Because these capabilities do not exist yet, no role can manage the CPT until you grant them. Add the capabilities to the roles that should have access, typically on plugin or theme activation so they are not added on every page load:
“`php register_activation_hook( __FILE__, ‘darazhost_add_secure_doc_caps’ );
function darazhost_add_secure_doc_caps() { $role = get_role( ‘administrator’ ); $caps = array( ‘edit_secure_doc’, ‘read_secure_doc’, ‘delete_secure_doc’, ‘edit_secure_docs’, ‘edit_others_secure_docs’, ‘publish_secure_docs’, ‘read_private_secure_docs’, ); foreach ( $caps as $cap ) { $role->add_cap( $cap ); } } “`
Grant a narrower subset to a custom role (for example, a “Member” role that can `read_secure_doc` but not edit). This gives you role-based access control inside the admin without touching the front end yet.
How do you block front-end access with template_redirect?
Capabilities govern the admin. To control who can *view* a CPT on the public site, hook into `template_redirect`, which fires before the template loads. There you can check the request, evaluate the current user, and redirect anyone unauthorized.
“`php add_action( ‘template_redirect’, ‘darazhost_protect_secure_doc_frontend’ );
function darazhost_protect_secure_doc_frontend() { // Only act on our CPT (single views and archives). if ( ! is_singular( ‘secure_doc’ ) && ! is_post_type_archive( ‘secure_doc’ ) ) { return; }
// Require login. if ( ! is_user_logged_in() ) { wp_safe_redirect( wp_login_url( get_permalink() ) ); exit; }
// Require the read capability for logged-in users. if ( ! current_user_can( ‘read_secure_doc’ ) ) { wp_safe_redirect( home_url( ‘/access-denied/’ ) ); exit; } } “`
Key points: always call `exit` after `wp_safe_redirect()` so execution stops, and use `wp_safe_redirect()` (not `wp_redirect()`) to limit redirects to allowed hosts. If you want unauthorized users to see a “members only” message instead of a redirect, swap the template using the `template_include` filter rather than redirecting.
The trap most tutorials miss: `template_redirect` only protects the *rendered front end*. If your CPT was registered with `show_in_rest => true` (the default for Gutenberg-edited types), the same content is still reachable at `https://example.com/wp-json/wp/v2/secure_doc` — no login required. A visitor who is redirected away from the page can simply request the JSON and read every “protected” post, including titles, content, and excerpts. Locking the template is not locking the data. You must restrict the REST endpoint as a separate, deliberate step.
How do you stop a custom post type from leaking through the REST API?
If your CPT does not need block-editor or headless access, the cleanest fix is to not expose it at all: set `show_in_rest => false` at registration (as in the first example). The type disappears from `/wp-json/wp/v2/` entirely.
When you *do* need REST access (for the block editor, or a decoupled front end) but still want to gate who reads it, filter the REST permission. The `rest_{$post_type}_query` and item-level permission filters let you enforce authentication:
“`php add_filter( ‘rest_secure_doc_query’, ‘darazhost_rest_require_auth’, 10, 2 );
function darazhost_rest_require_auth( $args, $request ) { if ( ! current_user_can( ‘read_secure_doc’ ) ) { // Force an empty result set for unauthorized callers. $args[‘post__in’] = array( 0 ); } return $args; } “`
For stricter control, register the CPT with a custom controller or use the `rest_pre_dispatch` filter to reject requests to the route outright when the user lacks capability. Also consider that `show_in_rest` interacts with `public`: even a non-public CPT will appear in REST if `show_in_rest` is true, so audit both flags together.
Which approach should you use? A comparison
Each method protects a different surface. Most secure setups combine several.
| Approach | What it protects | Best for | Limitation |
|---|---|---|---|
| Custom capabilities (`capability_type` + `map_meta_cap`) | Admin: who can edit, delete, publish, read private | Role-based editorial control | Does not block front-end or REST reads on its own |
| `template_redirect` redirect | Front-end single/archive views | Gating rendered pages by login or role | Does not cover REST API or feeds |
| `show_in_rest => false` | REST API exposure | CPTs with no headless/Gutenberg need | Disables block editor for the type |
| REST permission filters | REST endpoint when exposure is required | Headless or block-editor CPTs | Requires careful per-route logic |
| Membership plugin | Content access tied to plans/payments | Selling or drip-feeding gated content | Adds a dependency; configure carefully |
| Server-level security | The whole site (brute force, injection, transport) | Every site | Complements, never replaces, app-level rules |
When should you use a membership plugin instead?
Hand-coded capability and redirect logic is ideal for fixed, role-based access: staff, clients, or a known set of roles. But if you need to sell access, run subscriptions, drip-feed lessons over time, or manage thousands of members with self-service signup, a dedicated membership plugin is the better path. These tools handle payment gateways, expiring access, and content scheduling that would be brittle to build by hand.
Choose a well-maintained plugin, keep it updated, and confirm it gates both the front end and the REST API. Some membership plugins protect the visible page but leave the underlying CPT readable via `/wp-json` — test the JSON endpoint directly before trusting it.
How do you secure the admin and remaining gaps?
Even with capabilities set, tighten the wider surface:
- Limit admin access to the roles that truly need it; remove `show_ui`/`show_in_menu` for types managed only programmatically.
- Disable XML-RPC if unused, and protect the `wp-login.php` page against brute force.
- Restrict feeds. A CPT exposed in RSS can leak the same way the REST API does; filter it out if needed.
- Audit user roles regularly so old accounts do not retain capabilities.
Application-level controls only hold if the server beneath them is sound. A misconfigured host, missing firewall, or unpatched stack can undermine even flawless capability mapping.
DarazHost secure WordPress hosting is built to complement the application-level protections in this guide. Our infrastructure includes server-level security with a managed firewall, free SSL for encrypted transport, and proactive hardening that reduces the attack surface around your CPT logic. For sites running membership areas or gated content, we provide reliable, fast hosting tuned for dynamic, logged-in traffic, plus 24/7 support to help you keep both the server and your access rules airtight. Strong CPT permissions and a secured server work best together.
Frequently asked questions
Does setting `public => false` make a custom post type private? Partly. It removes the type from front-end queries and search, but it does not automatically secure the REST API (check `show_in_rest`) and does not restrict admin capabilities. Use it as one layer, not the whole solution.
Why is my protected CPT still showing in the REST API? Because `show_in_rest` is set to `true`, often by default for Gutenberg-compatible types. Set it to `false`, or add a REST permission filter to require capability before returning data.
What is the difference between `capability_type` and `map_meta_cap`? `capability_type` names the base set of capabilities for the CPT. `map_meta_cap => true` tells WordPress to translate meta capabilities (like editing one specific post) into your primitive capabilities, so per-object checks work correctly.
Can `template_redirect` alone fully protect my content? No. It only guards rendered front-end views. The REST API, feeds, and direct admin access need their own controls. Layer `template_redirect` with REST hardening and capability checks.
Do I need a plugin to restrict a custom post type? Not for fixed, role-based access — capabilities and `template_redirect` cover that in code. You need a membership plugin when you require payments, subscriptions, expiring access, or self-service member management.