Skip to content

Wbcom Credits SDK

This reference covers WP Career Board Pro 1.4.3.

The credit system is built on the Wbcom Credits SDK - a bundled library at libs/wbcom-credits-sdk/ (kept in libs/, not vendor/, so it always ships in the release zip). It provides:

  • An append-only ledger (the {prefix}_credit_ledger table; for this plugin the prefix is wcb, so wp_wcb_credit_ledger).
  • A consumer pattern (entities that spend credits - job posting, featured upgrade).
  • An adapter pattern for e-commerce plugins (WooCommerce, WC Subscriptions, WC Memberships, PMPro, MemberPress).
  • A gateway pattern for direct payment processors (Stripe, PayPal).

This doc covers the contract for registering a slug and writing your own consumer, adapter, or gateway.

+-------------------------------------------+
| Wbcom Credits SDK (libs/) |
| |
| +--------------+ |
| | Ledger | <- single source |
| | (DB writes) | of truth |
| +--------------+ |
| ^ |
| +----+----+----------+ |
| | | | |
| Consumers Adapters Gateways |
| (job_post, (Woo, (Stripe, |
| featured) WCS, WCM, PayPal - |
| PMPro, direct) |
| MemberPress) |
+-----^----------^----------^---------------+
| | |
hold/deduct on order on webhook
on actions completed verified

A plugin registers everything (slug, prefix, consumers, settings) in one call on the wbcom_credits_sdk_registry action, which fires before the SDK loads. Pro does this in wp-career-board-pro.php:

add_action( 'wbcom_credits_sdk_registry', function ( \Wbcom\Credits\Registry $registry ) {
$registry->register( array(
'slug' => 'wp-career-board',
'prefix' => 'wcb', // table prefix: {wp}_wcb_credit_ledger
'version' => WCBP_VERSION,
'file' => WCBP_FILE,
'user_type' => 'employer',
'consumers' => array( /* see below */ ),
'settings' => array(
'low_threshold' => 5,
'purchase_url' => '',
'admin_settings_hook' => 'wcb_admin_settings_tabs',
),
) );
} );

A “consumer” is something the SDK debits credits FOR. Each consumer declares three lifecycle actions: hold_on (reserve credits), deduct_on (settle the hold permanently), and refund_on (release the hold). The SDK adds the listeners; you fire the actions. The cost callback receives the item id and returns the credit cost.

Pro registers two consumers inside the consumers array of its register() call:

'consumers' => array(
array(
'id' => 'job_post',
'label' => __( 'Job Posting', 'wp-career-board-pro' ),
'cost' => static function ( int $item_id ): int {
$board_id = (int) get_post_meta( $item_id, '_wcb_board_id', true );
$base = $board_id
? (int) ( ( new \WCB\Pro\Modules\Boards\BoardSettings() )->get( $board_id )['credit_cost'] ?? 0 )
: 0;
return (int) apply_filters( 'wcbp_consumer_cost', $base, get_current_user_id(), $item_id, $board_id, 'job_post' );
},
'hold_on' => 'wcb_job_created', // Hold credits when this action fires
'deduct_on' => 'wcb_job_approved', // Settle the hold when this fires
'refund_on' => 'wcb_job_rejected', // Release the hold when this fires
),
array(
'id' => 'featured_upgrade',
'label' => __( 'Featured Upgrade', 'wp-career-board-pro' ),
'cost' => static function ( int $item_id ): int {
$base = (int) get_option( 'wcbp_featured_upgrade_cost', 10 );
return (int) apply_filters( 'wcbp_consumer_cost', $base, get_current_user_id(), $item_id, 0, 'featured_upgrade' );
},
'hold_on' => 'wcb_featured_upgrade_requested',
'deduct_on' => 'wcb_featured_upgrade_completed',
'refund_on' => 'wcb_featured_upgrade_failed',
),
),

To add your own consumer, append another entry to the consumers array in your own register() call (or call $registry->register() again for a separate slug). The cost callback signature is function ( int $item_id ): int. Fire your hold_on / deduct_on / refund_on actions when the relevant lifecycle events happen in your code - the SDK takes care of the ledger writes.

Adapters - automatic credit grants from e-commerce plugins

Section titled “Adapters - automatic credit grants from e-commerce plugins”

An “adapter” listens for a specific plugin’s “order completed” event and writes a topup ledger row. The SDK ships five adapters, which self-register through the SDK’s adapter registry when their host plugin is active:

Adapter File Listens to
WooCommerce libs/wbcom-credits-sdk/src/Adapters/WooCommerce.php woocommerce_order_status_completed
WC Subscriptions libs/wbcom-credits-sdk/src/Adapters/WooSubscriptions.php WC Subscriptions renewal/payment events
WC Memberships libs/wbcom-credits-sdk/src/Adapters/WooMemberships.php WC Memberships grant events
Paid Memberships Pro libs/wbcom-credits-sdk/src/Adapters/PMPro.php PMPro membership-change / payment events
MemberPress libs/wbcom-credits-sdk/src/Adapters/MemberPress.php MemberPress transaction-completed event

Each adapter implements AdapterInterface (libs/wbcom-credits-sdk/src/Adapters/AdapterInterface.php). Note the methods are instance methods, not static:

interface AdapterInterface {
public function get_id(): string;
public function get_label(): string;
public function is_available(): bool; // host plugin active?
public function register_hooks( string $slug ): void;
public function get_mappable_items(): array; // products/levels for the admin mapping UI
}

To add a new adapter (e.g. for Easy Digital Downloads), implement the interface, hook the host plugin’s purchase event in register_hooks(), and write a topup row with Credits::topup():

namespace MyAddon\Credits;
use Wbcom\Credits\Adapters\AdapterInterface;
use Wbcom\Credits\Credits;
class EDD implements AdapterInterface {
public function get_id(): string {
return 'edd';
}
public function get_label(): string {
return 'Easy Digital Downloads';
}
public function is_available(): bool {
return class_exists( 'Easy_Digital_Downloads' );
}
public function register_hooks( string $slug ): void {
add_action( 'edd_complete_purchase', function ( $payment_id ) use ( $slug ) {
$user_id = (int) edd_get_payment_user_id( $payment_id );
foreach ( edd_get_payment_meta_cart_details( $payment_id ) as $item ) {
$credits = (int) $this->credits_for_product( $slug, (int) $item['id'] );
if ( $credits > 0 ) {
// Signature: topup( $slug, $user_id, $amount, $note )
Credits::topup( $slug, $user_id, $credits, 'EDD order #' . $payment_id );
}
}
});
}
public function get_mappable_items(): array {
// Return EDD products in the shape the admin mapping UI expects.
return array();
}
private function credits_for_product( string $slug, int $product_id ): int {
$mappings = (array) get_option( "{$slug}_credit_mappings", array() );
foreach ( $mappings as $row ) {
if ( 'edd' === ( $row['adapter'] ?? '' ) && (int) $row['item_id'] === $product_id ) {
return (int) $row['credits'];
}
}
return 0;
}
}

A “gateway” is for selling credits directly without an e-commerce plugin in between (Stripe Checkout, PayPal). The SDK ships two gateways: Stripe and PayPal (libs/wbcom-credits-sdk/src/Gateways/).

The checkout and webhook flow is reachable today. The SDK’s Webhook_Controller registers three REST routes per slug under the SDK’s own wbcom-credits/v1 namespace (separate from the plugin’s wcb/v1 namespace):

POST /wbcom-credits/v1/wp-career-board/checkout/{gateway} Create a hosted checkout session.
POST /wbcom-credits/v1/wp-career-board/webhook/{gateway} Public, provider-signed. Adjusts the ledger.
POST /wbcom-credits/v1/wp-career-board/refund/{gateway} Refund a prior checkout.

The webhook is what actually credits the ledger - it is verified by the gateway’s verify_signature() before any ledger write, so a forged callback can’t grant credits. Refunds initiated in the provider dashboard also flow back through the webhook.

If you’re writing a custom gateway, implement GatewayInterface (libs/wbcom-credits-sdk/src/Gateways/GatewayInterface.php). Abstract_Gateway provides a default handle_webhook() you can extend:

interface GatewayInterface {
public function get_id(): string;
public function get_label(): string;
public function is_available(): bool;
public function get_settings_fields(): array;
public function create_checkout( string $slug, int $user_id, int $credits,
int $price_cents, string $currency = 'USD',
?string $return_url = null ): string;
public function verify_signature( string $raw_body, array $headers ): bool;
public function normalize_event( array $payload ): ?Gateway_Event;
public function handle_webhook( string $slug, array $payload ): \WP_REST_Response;
public function refund( string $slug, string $session_id, ?int $amount_cents = null ): bool;
}

Once registered via Gateway_Registry, the admin Credits settings UI auto-discovers it.

Every credit movement writes one row. The table is named {wp_prefix}{prefix}_credit_ledger (for this plugin, wp_wcb_credit_ledger); the SDK creates one ledger table per registered prefix, so the table itself scopes the data and there is no slug column. Schema:

Column Type Notes
id bigint unsigned PK Auto-increment
user_id bigint unsigned The credit holder (employer)
item_id bigint unsigned The consumed entity (job, etc.); 0 for top-ups/adjustments
entry_type varchar(20) One of topup, hold, deduction, refund
amount int Signed: positive for topup/refund, negative for hold/deduction
note varchar(255) Free-form context string
created_at datetime Defaults to CURRENT_TIMESTAMP

Indexes: idx_user_id (user_id), idx_entry_type (entry_type). The balance is SUM(amount) for the user. The ledger is append-only; the only physical DELETE is cancel_hold().

To read the ledger:

$balance = \Wbcom\Credits\Credits::get_balance( 'wp-career-board', $user_id );
$rows = \Wbcom\Credits\Credits::get_ledger( 'wp-career-board', $user_id, 50 );
// or directly: \Wbcom\Credits\Ledger::get_history( 'wcb', $user_id, $limit, $offset )

Never INSERT/UPDATE the ledger directly - use the Credits helpers, which use these exact signatures:

Credits::topup( string $slug, int $user_id, int $amount, string $note = '' ): int|false;
Credits::hold( string $slug, int $user_id, int $amount, int $item_id, string $note = '' ): int|false;
Credits::deduct( string $slug, int $user_id, int $amount, int $item_id, string $note = '' ): bool;
Credits::refund( string $slug, int $user_id, int $amount, int $item_id, string $note = '' ): int|false;
Credits::cancel_hold( string $slug, int $user_id, int $item_id ): void;
Credits::adjust( string $slug, int $user_id, int $amount, string $note = '' ): int|false;
Credits::get_cost( string $slug, string $consumer_id, int $item_id = 0 ): int;
Credits::get_purchase_url( string $slug ): string;

Note the 4th argument to topup() is a string note, not an array. When a consumer settles, Ledger::deduct_with_hold_release() releases the outstanding hold (writing a refund row for the held amount) and writes a deduction row for the actual cost in one transaction-safe step.

  • libs/wbcom-credits-sdk/src/ - the SDK source (bundled, not loaded over the network).
  • audit/journeys/security/license-required-for-pro-rest.md - the contract that Pro REST (including credit endpoints) stays operational regardless of license status (license gates updates only, never runtime features).
  • Pro hooks for credits in 03-hooks-reference.md: wcbp_consumer_cost (cost filter), wcbp_credit_consumed, wcbp_credits_low, and wcbp_credits_topped_up (the last two re-emitted from the SDK’s wbcom_credits_low / wbcom_credits_topped_up).