Appearance
For Developers
Experimental until 1.0
Everything in this section works and is used by the plugin itself, but it is not a frozen contract yet. Names and signatures may change before 1.0. If you build on it, say which version you built against — and tell us, because real usage is what decides which parts get locked down.
What is not here matters too: see "What you cannot extend yet" below before planning anything, so you don't spend an afternoon looking for a hook that isn't there.
Data
| What | Where |
|---|---|
| Ledger (append-only, source of truth) | wp_vxp_ledger |
| Rewards catalog | wp_vxp_rewards |
| Redemptions | wp_vxp_redemptions |
| Badges held | wp_vxp_user_badges |
| Cached balance | usermeta vxp_balance |
| Cached level | usermeta vxp_level |
| Daily streak | usermeta vxp_streak |
| Leaderboard opt-out | usermeta vxp_leaderboard_optout |
Everything derived — balances, levels, ranks, leaderboards — is a cache rebuildable from the ledger. If anything ever looks wrong, the ledger is right and the cache is not: wp voxel-points recount.
Read the ledger through the methods below rather than querying the table directly. The schema is the least stable thing here.
Getting at the plugin
php
$vxp = Voxel_Points_Plugin::instance();That object exposes the services this section documents: ->ledger, ->levels, ->badges, ->rewards, ->settings, ->members, and ->streaks.
Other properties on it (->earning, ->reversals, ->name_badge, ->backfill, ->importers, ->expiry, ->rest) are internal — wiring for the plugin's own screens and event handling, and the parts most likely to change. Treat them as private.
Check for null before using ->streaks. It, along with ->earning, ->reversals and ->name_badge, is only constructed when the Voxel theme is active; on a site running another theme those are null while the ledger, levels, badges and rewards all keep working.
Reading a member's points
php
$ledger = Voxel_Points_Plugin::instance()->ledger;
$ledger->get_balance( $user_id ); // spendable balance, can be negative
$ledger->get_display_balance( $user_id ); // the same, floored at 0 if the site asks
$ledger->earned_total( $user_id ); // lifetime earned, net of reversals
$ledger->rank( $user_id ); // all-time rank, or null if unranked
$ledger->query( [ 'user_id' => $user_id, 'per_page' => 20 ] );
$ledger->leaderboard( $from, $to, 10, $excluded_roles );Levels, badges and streaks:
php
$vxp = Voxel_Points_Plugin::instance();
$vxp->levels->get_user_level( $user_id ); // current level, or null
$vxp->levels->get_progress( $user_id ); // current + next + percentage
$vxp->badges->get_user_badges( $user_id ); // full definitions, with award dates
$vxp->badges->user_has( $user_id, 'first_review' );
// Null without the Voxel theme, so check first (the plugin targets PHP 7.4,
// where there is no ?-> operator).
if ( $vxp->streaks ) {
$vxp->streaks->get_streak( $user_id ); // current + longest
}Levels track lifetime earned, not balance, so spending never demotes.
Writing points from your own code
php
$ledger = Voxel_Points_Plugin::instance()->ledger;
// Award, deduped: the same (user, event, reference) can only ever award once.
$ledger->award( $user_id, 25, [
'event_key' => 'my_plugin/thing_done',
'reference_type' => 'thing',
'reference_id' => $thing_id,
'note' => 'Did the thing',
] );
$ledger->spend( $user_id, 100, [ 'event_key' => 'my_plugin/bought' ] );
$ledger->adjust( $user_id, -50, 'Manual correction' );
$ledger->reverse_row( $row_id, 'Undone' );award() returns the new row id, or 0 when it was deduped — that is the signal the reference had already earned, not an error. Dedup is enforced by a UNIQUE database index, so concurrent requests cannot both win.
Pick an event_key namespaced to your plugin. It is what dedup, daily caps and rule-count badges all group by, and it shows up in the member's history.
Awards written this way are ordinary ledger rows: they move balances, fire the actions below, count toward levels, and can be reversed. They do not appear in the Earning screen — that lists this plugin's own rules.
Actions
php
// Every ledger write. $type is earn|spend|reversal|adjust|expire.
do_action( 'voxel_points/ledger/row_added',
$row_id, $user_id, $points, $type, $args, $balance );
do_action( 'voxel_points/level_up', $user_id, $level, $previous_key );
do_action( 'voxel_points/badge_earned', $user_id, $badge );
do_action( 'voxel_points/streak/advanced', $user_id, $current, $longest );
do_action( 'voxel_points/streak/milestone', $user_id, $streak_day );ledger/row_added is the one to hook for anything reactive — it fires after the balance has been recounted, so $balance is already correct.
Careful with re-entrancy: calling award() inside a ledger/row_added handler fires the action again. Guard it, or dedup it with a reference the second call will collide on.
App events
Every one of the above is also a Voxel app event, which is usually the easier route — no code, and Voxel Toolkit's SMS, Push and Notification Preferences pick them up for free. Eight are registered; see App events & notifications.
REST
Namespace voxel-points/v1. Two tiers:
| Tier | Requires | Routes |
|---|---|---|
| Admin | manage_options | the admin screens' own API — activity, members, earning, rewards, achievements, settings, import, backfill, license |
| Member | logged in | /store, /store/posts, /store/redeem, /me/privacy |
All routes take the standard X-WP-Nonce header, which means they are built for same-origin browser calls, not for external services. There is deliberately no token auth and no public read API yet — see below.
WP-CLI
wp voxel-points recount | balance | stats
wp voxel-points import | import-undo
wp voxel-points backfill | backfill-undo | backfill-cancel
wp voxel-points badge | backfill-badges | purge-orphans
wp voxel-points expireimport, backfill and expire all take --dry-run. See Maintenance & Tooling.
What you cannot extend yet
Being straight about the edges, so nobody goes looking for a hook that isn't there. As of v0.7.4 the plugin fires actions but has no filters, and three catalogs are closed:
| You might want to | Today |
|---|---|
| Add your own earning rule to the Earning screen | Not possible. The rule catalog is built internally. Award directly with $ledger->award() instead — it behaves identically, it just isn't configurable in the UI. |
| Add a reward type | Not possible. Types are promotion, priority_boost, manual. Use a manual reward and act on the fulfillment yourself. |
| Add a badge criterion | Not possible. Criteria are points, rule count, level, streak, manual. Use a manual badge and award it with $vxp->badges->award(). |
| Change or block an award before it lands | Not possible — there is no before_award filter. Decide before you call award(). |
| Read a member's points from an external app | Not directly. The REST layer is nonce-authenticated for same-origin use; there is no public read endpoint or token auth. |
Each of those has a working way around it today, which is why they are not urgent. If one of them is blocking something you are building, that is the useful thing to report — the workarounds above are what decide which of these gets a real extension point first.
Design guarantees
- The plugin never modifies Voxel behavior. Points observe events; they never change what an event does. Every integration is additive.
- History is never rewritten. Corrections are new rows (reversals), never edits or deletes. The one mutable column is
dedup_key, and only to release a slot so a legitimately re-triggered event can award again. - Nothing is destroyed on deactivate. Uninstall drops data only when Keep data on uninstall is switched off.

