What You’ll Learn

In this tutorial, you’ll learn how to create powerful custom REST API endpoints in WordPress that can:

  • Handle GET, POST, PUT, and DELETE requests
  • Validate and sanitize user input
  • Implement proper authentication
  • Return structured JSON responses
  • Work seamlessly with modern JavaScript frameworks

By the end, you’ll have built a complete API for managing custom content — perfect for headless WordPress setups, mobile apps, or SaaS integrations.

Prerequisites

Before we start, make sure you have:

  • Basic understanding of PHP and WordPress
  • A local WordPress installation (Local, XAMPP, or similar)
  • Knowledge of how REST APIs work
  • A tool like Postman or Insomnia for testing (optional but recommended)

Why Custom REST API Endpoints?

WordPress comes with built-in REST API routes for posts, pages, and users. But what if you need to:

  • Create custom data structures
  • Build a mobile app backend
  • Integrate with third-party services
  • Create a headless WordPress setup
  • Build custom admin dashboards

That’s where custom endpoints come in. They give you full control over your API architecture.

Project Overview: Building a Task Management API

We’ll build a complete API for managing tasks with the following features:

  • Create new tasks
  • Retrieve all tasks or a single task
  • Update existing tasks
  • Delete tasks
  • Mark tasks as complete

Perfect for a to-do app, project management system, or any task-based workflow.

Step 1: Setting Up the Custom Post Type

First, let’s create a custom post type for our tasks. Add this to your theme’s functions.php:

<?php
/**
 * Register Tasks Custom Post Type
 */
function register_tasks_post_type() {
    $args = array(
        'label'               => 'Tasks',
        'public'              => true,
        'publicly_queryable'  => true,
        'show_ui'             => true,
        'show_in_menu'        => true,
        'query_var'           => true,
        'rewrite'             => array('slug' => 'task'),
        'capability_type'     => 'post',
        'has_archive'         => true,
        'hierarchical'        => false,
        'menu_position'       => 5,
        'menu_icon'           => 'dashicons-list-view',
        'supports'            => array('title', 'editor', 'author'),
        'show_in_rest'        => true, // Important for REST API access
        'rest_base'           => 'tasks',
    );
    
    register_post_type('task', $args);
}
add_action('init', 'register_tasks_post_type');

Key points:

  • show_in_rest enables REST API access
  • rest_base defines the API endpoint name
  • We support title, content, and author

Step 2: Adding Custom Meta Fields

Tasks need additional data like priority, due date, and completion status. Let’s add custom fields:

<?php
/**
 * Register Task Meta Fields
 */
function register_task_meta_fields() {
    // Priority field
    register_post_meta('task', 'task_priority', array(
        'type'         => 'string',
        'single'       => true,
        'show_in_rest' => true,
        'default'      => 'medium',
    ));
    
    // Due date field
    register_post_meta('task', 'task_due_date', array(
        'type'         => 'string',
        'single'       => true,
        'show_in_rest' => true,
    ));
    
    // Completion status
    register_post_meta('task', 'task_completed', array(
        'type'         => 'boolean',
        'single'       => true,
        'show_in_rest' => true,
        'default'      => false,
    ));
}
add_action('init', 'register_task_meta_fields');

These fields are now accessible via the REST API automatically!


Step 3: Creating Custom REST API Endpoints

Now let’s create our custom endpoints. This is where the real power comes in:

<?php
/**
 * Register Custom REST API Routes
 */
function register_custom_task_routes() {
    $namespace = 'mytasks/v1';
    
    // Get all tasks with custom filtering
    register_rest_route($namespace, '/tasks', array(
        'methods'  => 'GET',
        'callback' => 'get_all_tasks',
        'permission_callback' => '__return_true', // Public access
    ));
    
    // Get single task by ID
    register_rest_route($namespace, '/tasks/(?P<id>\d+)', array(
        'methods'  => 'GET',
        'callback' => 'get_single_task',
        'permission_callback' => '__return_true',
        'args' => array(
            'id' => array(
                'validate_callback' => function($param) {
                    return is_numeric($param);
                }
            ),
        ),
    ));
    
    // Create new task
    register_rest_route($namespace, '/tasks', array(
        'methods'  => 'POST',
        'callback' => 'create_task',
        'permission_callback' => 'check_user_permission',
    ));
    
    // Update existing task
    register_rest_route($namespace, '/tasks/(?P<id>\d+)', array(
        'methods'  => 'PUT',
        'callback' => 'update_task',
        'permission_callback' => 'check_user_permission',
        'args' => array(
            'id' => array(
                'validate_callback' => function($param) {
                    return is_numeric($param);
                }
            ),
        ),
    ));
    
    // Delete task
    register_rest_route($namespace, '/tasks/(?P<id>\d+)', array(
        'methods'  => 'DELETE',
        'callback' => 'delete_task',
        'permission_callback' => 'check_user_permission',
        'args' => array(
            'id' => array(
                'validate_callback' => function($param) {
                    return is_numeric($param);
                }
            ),
        ),
    ));
    
    // Mark task as complete
    register_rest_route($namespace, '/tasks/(?P<id>\d+)/complete', array(
        'methods'  => 'POST',
        'callback' => 'mark_task_complete',
        'permission_callback' => 'check_user_permission',
        'args' => array(
            'id' => array(
                'validate_callback' => function($param) {
                    return is_numeric($param);
                }
            ),
        ),
    ));
}
add_action('rest_api_init', 'register_custom_task_routes');

Step 4: Implementing the Callback Functions

Now let’s implement each endpoint’s logic:

Get All Tasks

<?php
/**
 * Get all tasks with filtering options
 */
function get_all_tasks($request) {
    $params = $request->get_params();
    
    $args = array(
        'post_type'      => 'task',
        'posts_per_page' => isset($params['per_page']) ? intval($params['per_page']) : 10,
        'paged'          => isset($params['page']) ? intval($params['page']) : 1,
        'post_status'    => 'publish',
    );
    
    // Filter by completion status
    if (isset($params['completed'])) {
        $args['meta_query'] = array(
            array(
                'key'     => 'task_completed',
                'value'   => $params['completed'] === 'true' ? '1' : '0',
                'compare' => '=',
            ),
        );
    }
    
    // Filter by priority
    if (isset($params['priority'])) {
        $args['meta_query'][] = array(
            'key'     => 'task_priority',
            'value'   => sanitize_text_field($params['priority']),
            'compare' => '=',
        );
    }
    
    $query = new WP_Query($args);
    $tasks = array();
    
    if ($query->have_posts()) {
        while ($query->have_posts()) {
            $query->the_post();
            $tasks[] = format_task_response(get_the_ID());
        }
        wp_reset_postdata();
    }
    
    return new WP_REST_Response(array(
        'success' => true,
        'data'    => $tasks,
        'total'   => $query->found_posts,
        'pages'   => $query->max_num_pages,
    ), 200);
}

Get Single Task

<?php
/**
 * Get a single task by ID
 */
function get_single_task($request) {
    $task_id = intval($request['id']);
    
    if (!get_post($task_id) || get_post_type($task_id) !== 'task') {
        return new WP_Error(
            'task_not_found',
            'Task not found',
            array('status' => 404)
        );
    }
    
    return new WP_REST_Response(array(
        'success' => true,
        'data'    => format_task_response($task_id),
    ), 200);
}

Create Task

<?php
/**
 * Create a new task
 */
function create_task($request) {
    $params = $request->get_params();
    
    // Validate required fields
    if (empty($params['title'])) {
        return new WP_Error(
            'missing_title',
            'Task title is required',
            array('status' => 400)
        );
    }
    
    // Create the task
    $task_data = array(
        'post_title'   => sanitize_text_field($params['title']),
        'post_content' => isset($params['description']) ? wp_kses_post($params['description']) : '',
        'post_type'    => 'task',
        'post_status'  => 'publish',
        'post_author'  => get_current_user_id(),
    );
    
    $task_id = wp_insert_post($task_data);
    
    if (is_wp_error($task_id)) {
        return new WP_Error(
            'task_creation_failed',
            'Failed to create task',
            array('status' => 500)
        );
    }
    
    // Add meta fields
    if (isset($params['priority'])) {
        update_post_meta($task_id, 'task_priority', sanitize_text_field($params['priority']));
    }
    
    if (isset($params['due_date'])) {
        update_post_meta($task_id, 'task_due_date', sanitize_text_field($params['due_date']));
    }
    
    update_post_meta($task_id, 'task_completed', false);
    
    return new WP_REST_Response(array(
        'success' => true,
        'message' => 'Task created successfully',
        'data'    => format_task_response($task_id),
    ), 201);
}

Update Task

<?php
/**
 * Update an existing task
 */
function update_task($request) {
    $task_id = intval($request['id']);
    $params = $request->get_params();
    
    // Check if task exists
    if (!get_post($task_id) || get_post_type($task_id) !== 'task') {
        return new WP_Error(
            'task_not_found',
            'Task not found',
            array('status' => 404)
        );
    }
    
    // Check ownership
    $task = get_post($task_id);
    if ($task->post_author != get_current_user_id() && !current_user_can('edit_others_posts')) {
        return new WP_Error(
            'unauthorized',
            'You do not have permission to edit this task',
            array('status' => 403)
        );
    }
    
    // Update task
    $task_data = array('ID' => $task_id);
    
    if (isset($params['title'])) {
        $task_data['post_title'] = sanitize_text_field($params['title']);
    }
    
    if (isset($params['description'])) {
        $task_data['post_content'] = wp_kses_post($params['description']);
    }
    
    wp_update_post($task_data);
    
    // Update meta fields
    if (isset($params['priority'])) {
        update_post_meta($task_id, 'task_priority', sanitize_text_field($params['priority']));
    }
    
    if (isset($params['due_date'])) {
        update_post_meta($task_id, 'task_due_date', sanitize_text_field($params['due_date']));
    }
    
    if (isset($params['completed'])) {
        update_post_meta($task_id, 'task_completed', (bool)$params['completed']);
    }
    
    return new WP_REST_Response(array(
        'success' => true,
        'message' => 'Task updated successfully',
        'data'    => format_task_response($task_id),
    ), 200);
}

Delete Task

<?php
/**
 * Delete a task
 */
function delete_task($request) {
    $task_id = intval($request['id']);
    
    // Check if task exists
    if (!get_post($task_id) || get_post_type($task_id) !== 'task') {
        return new WP_Error(
            'task_not_found',
            'Task not found',
            array('status' => 404)
        );
    }
    
    // Check ownership
    $task = get_post($task_id);
    if ($task->post_author != get_current_user_id() && !current_user_can('delete_others_posts')) {
        return new WP_Error(
            'unauthorized',
            'You do not have permission to delete this task',
            array('status' => 403)
        );
    }
    
    $result = wp_delete_post($task_id, true);
    
    if (!$result) {
        return new WP_Error(
            'deletion_failed',
            'Failed to delete task',
            array('status' => 500)
        );
    }
    
    return new WP_REST_Response(array(
        'success' => true,
        'message' => 'Task deleted successfully',
    ), 200);
}

Mark Task Complete

<?php
/**
 * Mark a task as complete
 */
function mark_task_complete($request) {
    $task_id = intval($request['id']);
    
    if (!get_post($task_id) || get_post_type($task_id) !== 'task') {
        return new WP_Error(
            'task_not_found',
            'Task not found',
            array('status' => 404)
        );
    }
    
    update_post_meta($task_id, 'task_completed', true);
    
    return new WP_REST_Response(array(
        'success' => true,
        'message' => 'Task marked as complete',
        'data'    => format_task_response($task_id),
    ), 200);
}

Step 5: Helper Functions

Add these utility functions:

<?php
/**
 * Format task data for API response
 */
function format_task_response($task_id) {
    $task = get_post($task_id);
    
    return array(
        'id'          => $task->ID,
        'title'       => $task->post_title,
        'description' => $task->post_content,
        'priority'    => get_post_meta($task->ID, 'task_priority', true),
        'due_date'    => get_post_meta($task->ID, 'task_due_date', true),
        'completed'   => (bool)get_post_meta($task->ID, 'task_completed', true),
        'author'      => array(
            'id'   => $task->post_author,
            'name' => get_the_author_meta('display_name', $task->post_author),
        ),
        'created_at'  => $task->post_date,
        'updated_at'  => $task->post_modified,
    );
}

/**
 * Check user permissions for protected endpoints
 */
function check_user_permission($request) {
    return current_user_can('edit_posts');
}

Step 6: Testing Your API

Now let’s test the endpoints! Your API is available at:

https://yoursite.com/wp-json/mytasks/v1/

Example Requests:

Get All Tasks:

GET https://yoursite.com/wp-json/mytasks/v1/tasks

Get Completed Tasks:

GET https://yoursite.com/wp-json/mytasks/v1/tasks?completed=true

Get Single Task:

GET https://yoursite.com/wp-json/mytasks/v1/tasks/123

Create Task:

POST https://yoursite.com/wp-json/mytasks/v1/tasks
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "title": "Build REST API Tutorial",
  "description": "Write comprehensive guide for custom endpoints",
  "priority": "high",
  "due_date": "2025-11-15"
}

Update Task:

PUT https://yoursite.com/wp-json/mytasks/v1/tasks/123
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "priority": "medium",
  "completed": true
}

Delete Task:

DELETE https://yoursite.com/wp-json/mytasks/v1/tasks/123
Authorization: Bearer YOUR_TOKEN

Step 7: Adding Authentication

For production, you’ll need proper authentication. Here’s how to add JWT authentication:

Install JWT Authentication Plugin

Add this to your wp-config.php:

define('JWT_AUTH_SECRET_KEY', 'your-secret-key-here');
define('JWT_AUTH_CORS_ENABLE', true);

Update Permission Callback

function check_user_permission($request) {
    // JWT authentication will handle this
    return is_user_logged_in();
}

Best Practices & Security Tips

1. Always Validate Input

$priority = sanitize_text_field($params['priority']);
$date = sanitize_text_field($params['due_date']);

2. Use Proper HTTP Status Codes

  • 200: Success
  • 201: Created
  • 400: Bad Request
  • 401: Unauthorized
  • 403: Forbidden
  • 404: Not Found
  • 500: Server Error

3. Implement Rate Limiting

function rate_limit_check($request) {
    // Implement rate limiting logic
    // Return true if allowed, false otherwise
}

4. Add CORS Headers (if needed)

add_action('rest_api_init', function() {
    remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
    add_filter('rest_pre_serve_request', function($value) {
        header('Access-Control-Allow-Origin: *');
        header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
        header('Access-Control-Allow-Credentials: true');
        return $value;
    });
});

5. Log API Requests

function log_api_request($request) {
    error_log('API Request: ' . $request->get_route());
}
add_action('rest_api_init', 'log_api_request');

Advanced Features to Add

Once you’ve mastered the basics, try adding:

  1. Pagination with meta information
  2. Sorting and ordering
  3. Advanced filtering (date ranges, search)
  4. Bulk operations (update/delete multiple tasks)
  5. Webhooks for real-time updates
  6. API versioning (v1, v2, etc.)
  7. Caching for better performance
  8. Rate limiting per user

Consuming Your API with JavaScript

Here’s how to use your API from a frontend:

// Fetch all tasks
async function getTasks() {
    const response = await fetch('https://yoursite.com/wp-json/mytasks/v1/tasks');
    const data = await response.json();
    console.log(data);
}

// Create a task
async function createTask(taskData) {
    const response = await fetch('https://yoursite.com/wp-json/mytasks/v1/tasks', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + token
        },
        body: JSON.stringify(taskData)
    });
    const data = await response.json();
    return data;
}

// Update a task
async function updateTask(taskId, updates) {
    const response = await fetch(`https://yoursite.com/wp-json/mytasks/v1/tasks/${taskId}`, {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + token
        },
        body: JSON.stringify(updates)
    });
    return await response.json();
}

// Delete a task
async function deleteTask(taskId) {
    const response = await fetch(`https://yoursite.com/wp-json/mytasks/v1/tasks/${taskId}`, {
        method: 'DELETE',
        headers: {
            'Authorization': 'Bearer ' + token
        }
    });
    return await response.json();
}

Troubleshooting Common Issues

Issue: 404 – Route Not Found

Solution: Make sure you’ve flushed permalinks. Go to Settings → Permalinks and click Save.

Issue: 401 – Unauthorized

Solution: Check your authentication headers and make sure JWT is configured correctly.

Issue: CORS Errors

Solution: Add proper CORS headers as shown in the security section.

Issue: 500 – Internal Server Error

Solution: Enable WordPress debug mode and check error logs:

define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);

Conclusion

Congratulations! You’ve built a complete custom REST API in WordPress. This foundation can be extended to create:

  • Mobile app backends
  • Headless CMS setups
  • Third-party integrations
  • Custom admin dashboards
  • SaaS platforms

The possibilities are endless.