{"id":9429,"date":"2026-08-10T13:37:21","date_gmt":"2026-08-10T11:37:21","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/toolbox\/wordpress-rest-route-permission-generator\/"},"modified":"2026-08-10T13:45:12","modified_gmt":"2026-08-10T11:45:12","slug":"wordpress-rest-route-permission-generator","status":"publish","type":"page","link":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/wordpress-rest-route-permission-generator\/","title":{"rendered":"WordPress REST Route &#038; Permission Callback Generator"},"content":{"rendered":"<div class=\"gtm-analyser-container\" style=\"background: var(--bg-panel, #1e1e24); padding: 25px; border-radius: 8px; border: 1px solid var(--border, #2a2a35);\">\n<p style=\"color: var(--text-secondary, #9aa0aa); margin-bottom: 20px;\">Describe the route once and get the whole scaffold back: the register_rest_route call, an argument schema with a sanitize and a validate callback for every parameter, a permission callback that matches the level you picked, and callback functions that return WP_REST_Response or WP_Error. Nothing leaves this browser tab.<\/p>\n<div style=\"display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 20px;\">\n<div style=\"flex: 2; min-width: 200px;\">\n            <label for=\"rr-namensraum\" style=\"color: var(--text-secondary, #9aa0aa); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Namespace<\/label><br \/>\n            <input type=\"text\" id=\"rr-namensraum\" value=\"my-plugin\" class=\"form-control\" style=\"width: 100%; padding: 12px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; font-family: monospace;\">\n        <\/div>\n<div style=\"flex: 1; min-width: 120px;\">\n            <label for=\"rr-version\" style=\"color: var(--text-secondary, #9aa0aa); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Version<\/label><br \/>\n            <input type=\"text\" id=\"rr-version\" value=\"v1\" class=\"form-control\" style=\"width: 100%; padding: 12px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; font-family: monospace;\">\n        <\/div>\n<\/p><\/div>\n<div style=\"margin-bottom: 20px;\">\n        <label for=\"rr-route\" style=\"color: var(--text-secondary, #9aa0aa); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Route, regular expression placeholders allowed<\/label><br \/>\n        <input type=\"text\" id=\"rr-route\" value=\"\/items\/(?P&lt;id&gt;\\d+)\" class=\"form-control\" style=\"width: 100%; padding: 12px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; font-family: monospace;\">\n    <\/div>\n<div style=\"margin-bottom: 20px;\">\n        <span style=\"color: var(--text-secondary, #9aa0aa); display: block; font-size: 0.85rem; margin-bottom: 8px;\">HTTP methods<\/span><\/p>\n<div style=\"display: flex; gap: 18px; flex-wrap: wrap;\">\n            <label for=\"rr-m-get\" style=\"color: var(--text-primary, #e8e8ee); font-family: monospace; display: flex; align-items: center; gap: 6px;\"><input type=\"checkbox\" id=\"rr-m-get\" checked> GET<\/label><br \/>\n            <label for=\"rr-m-post\" style=\"color: var(--text-primary, #e8e8ee); font-family: monospace; display: flex; align-items: center; gap: 6px;\"><input type=\"checkbox\" id=\"rr-m-post\"> POST<\/label><br \/>\n            <label for=\"rr-m-put\" style=\"color: var(--text-primary, #e8e8ee); font-family: monospace; display: flex; align-items: center; gap: 6px;\"><input type=\"checkbox\" id=\"rr-m-put\"> PUT<\/label><br \/>\n            <label for=\"rr-m-patch\" style=\"color: var(--text-primary, #e8e8ee); font-family: monospace; display: flex; align-items: center; gap: 6px;\"><input type=\"checkbox\" id=\"rr-m-patch\"> PATCH<\/label><br \/>\n            <label for=\"rr-m-delete\" style=\"color: var(--text-primary, #e8e8ee); font-family: monospace; display: flex; align-items: center; gap: 6px;\"><input type=\"checkbox\" id=\"rr-m-delete\"> DELETE<\/label>\n        <\/div>\n<\/p><\/div>\n<div style=\"margin-bottom: 20px;\">\n        <span style=\"color: var(--text-secondary, #9aa0aa); display: block; font-size: 0.85rem; margin-bottom: 8px;\">Arguments<\/span><\/p>\n<div id=\"rr-argumente\"><\/div>\n<p>        <button id=\"rr-arg-plus\" class=\"button\" style=\"background: transparent; color: var(--text-primary, #e8e8ee); border: 1px solid var(--border, #2a2a35); padding: 10px 18px; border-radius: 6px; cursor: pointer;\">Add argument<\/button>\n    <\/div>\n<div style=\"display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 20px;\">\n<div style=\"flex: 1; min-width: 240px;\">\n            <label for=\"rr-stufe\" style=\"color: var(--text-secondary, #9aa0aa); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Permission level<\/label><br \/>\n            <select id=\"rr-stufe\" class=\"form-control\" style=\"width: 100%; padding: 12px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px;\"><option value=\"public\">Public read access<\/option><option value=\"loggedin\">Logged-in users<\/option><option value=\"capability\" selected>Own capability<\/option><option value=\"apppassword\">Application password only<\/option><\/select>\n        <\/div>\n<div style=\"flex: 1; min-width: 240px;\">\n            <label for=\"rr-faehigkeit\" style=\"color: var(--text-secondary, #9aa0aa); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Capability<\/label><br \/>\n            <input type=\"text\" id=\"rr-faehigkeit\" value=\"manage_options\" class=\"form-control\" style=\"width: 100%; padding: 12px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; font-family: monospace;\">\n        <\/div>\n<\/p><\/div>\n<p>    <button id=\"rr-btn\" class=\"button\" style=\"background: var(--accent, #35d0a5); color: #0b1114; border: none; padding: 12px 24px; border-radius: 6px; font-weight: 700; cursor: pointer;\">Generate code<\/button><\/p>\n<div id=\"rr-ergebnis\" style=\"margin-top: 25px;\"><\/div>\n<\/div>\n<p><script>\n(function () {\n    'use strict';<\/p>\n<p>    \/\/ --- reine Rechenlogik, ohne jeden DOM-Zugriff ---<\/p>\n<p>    const TYPEN = {\n        string: {\n            sanitize: 'sanitize_text_field',\n            pruefung: 'is_string( $value )',\n            hinweis: 'must be a string',\n            beispiel: 'example',\n            json: '\"example\"'\n        },\n        integer: {\n            sanitize: 'absint',\n            pruefung: 'rest_is_integer( $value )',\n            hinweis: 'must be an integer',\n            beispiel: '1',\n            json: '1'\n        },\n        number: {\n            sanitize: 'floatval',\n            pruefung: 'is_numeric( $value )',\n            hinweis: 'must be a number',\n            beispiel: '1.5',\n            json: '1.5'\n        },\n        boolean: {\n            sanitize: 'rest_sanitize_boolean',\n            pruefung: 'is_bool( rest_sanitize_boolean( $value ) )',\n            hinweis: 'must be true or false',\n            beispiel: 'true',\n            json: 'true'\n        },\n        array: {\n            sanitize: 'wp_parse_list',\n            pruefung: 'is_array( wp_parse_list( $value ) )',\n            hinweis: 'must be an array or a comma separated list',\n            beispiel: 'a,b',\n            json: '[\"a\",\"b\"]'\n        }\n    };<\/p>\n<p>    const METHODEN = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];\n    const SCHREIBEND = ['POST', 'PUT', 'PATCH', 'DELETE'];\n    const MIT_KOERPER = ['POST', 'PUT', 'PATCH'];\n    const RESERVIERT = ['wp', 'oembed', 'wp-site-health', 'wp-block-editor', 'batch'];\n    const ERFOLG = { GET: 200, POST: 201, PUT: 200, PATCH: 200, DELETE: 200 };<\/p>\n<p>    function phpText(wert) {\n        return String(wert).replace(\/\\\\\/g, '\\\\\\\\').replace(\/'\/g, \"\\\\'\");\n    }<\/p>\n<p>    function phpBezeichner(wert) {\n        let s = String(wert).toLowerCase().replace(\/[^a-z0-9]+\/g, '_').replace(\/^_+\/, '').replace(\/_+$\/, '');\n        if (s === '') { s = 'lw'; }\n        if (\/^[0-9]\/.test(s)) { s = 'lw_' + s; }\n        return s;\n    }<\/p>\n<p>    function routenName(route) {\n        const teile = String(route).split('\/').filter(function (t) {\n            return t !== '' && t.indexOf('(') === -1 && t.indexOf('[') === -1;\n        });\n        if (teile.length === 0) { return 'index'; }\n        return phpBezeichner(teile.join('_'));\n    }<\/p>\n<p>    function platzhalter(route) {\n        const treffer = String(route).match(\/\\(\\?P<[A-Za-z_][A-Za-z0-9_]*>\/g) || [];\n        return treffer.map(function (t) { return t.slice(4, t.length - 1); });\n    }<\/p>\n<p>    function fuelle(text, breite) {\n        let s = String(text);\n        while (s.length < breite) { s += ' '; }\n        return s;\n    }\n\n    function baueArgsFunktion(name, argumente) {\n        const z = [];\n        z.push('\/**');\n        z.push(' * Argument schema for the route.');\n        z.push(' *');\n        z.push(' * @return array');\n        z.push(' *\/');\n        z.push('function ' + name + '() {');\n        z.push('    return array(');\n        argumente.forEach(function (arg) {\n            const t = TYPEN[arg.typ];\n            z.push(\"        '\" + phpText(arg.name) + \"' => array(\");\n            z.push('            ' + fuelle(\"'type'\", 19) + \" => '\" + arg.typ + \"',\");\n            z.push('            ' + fuelle(\"'required'\", 19) + ' => ' + (arg.pflicht ? 'true' : 'false') + ',');\n            z.push('            ' + fuelle(\"'sanitize_callback'\", 19) + \" => '\" + t.sanitize + \"',\");\n            z.push('            ' + fuelle(\"'validate_callback'\", 19) + ' => function ( $value, $request, $param ) {');\n            z.push('                if ( ' + t.pruefung + ' ) {');\n            z.push('                    return true;');\n            z.push('                }');\n            z.push('');\n            z.push('                return new WP_Error(');\n            z.push(\"                    'rest_invalid_param',\");\n            z.push(\"                    sprintf( 'The parameter %s \" + t.hinweis + \".', $param ),\");\n            z.push(\"                    array( 'status' => 400 )\");\n            z.push('                );');\n            z.push('            },');\n            z.push('        ),');\n        });\n        z.push('    );');\n        z.push('}');\n        return z.join('\\n');\n    }<\/p>\n<p>    function baueRechteFunktion(name, bedingung, fehlercode, meldung) {\n        const z = [];\n        z.push('\/**');\n        z.push(' * Permission callback.');\n        z.push(' *');\n        z.push(' * @param WP_REST_Request $request Full details about the request.');\n        z.push(' * @return true|WP_Error');\n        z.push(' *\/');\n        z.push('function ' + name + '( WP_REST_Request $request ) {');\n        z.push('    if ( ' + bedingung + ' ) {');\n        z.push('        return true;');\n        z.push('    }');\n        z.push('');\n        z.push('    return new WP_Error(');\n        z.push(\"        '\" + fehlercode + \"',\");\n        z.push(\"        '\" + phpText(meldung) + \"',\");\n        z.push(\"        array( 'status' => rest_authorization_required_code() )\");\n        z.push('    );');\n        z.push('}');\n        return z.join('\\n');\n    }<\/p>\n<p>    function baueCallback(basis, methode, nsVoll, route, argumente) {\n        const z = [];\n        z.push('\/**');\n        z.push(' * Handles ' + methode + ' requests for ' + nsVoll + route + '.');\n        z.push(' *');\n        z.push(' * @param WP_REST_Request $request Full details about the request.');\n        z.push(' * @return WP_REST_Response|WP_Error');\n        z.push(' *\/');\n        z.push('function ' + basis + '_' + methode.toLowerCase() + '( WP_REST_Request $request ) {');\n        let breite = 4;\n        argumente.forEach(function (a) {\n            const laenge = (a.name === 'request' ? a.name.length + 6 : a.name.length);\n            if (laenge > breite) { breite = laenge; }\n        });\n        argumente.forEach(function (a) {\n            const variable = (a.name === 'request' ? 'param_' + a.name : a.name);\n            z.push('    $' + fuelle(variable, breite) + \" = $request->get_param( '\" + phpText(a.name) + \"' );\");\n        });\n        if (argumente.length > 0) { z.push(''); }\n        z.push('    \/\/ TODO: replace this demo payload with your own work.');\n        z.push('    $data = array(');\n        if (argumente.length > 0) {\n            let schluessel = 0;\n            argumente.forEach(function (a) {\n                if (a.name.length + 2 > schluessel) { schluessel = a.name.length + 2; }\n            });\n            argumente.forEach(function (a) {\n                const variable = (a.name === 'request' ? 'param_' + a.name : a.name);\n                z.push('        ' + fuelle(\"'\" + phpText(a.name) + \"'\", schluessel) + ' => $' + variable + ',');\n            });\n        } else {\n            z.push(\"        'ok' => true,\");\n        }\n        z.push('    );');\n        z.push('');\n        z.push('    if ( empty( $data ) ) {');\n        z.push('        return new WP_Error(');\n        z.push(\"            '\" + basis + \"_not_found',\");\n        z.push(\"            'No data was found for this request.',\");\n        z.push(\"            array( 'status' => 404 )\");\n        z.push('        );');\n        z.push('    }');\n        z.push('');\n        z.push('    return new WP_REST_Response( $data, ' + ERFOLG[methode] + ' );');\n        z.push('}');\n        return z.join('\\n');\n    }<\/p>\n<p>    function rechteName(basis, stufe, methode) {\n        if (stufe === 'public' && methode === 'GET') { return \"'__return_true'\"; }\n        if (stufe === 'public') { return \"'\" + basis + \"_write_permission_check'\"; }\n        return \"'\" + basis + \"_permission_check'\";\n    }<\/p>\n<p>    function baueRegister(basis, nsVoll, route, methoden, stufe, hatArgs) {\n        const z = [];\n        z.push(\"add_action( 'rest_api_init', '\" + basis + \"_register_routes' );\");\n        z.push('');\n        z.push('\/**');\n        z.push(' * Registers the route.');\n        z.push(' *');\n        z.push(' * @return void');\n        z.push(' *\/');\n        z.push('function ' + basis + '_register_routes() {');\n        z.push('    register_rest_route(');\n        z.push(\"        '\" + phpText(nsVoll) + \"',\");\n        z.push(\"        '\" + phpText(route) + \"',\");\n        z.push('        array(');\n        methoden.forEach(function (m) {\n            const ruf = rechteName(basis, stufe, m);\n            z.push('            array(');\n            z.push('                ' + fuelle(\"'methods'\", 21) + \" => '\" + m + \"',\");\n            z.push('                ' + fuelle(\"'callback'\", 21) + \" => '\" + basis + '_' + m.toLowerCase() + \"',\");\n            if (ruf === \"'__return_true'\") {\n                z.push('                \/\/ Read only endpoint, open to everyone on purpose.');\n            }\n            z.push('                ' + fuelle(\"'permission_callback'\", 21) + ' => ' + ruf + ',');\n            z.push('                ' + fuelle(\"'args'\", 21) + ' => ' + (hatArgs ? basis + '_args()' : 'array()') + ',');\n            z.push('            ),');\n        });\n        z.push('        )');\n        z.push('    );');\n        z.push('}');\n        return z.join('\\n');\n    }<\/p>\n<p>    function beispielRoute(route) {\n        let r = String(route);\n        r = r.replace(\/\\(\\?P<[A-Za-z_][A-Za-z0-9_]*>([^)]*)\\)\/g, function (ganz, muster) {\n            return \/\\\\d|0-9\/.test(muster) ? '123' : 'example';\n        });\n        r = r.replace(\/\\([^()]*\\)\/g, 'example');\n        return r;\n    }<\/p>\n<p>    function formatiere(methode, nsVoll, route, argumente, stufe) {\n        const belegt = platzhalter(route);\n        const frei = argumente.filter(function (a) { return belegt.indexOf(a.name) === -1; });\n        const koerper = MIT_KOERPER.indexOf(methode) !== -1;\n        let url = 'https:\/\/example.com\/wp-json\/' + nsVoll + beispielRoute(route);\n        if (!koerper && frei.length > 0) {\n            url += '?' + frei.map(function (a) {\n                return encodeURIComponent(a.name) + '=' + encodeURIComponent(TYPEN[a.typ].beispiel);\n            }).join('&');\n        }\n        const zeilen = ['curl -X ' + methode + ' \"' + url + '\"'];\n        if (stufe !== 'public' || SCHREIBEND.indexOf(methode) !== -1) {\n            zeilen.push('  -u \"USERNAME:APPLICATION PASSWORD\"');\n        }\n        if (koerper && frei.length > 0) {\n            zeilen.push('  -H \"Content-Type: application\/json\"');\n            zeilen.push(\"  -d '{\" + frei.map(function (a) {\n                return '\"' + a.name + '\":' + TYPEN[a.typ].json;\n            }).join(',') + \"}'\");\n        }\n        return zeilen.join(' \\\\\\n');\n    }<\/p>\n<p>    function berechne(eingabe) {\n        const ergebnis = { fehler: [], warnungen: [], php: '', aufrufe: [] };\n        const roh = eingabe || {};\n        const ns = String(roh.namensraum === undefined ? '' : roh.namensraum).trim();\n        const version = String(roh.version === undefined ? '' : roh.version).trim();\n        const route = String(roh.route === undefined ? '' : roh.route).trim();\n        const stufe = String(roh.stufe === undefined ? 'capability' : roh.stufe);\n        const faehigkeit = String(roh.faehigkeit === undefined ? '' : roh.faehigkeit).trim();\n        const rohMethoden = (roh.methoden || []);\n        const methoden = METHODEN.filter(function (m) { return rohMethoden.indexOf(m) !== -1; });\n        const rohArgumente = (roh.argumente || []);<\/p>\n<p>        const hatSchreib = methoden.some(function (m) { return SCHREIBEND.indexOf(m) !== -1; });\n        const brauchtFaehigkeit = (stufe === 'capability') || (stufe === 'public' && hatSchreib);<\/p>\n<p>        if (!\/^[A-Za-z0-9_-]+$\/.test(ns)) { ergebnis.fehler.push('Enter a namespace. Letters, digits, hyphen and underscore are allowed.'); }\n        if (!\/^[A-Za-z0-9_-]+$\/.test(version)) { ergebnis.fehler.push('Enter a version. Letters, digits, hyphen and underscore are allowed.'); }\n        if (route.charAt(0) !== '\/') { ergebnis.fehler.push('Enter a route that starts with a slash, for example \/items.'); }\n        if (methoden.length === 0) { ergebnis.fehler.push('Select at least one HTTP method.'); }\n        if (brauchtFaehigkeit && !\/^[A-Za-z0-9_]+$\/.test(faehigkeit)) { ergebnis.fehler.push('Enter a capability. Letters, digits and underscores are allowed, for example manage_options.'); }<\/p>\n<p>        const argumente = [];\n        const gesehen = {};\n        rohArgumente.forEach(function (a) {\n            const name = String(a && a.name !== undefined ? a.name : '').trim();\n            if (name === '') { return; }\n            const typ = (a && TYPEN[a.typ]) ? a.typ : 'string';\n            if (!\/^[A-Za-z_][A-Za-z0-9_]*$\/.test(name)) {\n                ergebnis.fehler.push('The argument name %s does not work. Start with a letter or an underscore and use only letters, digits and underscores.'.replace('%s', name));\n                return;\n            }\n            if (gesehen[name] === true) {\n                ergebnis.fehler.push('The argument name %s appears more than once.'.replace('%s', name));\n                return;\n            }\n            gesehen[name] = true;\n            argumente.push({ name: name, typ: typ, pflicht: !!(a && a.pflicht) });\n        });<\/p>\n<p>        if (ergebnis.fehler.length > 0) { return ergebnis; }<\/p>\n<p>        const nsVoll = ns + '\/' + version;\n        const basis = phpBezeichner(ns) + '_' + phpBezeichner(version) + '_' + routenName(route);\n        const hatArgs = argumente.length > 0;<\/p>\n<p>        const teile = [];\n        teile.push('<?php');\n        teile.push('\/**');\n        teile.push(' * REST route: ' + nsVoll + route);\n        teile.push(' * Methods:    ' + methoden.join(', '));\n        teile.push(' *');\n        teile.push(' * Generated scaffold. Replace the demo payload with your own logic.');\n        teile.push(' *\/');\n        teile.push('');\n        teile.push(\"defined( 'ABSPATH' ) || exit;\");\n        teile.push('');\n        teile.push(baueRegister(basis, nsVoll, route, methoden, stufe, hatArgs));\n\n        if (hatArgs) {\n            teile.push('');\n            teile.push(baueArgsFunktion(basis + '_args', argumente));\n        }\n\n        if (stufe === 'loggedin') {\n            teile.push('');\n            teile.push(baueRechteFunktion(\n                basis + '_permission_check',\n                'is_user_logged_in()',\n                'rest_forbidden',\n                'You must be logged in to use this route.'\n            ));\n        } else if (stufe === 'capability') {\n            teile.push('');\n            teile.push(baueRechteFunktion(\n                basis + '_permission_check',\n                \"current_user_can( '\" + phpText(faehigkeit) + \"' )\",\n                'rest_forbidden',\n                'You are not allowed to use this route.'\n            ));\n        } else if (stufe === 'apppassword') {\n            teile.push('');\n            teile.push(baueRechteFunktion(\n                basis + '_permission_check',\n                'is_user_logged_in() &#038;&#038; rest_get_authenticated_app_password()',\n                'rest_application_password_required',\n                'This route only accepts requests that are authenticated with an application password.'\n            ));\n        } else if (stufe === 'public' &#038;&#038; hatSchreib) {\n            teile.push('');\n            teile.push(baueRechteFunktion(\n                basis + '_write_permission_check',\n                \"current_user_can( '\" + phpText(faehigkeit) + \"' )\",\n                'rest_forbidden',\n                'You are not allowed to write to this route.'\n            ));\n        }\n\n        methoden.forEach(function (m) {\n            teile.push('');\n            teile.push(baueCallback(basis, m, nsVoll, route, argumente));\n        });\n\n        ergebnis.php = teile.join('\\n') + '\\n';\n\n        ergebnis.aufrufe = methoden.map(function (m) {\n            return formatiere(m, nsVoll, route, argumente, stufe);\n        });\n\n        if (stufe === 'public' &#038;&#038; hatSchreib) {\n            ergebnis.warnungen.push('Public write access is never safe, so the write methods were given a capability check against %s instead. Pick a permission level that matches what the route really does.'.replace('%s', faehigkeit));\n        }\n        if (hatSchreib &#038;&#038; stufe !== 'apppassword') {\n            ergebnis.warnungen.push('Requests that authenticate through the login cookie also need a valid X-WP-Nonce header for the wp_rest action. Without it WordPress treats the request as logged out and the permission callback fails.');\n        }\n        if (RESERVIERT.indexOf(ns.toLowerCase()) !== -1) {\n            ergebnis.warnungen.push('The namespace %s is reserved by WordPress core. Your route can collide with a core endpoint, so pick a namespace of your own.'.replace('%s', ns));\n        }\n        const offen = platzhalter(route).filter(function (name) { return gesehen[name] !== true; });\n        if (offen.length > 0) {\n            ergebnis.warnungen.push('The route carries the placeholder %s, but no argument of that name is declared. Add it so the value gets sanitized and validated.'.replace('%s', offen.join(', ')));\n        }<\/p>\n<p>        return ergebnis;\n    }<\/p>\n<p>    \/\/ fuer die Pruefung von aussen erreichbar\n    if (typeof window !== 'undefined') {\n        window.LW_TEST = window.LW_TEST || {};\n        window.LW_TEST.RR = {\n            berechne: berechne,\n            formatiere: formatiere,\n            phpBezeichner: phpBezeichner,\n            phpText: phpText,\n            platzhalter: platzhalter\n        };\n    }<\/p>\n<p>    \/\/ --- DOM-Anbindung, sauber abgesichert ---<\/p>\n<p>    const btn = document.getElementById('rr-btn');\n    const out = document.getElementById('rr-ergebnis');\n    if (!btn || !out) { return; }<\/p>\n<p>    const argBox = document.getElementById('rr-argumente');\n    const argPlus = document.getElementById('rr-arg-plus');<\/p>\n<p>    const FELD_STIL = 'padding: 10px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; font-family: monospace;';<\/p>\n<p>    function baueZeile(name, typ, pflicht) {\n        const zeile = document.createElement('div');\n        zeile.setAttribute('style', 'display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 10px;');<\/p>\n<p>        const nameFeld = document.createElement('input');\n        nameFeld.type = 'text';\n        nameFeld.className = 'form-control';\n        nameFeld.value = name;\n        nameFeld.placeholder = 'Argument name';\n        nameFeld.setAttribute('style', 'flex: 1; min-width: 150px; ' + FELD_STIL);\n        zeile.appendChild(nameFeld);<\/p>\n<p>        const typFeld = document.createElement('select');\n        typFeld.className = 'form-control';\n        typFeld.setAttribute('style', 'min-width: 120px; ' + FELD_STIL);\n        ['string', 'integer', 'number', 'boolean', 'array'].forEach(function (t) {\n            const o = document.createElement('option');\n            o.value = t;\n            o.textContent = t;\n            if (t === typ) { o.selected = true; }\n            typFeld.appendChild(o);\n        });\n        zeile.appendChild(typFeld);<\/p>\n<p>        const pflichtFeld = document.createElement('input');\n        pflichtFeld.type = 'checkbox';\n        pflichtFeld.checked = pflicht === true;<\/p>\n<p>        const pflichtLabel = document.createElement('label');\n        pflichtLabel.setAttribute('style', 'color: var(--text-secondary, #9aa0aa); font-size: 0.85rem; display: flex; align-items: center; gap: 6px;');\n        pflichtLabel.appendChild(pflichtFeld);\n        pflichtLabel.appendChild(document.createTextNode(' Required'));\n        zeile.appendChild(pflichtLabel);<\/p>\n<p>        const weg = document.createElement('button');\n        weg.type = 'button';\n        weg.className = 'button';\n        weg.textContent = 'Remove';\n        weg.setAttribute('style', 'background: transparent; color: var(--text-secondary, #9aa0aa); border: 1px solid var(--border, #2a2a35); padding: 9px 14px; border-radius: 6px; cursor: pointer;');\n        weg.addEventListener('click', function () {\n            if (zeile.parentNode) { zeile.parentNode.removeChild(zeile); }\n        });\n        zeile.appendChild(weg);<\/p>\n<p>        return zeile;\n    }<\/p>\n<p>    function leseArgumente() {\n        const liste = [];\n        if (!argBox) { return liste; }\n        const zeilen = argBox.children;\n        for (let i = 0; i < zeilen.length; i++) {\n            const nameFeld = zeilen[i].querySelector('input[type=\"text\"]');\n            const typFeld = zeilen[i].querySelector('select');\n            const pflichtFeld = zeilen[i].querySelector('input[type=\"checkbox\"]');\n            if (!nameFeld || !typFeld || !pflichtFeld) { continue; }\n            liste.push({\n                name: nameFeld.value,\n                typ: typFeld.value,\n                pflicht: pflichtFeld.checked\n            });\n        }\n        return liste;\n    }\n\n    function leseMethoden() {\n        const liste = [];\n        METHODEN.forEach(function (m) {\n            const feld = document.getElementById('rr-m-' + m.toLowerCase());\n            if (feld &#038;&#038; feld.checked) { liste.push(m); }\n        });\n        return liste;\n    }\n\n    function wert(id) {\n        const feld = document.getElementById(id);\n        return feld ? feld.value : '';\n    }\n\n    function ueberschrift(text) {\n        const p = document.createElement('p');\n        p.setAttribute('style', 'color: var(--text-primary, #e8e8ee); font-weight: 700; margin: 0 0 8px 0;');\n        p.textContent = text;\n        return p;\n    }\n\n    function codeBlock(text) {\n        const pre = document.createElement('pre');\n        pre.setAttribute('style', 'background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; padding: 15px; margin: 0 0 20px 0; overflow-x: auto; font-family: monospace; font-size: 0.82rem; line-height: 1.5; white-space: pre;');\n        pre.textContent = text;\n        return pre;\n    }\n\n    function zeigeFehler(liste) {\n        out.textContent = '';\n        const kasten = document.createElement('div');\n        kasten.setAttribute('style', 'background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); border-left: 3px solid var(--accent, #35d0a5); border-radius: 6px; padding: 15px;');\n        kasten.appendChild(ueberschrift('Please correct this first:'));\n        const ul = document.createElement('ul');\n        ul.setAttribute('style', 'margin: 0; padding-left: 20px; color: var(--text-secondary, #9aa0aa);');\n        liste.forEach(function (t) {\n            const li = document.createElement('li');\n            li.setAttribute('style', 'margin-bottom: 5px;');\n            li.textContent = t;\n            ul.appendChild(li);\n        });\n        kasten.appendChild(ul);\n        out.appendChild(kasten);\n    }\n\n    function zeigeErgebnis(daten) {\n        out.textContent = '';\n\n        out.appendChild(ueberschrift('PHP code'));\n        out.appendChild(codeBlock(daten.php));\n\n        out.appendChild(ueberschrift('Example calls'));\n        out.appendChild(codeBlock(daten.aufrufe.join('\\n\\n')));\n\n        out.appendChild(ueberschrift('Notes and warnings'));\n        const kasten = document.createElement('div');\n        kasten.setAttribute('style', 'background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); border-left: 3px solid var(--accent, #35d0a5); border-radius: 6px; padding: 15px;');\n        if (daten.warnungen.length === 0) {\n            const p = document.createElement('p');\n            p.setAttribute('style', 'margin: 0; color: var(--text-secondary, #9aa0aa);');\n            p.textContent = 'Nothing stands out. Read the generated callbacks once more before you put them into production.';\n            kasten.appendChild(p);\n        } else {\n            const ul = document.createElement('ul');\n            ul.setAttribute('style', 'margin: 0; padding-left: 20px; color: var(--text-secondary, #9aa0aa);');\n            daten.warnungen.forEach(function (t) {\n                const li = document.createElement('li');\n                li.setAttribute('style', 'margin-bottom: 8px;');\n                li.textContent = t;\n                ul.appendChild(li);\n            });\n            kasten.appendChild(ul);\n        }\n        out.appendChild(kasten);\n    }\n\n    if (argBox) {\n        argBox.appendChild(baueZeile('id', 'integer', true));\n    }\n\n    if (argPlus &#038;&#038; argBox) {\n        argPlus.addEventListener('click', function () {\n            argBox.appendChild(baueZeile('', 'string', false));\n        });\n    }\n\n    btn.addEventListener('click', function () {\n        const daten = berechne({\n            namensraum: wert('rr-namensraum'),\n            version: wert('rr-version'),\n            route: wert('rr-route'),\n            methoden: leseMethoden(),\n            argumente: leseArgumente(),\n            stufe: wert('rr-stufe'),\n            faehigkeit: wert('rr-faehigkeit')\n        });\n\n        if (daten.fehler.length > 0) {\n            zeigeFehler(daten.fehler);\n            return;\n        }<\/p>\n<p>        zeigeErgebnis(daten);\n    });\n})();\n<\/script><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Describe the route once and get the whole scaffold back: the register_rest_route call, an argument schema with a sanitize and a validate callback for every parameter, a permission callback that matches the level you picked, and callback functions that return WP_REST_Response or WP_Error. Nothing leaves this browser tab. Namespace Version Route, regular expression placeholders allowed [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":38,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"template-tool-base.php","meta":{"footnotes":""},"class_list":["post-9429","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>WordPress REST Route &amp; Permission Callback Generator - Lukas Wojcik - Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/wordpress-rest-route-permission-generator\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"WordPress REST Route &amp; Permission Callback Generator - Lukas Wojcik - Blog\" \/>\n<meta property=\"og:description\" content=\"Describe the route once and get the whole scaffold back: the register_rest_route call, an argument schema with a sanitize and a validate callback for every parameter, a permission callback that matches the level you picked, and callback functions that return WP_REST_Response or WP_Error. Nothing leaves this browser tab. Namespace Version Route, regular expression placeholders allowed [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/wordpress-rest-route-permission-generator\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"article:modified_time\" content=\"2026-08-10T11:45:12+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/wordpress-rest-route-permission-generator\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/wordpress-rest-route-permission-generator\\\/\",\"name\":\"WordPress REST Route & Permission Callback Generator - Lukas Wojcik - Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"datePublished\":\"2026-08-10T11:37:21+00:00\",\"dateModified\":\"2026-08-10T11:45:12+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/wordpress-rest-route-permission-generator\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/wordpress-rest-route-permission-generator\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/wordpress-rest-route-permission-generator\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Toolbox\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"WordPress REST Route &#038; Permission Callback Generator\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"luky\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"luky\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"WordPress REST Route & Permission Callback Generator - Lukas Wojcik - Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/wordpress-rest-route-permission-generator\/","og_locale":"en_US","og_type":"article","og_title":"WordPress REST Route & Permission Callback Generator - Lukas Wojcik - Blog","og_description":"Describe the route once and get the whole scaffold back: the register_rest_route call, an argument schema with a sanitize and a validate callback for every parameter, a permission callback that matches the level you picked, and callback functions that return WP_REST_Response or WP_Error. Nothing leaves this browser tab. Namespace Version Route, regular expression placeholders allowed [&hellip;]","og_url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/wordpress-rest-route-permission-generator\/","og_site_name":"Lukas Wojcik - Blog","article_modified_time":"2026-08-10T11:45:12+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg","type":"image\/jpeg"}],"twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/wordpress-rest-route-permission-generator\/","url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/wordpress-rest-route-permission-generator\/","name":"WordPress REST Route & Permission Callback Generator - Lukas Wojcik - Blog","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"datePublished":"2026-08-10T11:37:21+00:00","dateModified":"2026-08-10T11:45:12+00:00","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/wordpress-rest-route-permission-generator\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/wordpress-rest-route-permission-generator\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/wordpress-rest-route-permission-generator\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Toolbox","item":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/"},{"@type":"ListItem","position":3,"name":"WordPress REST Route &#038; Permission Callback Generator"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"luky","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"luky"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/9429","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=9429"}],"version-history":[{"count":1,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/9429\/revisions"}],"predecessor-version":[{"id":9447,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/9429\/revisions\/9447"}],"up":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/38"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=9429"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}