To add a sportsbook to WordPress you don't install a plugin: you store an API key in wp-config.php, add one PHP function that opens a session for the logged-in member, and register a shortcode that prints two lines of HTML. Your WordPress users become the players, and The Sportsbook handles odds, bets and settlement inside an iframe. It's a play-for-fun sportsbook: members bet free tokens, never real money.
Key takeaways
- No plugin needed. A constant, a session function and a shortcode are the whole integration, in code you own.
- Use
'wp-' . get_current_user_id()as the player id and a nickname as the display name. Never emails or real names. - Launch URLs are single use and expire after 60 seconds, so the sportsbook page must never be served from a page cache.
- Logged-out visitors get a sign-in prompt, not the sportsbook.
- A starting balance and a daily refill take one API call each, made safe against double grants by an idempotency key.
On this page9 sections
- Why there's no plugin step: the WordPress user is the player
- Store the API key safely
- A function that opens the session for the logged-in user
- A shortcode that prints the two lines
- Logged-out visitors: a sign-in prompt, not the sportsbook
- Giving tokens: a starting balance on first visit
- Caching plugins and launch URLs
- A sportsbook in your WordPress theme: mobile and width
- Frequently asked questions
Why there's no plugin step: the WordPress user is the player
When we checked the WordPress.org "betting" tag in September 2026, most listings were odds displays, odds converters and payback calculators: tools for people reading about bets, with no accounts or balances. That's a different product. What a community or fan site usually wants is a sportsbook its members can play on, with their own balances and bet history.
That needs identity, and WordPress already has it. Each logged-in user has a numeric id that never changes, which is exactly what the sportsbook needs as its user_ref. So the integration is thin: your server tells The Sportsbook who is playing, and the page shows the result. A plugin would only add a settings screen around three small pieces of code, and those pieces are easier to review when they live in your own repository.
The flow on every page view:
- A logged-in member opens your sportsbook page.
- WordPress calls
POST /b2b/v1/sessionswith your API key and the member's id. - The API returns a single-use
launch_url. - The shortcode prints that URL into two lines of HTML, and the SDK mounts the iframe.
If you haven't created a sportsbook yet, start on the Free plan (opens in a new tab). It takes a few minutes, nobody has to approve it, and the last screen shows your API key once.
Store the API key safely
The key looks like sbk_<prefix>_<secret> and can open a session for any player id you send, so treat it like a database password. Put it in wp-config.php, above the "That's all, stop editing" line:
// Your code: wp-config.phpdefine( 'SPORTSBOOK_API_KEY', getenv( 'SPORTSBOOK_API_KEY' ) ?: 'sbk_xxxx_xxxxxxxxxxxx' );If your host supports environment variables, set it there and leave the fallback out. Either way, keep it out of places editors and browsers can see: not in a page, a post, a custom field, theme options, or any JavaScript. And don't commit wp-config.php with the key in it. If the key ever leaks, create a new one in the console under Integration → API keys and revoke the old one; a revoked key stops working on the next request.
A function that opens the session for the logged-in user
Put the following in a must-use plugin (wp-content/mu-plugins/sportsbook.php) or your child theme's functions.php. It calls the documented sessions endpoint with three fields: user_ref, display_name and locale.
<?php// Your code: opens a sportsbook session for the logged-in WordPress user.function mysite_sportsbook_launch_url() { if ( ! is_user_logged_in() ) { return null; } $user = wp_get_current_user(); $body = array( 'user_ref' => 'wp-' . get_current_user_id(), 'locale' => 0 === strpos( get_user_locale(), 'es' ) ? 'es' : 'en', ); // AUP 9.1: a nickname only, never an email or a legal name. if ( $user->nickname && ! is_email( $user->nickname ) ) { $body['display_name'] = $user->nickname; } $response = wp_remote_post( 'https://api.thesportsbook.app/b2b/v1/sessions', array( 'headers' => array( 'Authorization' => 'Bearer ' . SPORTSBOOK_API_KEY, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( $body ), 'timeout' => 10, ) ); $code = wp_remote_retrieve_response_code( $response ); if ( is_wp_error( $response ) || $code < 200 || $code >= 300 ) { return null; // 403 USER_BLOCKED lands here too. } $data = json_decode( wp_remote_retrieve_body( $response ), true ); return isset( $data['launch_url'] ) ? $data['launch_url'] : null;}A few choices in there are deliberate.
The player id is wp- plus the user id. It's stable (WordPress never reuses or changes a user id), it's pseudonymous, and it comes from the server-side login, never from anything the browser sends. The prefix keeps your options open if you later connect a second system.
The display name is the nickname, and only if it isn't an email. Our Play-for-Fun Policy (AUP 9.1) says players are identified only with pseudonymous ids, and display names may only hold a nickname the player chose. WordPress's own "display name" field is often set to a first and last name, and on many sites the nickname defaults to the login, which may be an email address. The is_email() check skips those. display_name is optional, so sending nothing is fine.
Failures return null. A blocked member gets 403 USER_BLOCKED, and a network error is possible; in both cases the shortcode below shows a message instead of a broken iframe. The full field rules are in the docs section on opening a session (opens in a new tab).
A shortcode that prints the two lines
// Your code: [sportsbook] prints the embed for logged-in members.add_shortcode( 'sportsbook', function () { if ( ! is_user_logged_in() ) { return '<p>Sign in to play. <a href="' . esc_url( wp_login_url( get_permalink() ) ) . '">Sign in</a></p>'; } $launch_url = mysite_sportsbook_launch_url(); if ( ! $launch_url ) { return '<p>The sportsbook isn\'t available for your account right now.</p>'; } return '<div data-thesportsbook data-launch-url="' . esc_attr( $launch_url ) . '"></div>' . '<script src="https://play.thesportsbook.app/sdk.js"></script>';} );The last two lines are the standard embed:
<div data-thesportsbook data-launch-url="LAUNCH_URL_FROM_YOUR_SERVER"></div><script src="https://play.thesportsbook.app/sdk.js"></script>Create a page, add a Shortcode block with [sportsbook], and publish. The SDK mounts the iframe on page load with automatic height, so it grows with its content. Use the shortcode once per page.
A session lasts 12 hours. If a member leaves the tab open longer, the sportsbook asks for a new session, and reloading the page does it. If you want that to happen without a reload, expose the same function through a REST route and call updateToken() from the SDK's onSessionExpired callback; the sportsbook iframe integration guide shows the pattern.
Logged-out visitors: a sign-in prompt, not the sportsbook
The shortcode above never opens a session for an anonymous visitor, and that's on purpose.
- Players need a stable id. Without a login there's nothing to hang a balance or a bet history on.
- Sessions count toward your plan. A monthly active player is anyone who opens a session in a calendar month, counted once. The Free plan allows 250; a guest session on every visit would use that up with people who never bet.
- Your disclosures come first. The Play-for-Fun Policy (AUP 8.1) asks you to tell players, before they first play, that the sportsbook is for entertainment only, that tokens have no monetary value and can't be bought, sold, transferred or exchanged for anything of value, that no real money can be staked or won, that players must be 18 or older, and that you run the sportsbook and its tokens. The sign-in prompt, or your registration page, is a natural place for that text. The player notice template gives you model wording.
If you want visitors to see what they're signing up for, link to the public sportsbook demo (opens in a new tab) rather than opening a session for them.
Giving tokens: a starting balance on first visit
New players start at zero. In the managed mode, included on every plan, we keep each player's token balance as a record and you add tokens with POST /b2b/v1/users/{user_ref}/credit. The call creates the player if needed, so you can grant the starting balance before the first session:
// Your code: a one-time starting balance, safe to call on every visit.function mysite_sportsbook_starting_balance( $user_id ) { if ( get_user_meta( $user_id, 'sportsbook_started', true ) ) { return; } $response = wp_remote_post( 'https://api.thesportsbook.app/b2b/v1/users/wp-' . $user_id . '/credit', array( 'headers' => array( 'Authorization' => 'Bearer ' . SPORTSBOOK_API_KEY, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( array( 'amount' => 1000, 'idempotency_key' => 'start-wp-' . $user_id, 'reason' => 'Starting balance', ) ), 'timeout' => 10, ) ); $code = wp_remote_retrieve_response_code( $response ); if ( ! is_wp_error( $response ) && $code >= 200 && $code < 300 ) { update_user_meta( $user_id, 'sportsbook_started', 1 ); }}Call it from the shortcode before opening the session. The user meta flag saves an API call on later visits, but the idempotency_key is what makes it safe: the same key returns the original result with "replayed": true and never moves tokens twice, even if two tabs race or the meta write fails.
A daily refill is the same call with a key such as refill-wp-42-2026-09-28 (built with gmdate( 'Y-m-d' )), triggered when the member presses Get more tokens inside the sportsbook. That button fires a tokens-requested event on the page (with the two-line embed, a thesportsbook:tokens-requested DOM event on the container). Send the member to a refill page that calls your server; never grant tokens from the browser event alone, and never show a price there. The free token economy guide covers sizing, seasons and what counts as a free source.
Caching plugins and launch URLs
This is the mistake that breaks most WordPress embeds. A launch token is single use and expires after 60 seconds. If a page cache stores the rendered sportsbook page, the next member gets someone else's used token, or an expired one, and the sportsbook asks for a new session.
Many page caches skip logged-in users by default, which covers most visits here, but check yours rather than assuming. Then make the page uncacheable explicitly:
// Your code: never cache pages that contain [sportsbook].add_action( 'template_redirect', function () { $post = get_post(); if ( $post && has_shortcode( $post->post_content, 'sportsbook' ) ) { if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); } nocache_headers(); }} );DONOTCACHEPAGE is a convention many caching plugins honor, and nocache_headers() tells browsers and proxies not to store the response. Also add the page's URL to the exclusion list of any CDN or edge cache in front of the site. If you use a plugin that combines, defers or delays JavaScript, exclude play.thesportsbook.app/sdk.js from it too.
A sportsbook in your WordPress theme: mobile and width
The sportsbook chooses its layout from the width of its iframe, not the width of the window. On a narrow container the sidebar becomes a drawer and the betslip a bottom sheet, which is right for phones and wrong for a desktop page with a narrow content column.
- Give it the full width. Use a full-width page template, or set the block's alignment to full or wide if your theme supports it.
- Keep the height automatic. Don't wrap the shortcode in a container with a fixed height and
overflow: hidden. With automatic height your page scrolls, and the SDK keeps the betslip in the visible area. - Check sticky headers. A tall sticky header eats phone screen space; test a full bet on a real phone.
- List your site as an allowed origin. In the console under Settings → Allowed origins, add the exact origins you serve from, with and without
wwwif both resolve, plus staging.
Then brand it to match your theme in the console Studio: presets, colors, logo, a sidebar or top bar, list or grid cards, a floating or docked betslip. The general guide to embedding a sportsbook on any website covers branding and testing in more detail, and if you run a club or news site, see how a play-for-fun sportsbook works for sports media and fan sites.
Frequently asked questions
Is there a WordPress plugin for The Sportsbook?
No, and you don't need one. The integration is a constant in wp-config.php, one PHP function that opens a session for the logged-in member, and a shortcode that prints two lines of HTML. You can put that code in a must-use plugin or your child theme's functions.php and keep full control of it.
Can I use it on WordPress.com?
You need to run your own PHP, because the API key has to stay on the server. That works on any self-hosted WordPress and on hosted plans that let you add custom code or plugins. If your plan only allows HTML blocks, you'll need that PHP step to live somewhere else.
Does it work with the block editor and page builders?
Yes. The shortcode works in the block editor's Shortcode block, in the classic editor, and in page builders that have a shortcode element. Give that block the full content width, because the sportsbook picks its layout from the width it gets.
Do logged-out visitors count as players?
Only if you open sessions for them, and you shouldn't. A monthly active player is anyone who opens a session in a calendar month, and the Free plan allows 250. Show logged-out visitors a sign-in prompt instead, so every session belongs to a real member with a stable id.
Written by
The Sportsbook team
The people who build The Sportsbook. We have spent seven years building sportsbook software, and we write these guides from that work.
Last updated .