I built TrustGate from scratch — a business review and trust platform for the Indian market. Think verified business profiles, customer reviews, owner responses, and an embeddable trust badge that pulls live data.
The entire backend runs on a custom WordPress plugin using the REST API exclusively. No admin-ajax.php. Not a single call to it.
This post is about what I learned building it, what broke, what I got wrong the first time, and the decisions that actually held up.
Why REST API and not admin-ajax
When I started, the easy path was admin-ajax.php. Every tutorial uses it. It works. But it has real problems for a platform of this type.
Every request, regardless of whether a user is logged in or not, boots the entire WordPress admin. That is expensive. For a review platform where anonymous users are searching and browsing businesses constantly, that overhead adds up fast.
The REST API routes only load what they need. Public endpoints run lean. Authenticated endpoints check permissions early and bail out before doing unnecessary work.
The other reason is structure. admin-ajax.php encourages you to dump everything into one giant handler function. The REST API forces you into proper route registration with explicit HTTP methods, permission callbacks, and sanitization callbacks — all separated. Your code ends up cleaner whether you like it or not.
The basic structure I settled on
Everything lives under a versioned namespace:
add_action( 'rest_api_init', function() {
register_rest_route( 'trustgate/v1', '/businesses', [
'methods' => 'GET',
'callback' => 'tg_get_businesses',
'permission_callback' => '__return_true',
]);
register_rest_route( 'trustgate/v1', '/reviews', [
'methods' => 'POST',
'callback' => 'tg_submit_review',
'permission_callback' => 'tg_is_logged_in',
]);
});
Public routes like business listings get __return_true as the permission callback. Anything that writes data — submitting a review, claiming a business, owner responses — requires authentication.
The permission callback runs before the main callback. If it returns false, WordPress automatically returns a 403 before your main function even runs. This is clean and hard to accidentally skip.
The NULL safety problem I kept hitting
The platform stores a lot of optional data. Business logos, banner images, contact numbers, website URLs — not every business has all of these filled in. Early on I was doing things like:
$logo_url = get_post_meta( $business_id, 'tg_logo_url', true );
echo '<img src="' . $logo_url . '">';
Which works until it doesn't. Empty strings, NULL values from the database, and unset meta keys all behave slightly differently in PHP depending on context. The approach that fixed this completely was wrapping every meta retrieval:
function tg_get_meta( $post_id, $key, $default = '' ) {
$value = get_post_meta( $post_id, $key, true );
return ( $value !== '' && $value !== null && $value !== false )
? $value
: $default;
}
One function. Every meta call goes through it. No more inconsistent empty values breaking JSON responses.
Sanitization and validation are not the same thing
This distinction cost me time. I was mixing them up.
Validation checks whether the input is acceptable. If it fails, you return an error.
Sanitization cleans the input. You use it when you'd rather transform bad input than reject it.
For review submissions, the star rating should be validated — it must be an integer between 1 and 5, and if it isn't, the request should fail:
register_rest_route( 'trustgate/v1', '/reviews', [
'methods' => 'POST',
'callback' => 'tg_submit_review',
'permission_callback' => 'tg_is_logged_in',
'args' => [
'rating' => [
'required' => true,
'validate_callback' => function( $value ) {
return is_numeric( $value )
&& (int) $value >= 1
&& (int) $value <= 5;
},
],
'review_text' => [
'required' => true,
'sanitize_callback' => 'sanitize_textarea_field',
],
],
]);
Review text gets sanitized — strip any HTML, clean whitespace, but don't reject the whole request over formatting. Rating gets validated strictly. This split makes the API predictable.
Handling the trust badge
The embeddable badge was the most interesting technical piece to build.
Each verified business gets a badge they can paste on their website — a small script tag that renders their current TrustGate rating and review count dynamically. The script hits a public REST endpoint:
GET /wp-json/trustgate/v1/badge/{business_id}
Which returns:
{
"business_name": "Example Business",
"rating": 4.7,
"review_count": 83,
"verified": true,
"badge_url": "https://trustgate.in/business/example-business"
}
The embedded script fetches this and renders the badge in the business's own website. Under 4KB. Loads asynchronously so it has zero impact on the host page's performance.
The key decision here was keeping this endpoint completely public and cached aggressively. No authentication, no session lookup, just a fast database read and a JSON response. Response time for this endpoint matters more than almost anything else because it affects websites that are not even ours.
The claim verification workflow
Business owners can claim their profile by submitting documents — GST certificate, MCA filing, Udyam certificate. The claim goes into a pending queue that an admin reviews manually.
I built a three-attempt limit into this. After three failed or rejected claim attempts, the user account is flagged and the claim form is blocked. This was necessary because some users were submitting incomplete documentation repeatedly without fixing anything.
The flag status is stored in user meta and checked by a permission callback before the claim endpoint even processes:
function tg_can_submit_claim( $request ) {
if ( ! is_user_logged_in() ) {
return new WP_Error(
'not_logged_in',
'You must be logged in.',
[ 'status' => 401 ]
);
}
$attempts = (int) get_user_meta(
get_current_user_id(),
'tg_claim_attempts',
true
);
if ( $attempts >= 3 ) {
return new WP_Error(
'claim_blocked',
'Maximum claim attempts reached.',
[ 'status' => 403 ]
);
}
return true;
}
Returning a WP_Error from a permission callback sends the right HTTP status code automatically. The client receives a 403 with a clear message. No extra handling needed in the main callback.
Structured data and SEO
Every business profile page outputs JSON-LD structured data built from the REST API response. The review schema tells Google about the aggregate rating, individual reviews, and the business itself.
This was a deliberate architecture decision. The data driving the structured data markup comes from the same place as the data driving the page — the REST API. No duplication, no chance of them going out of sync.
The platform started ranking on Google within weeks of launch. Positions 2 to 7 for target keywords with zero advertising. Structured data was a significant factor.
What I got wrong
A few things I would do differently:
Custom tables vs post meta. Reviews are stored as a custom post type with meta. This made sense early on for simplicity but it is getting slow as volume grows. Custom database tables with proper indexing would have been faster and easier to query for aggregate data like average ratings.
Caching too late. I added object caching to expensive queries after I noticed slowdowns rather than designing for it from the start. Building cache invalidation into the write endpoints from day one would have saved refactoring time.
Not versioning endpoints sooner. The /trustgate/v1/ namespace is there from the start, which is good. But I did not think carefully about what a v2 would need to change until v1 was already embedded in the badge scripts on other websites. Versioning is cheap to add and expensive to skip.
The one thing that helped most
Treating every endpoint like a public API even when it was only being used internally. That means proper HTTP status codes, consistent JSON response shapes, explicit error messages, and no shortcuts on permission checks.
When you do this, debugging becomes straightforward. The browser dev tools tell you exactly what went wrong and why. Frontend code can rely on the response structure. And when you eventually do open an endpoint to external use, you have nothing to fix.
WordPress gets dismissed as a toy by a lot of developers. The REST API is genuinely good. The constraint is not the framework — it is whether you treat it seriously.
TrustGate is live at trustgate.in. Feedback welcome.
Top comments (0)