REST API
The BeatStore player is one client of beatstore/v1. Every public catalog the player can list, filter, and play is available as JSON on your WordPress domain—not on BeatStore’s servers, and not behind an API key we issue.
That is the product: software on hosting you control. The storefront UI is optional.
Base URL
https://yoursite.com/wp-json/beatstore/v1
Pretty permalinks (Settings → Permalinks → Post name) are recommended. Without them, WordPress still serves REST at ?rest_route=/beatstore/v1/beats.
What this is (and is not)
Use it when you want a custom storefront, a native app, a Discord bot, or a landing page that reads the same catalog the player does.
Do not expect platform-style API keys, OAuth apps, signed webhooks, or a BeatStore-hosted gateway. Writes that change a cart or increment plays use a WordPress wp_rest nonce from that same origin. Checkout and file delivery stay Easy Digital Downloads or WooCommerce.
| You want | Use |
|---|---|
| The BeatStore player as-is | [beatstore] or the BeatStore Player block |
| The same player on another site | iframe embed at /beatstore-embed/ |
| Custom chrome, same catalog | GET /beats (this page) |
| Custom checkout | Do not. Send buyers to EDD/Woo checkout |
Endpoints
| Method | Path | Auth |
|---|---|---|
GET | /beats | Public |
GET | /analytics | Logged-in user with edit_posts |
GET | /cart | Public |
POST | /cart | X-WP-Nonce or _wpnonce (wp_rest) |
POST | /play | Same nonce |
POST | /subscribe | Same nonce |
GET /beats
List catalog tracks. Default page size is 24; per_page max is 40.
| Query | Type | Notes |
|---|---|---|
page | integer | Default 1 |
per_page | integer | Default 24, max 40 |
search | string | Title search |
kind | string | Term slug (beat, kit, …) |
genre | string | Term slug |
mood | string | Term slug |
bpm_min | integer | Inclusive; 0 means unset |
bpm_max | integer | Inclusive; 0 means unset |
playlist | integer | Playlist post ID (beatstore_playlist) |
curl -sS "https://yoursite.com/wp-json/beatstore/v1/beats?per_page=2&genre=hip-hop"
{
"tracks": [
{
"id": 412,
"title": "Midnight Run",
"permalink": "https://yoursite.com/downloads/midnight-run/",
"artworkUrl": "https://yoursite.com/wp-content/uploads/midnight.jpg",
"previewUrl": "https://yoursite.com/wp-content/uploads/midnight-preview.mp3",
"sold": false,
"featured": true,
"plays": 1804,
"bpm": 140,
"key": "Am",
"duration": 187,
"kind": "beat",
"kindName": "Beat",
"genres": ["hip-hop"],
"genreNames": ["Hip-Hop"],
"moods": ["dark"],
"sections": [
{ "label": "Intro", "time": 0 },
{ "label": "Hook", "time": 32 }
],
"offers": [
{
"id": "1",
"name": "MP3 Lease",
"amount": "29.99",
"exclusive": false,
"commerceRef": "edd:412:1",
"includes": [
{ "kind": "delivery", "label": "MP3" },
{ "kind": "right", "label": "2,000 copies" },
{ "kind": "right", "label": "500k streams" }
]
}
]
}
],
"page": 1,
"perPage": 2,
"total": 86,
"hasMore": true
}
kind, genre, and mood query values are slugs. Display names are on the track as kindName / genreNames.
Track object
| Field | Type | Notes |
|---|---|---|
id | integer | EDD download ID or Woo product ID |
title | string | |
permalink | string | Product URL on your site |
artworkUrl | string | Featured image, or the default artwork setting; may be empty |
previewUrl | string | Streamable preview URL; may be empty |
sold | boolean | Exclusive already sold |
featured | boolean | true when the beat is pinned to the top of matching catalog/playlist results |
plays | integer | |
bpm | integer | 0 if unset |
key | string | Musical key; may be empty |
duration | integer | Preview length in seconds |
kind | string | Kind term slug |
kindName | string | Kind label |
genres | string[] | Genre slugs |
genreNames | string[] | Genre labels |
moods | string[] | Mood slugs |
sections | object[] | { label, time } — time is seconds from start |
offers | object[] | License tiers (see below) |
freeDownload | boolean | true when Mailchimp is connected and this beat offers a tagged preview MP3 for email |
Offer object
| Field | Type | Notes |
|---|---|---|
id | string | EDD price ID or Woo variation ID |
name | string | License name |
amount | string | Formatted price from the commerce plugin |
exclusive | boolean | Exclusive purchase marks the beat sold |
commerceRef | string | Stable {adapter}:{beatId}:{offerId} even if the license is renamed |
includes | object[] | Optional. { kind, label, value? }. kind is delivery (MP3 / WAV / ZIP from the files on that price or variation) or right (usage cap from a mapped Contracts template). Omitted when empty. Labels only. Never file URLs or names. |
Buyer download URLs are not in this payload. Delivery stays on the EDD/Woo order. includes may list file types so the license picker can show what each offer ships.
GET /analytics
Privileged plays leaderboard for BeatStore → Analytics. Requires a logged-in user with edit_posts. Unauthenticated requests return 401. This is not the public catalog: it includes hidden tracks, sorts by plays, and caps at 200 rows.
curl -sS -u admin:password "https://yoursite.com/wp-json/beatstore/v1/analytics?days=30"
days is 7, 30, 90, or 0 (all time, default). All time ranks by lifetime _beatstore_plays. The other values rank by plays in that window.
{
"tracks": [
{
"id": 412,
"title": "Midnight Run",
"artworkUrl": "",
"plays": 1804,
"featured": true,
"hidden": false,
"kind": "beat",
"editUrl": "https://yoursite.com/wp-admin/post.php?post=412&action=edit",
"permalink": "https://yoursite.com/downloads/midnight-run/"
}
],
"totals": { "plays": 12000, "tracks": 86, "listeners": 410 },
"days": 0,
"daily": [
{ "date": "2026-08-11", "plays": 40 },
{ "date": "2026-09-09", "plays": 18 }
],
"reportsUrl": "https://yoursite.com/wp-admin/edit.php?post_type=download&page=edd-reports",
"settingsUrl": "https://yoursite.com/wp-admin/admin.php?page=beatstore-settings",
"analyticsPlays": true
}
reportsUrl is EDD reports, WooCommerce Analytics, or empty when no commerce plugin is active. analyticsPlays is the Settings toggle for gtag / GTM play events. totals.listeners is distinct visitors who started a play after this update (hashed IP and browser). For days 7/30/90 that is visitors active in the window. daily is the matching site-timezone days (zeros included). All time still sends the last 90 daily rows. Daily plays, range leaderboard plays, and unique listeners start counting when a track plays after this update. They are not a backfill of lifetime _beatstore_plays. totals.tracks is still the published catalog size (used when the list is capped).
Do not use this endpoint for a public storefront. Keep using GET /beats.
Cart
GET /cart returns the current commerce cart (cookies / session on that origin).
{
"count": 1,
"total": "29.99",
"checkoutUrl": "https://yoursite.com/checkout/",
"items": [
{ "key": "abc", "id": 412, "priceId": "1" }
]
}
POST /cart adds a license. JSON body:
{ "beatId": 412, "offerId": "1" }
curl -sS -X POST "https://yoursite.com/wp-json/beatstore/v1/cart" \
-H "Content-Type: application/json" \
-H "X-WP-Nonce: YOUR_WP_REST_NONCE" \
-d '{"beatId":412,"offerId":"1"}'
The nonce is a WordPress wp_rest nonce. The player gets one when it renders on your site (wp_create_nonce( 'wp_rest' )). Logged-out visitors can receive a nonce on a page of your WordPress install; this is not a secret API key, and it is not valid as a cross-origin app credential.
On success the response is the same shape as GET /cart. Errors:
| Status | Code | When |
|---|---|---|
| 404 | beatstore_not_found | Unknown beatId |
| 409 | beatstore_sold | Exclusive already sold |
| 400 | beatstore_offer | Unknown offerId |
| 403 | — | Missing or invalid nonce |
| 500 | beatstore_cart | Commerce adapter failed |
After a successful add, send the buyer to checkoutUrl. BeatStore does not take payment over REST.
POST /play
Increments the play count for a beat. Same nonce as cart.
{ "beatId": 412 }
{ "plays": 1805 }
404 if the beat does not exist.
POST /subscribe
Mailchimp free-download gate. Same nonce as cart. Adds the listener to your audience and emails a signed link to the Media Library preview MP3 (not paid license files).
{ "beatId": 412, "email": "listener@example.com", "name": "Alex" }
id is accepted as an alias for beatId.
{ "ok": true, "message": "Check your email for the download link." }
| Status | Code | When |
|---|---|---|
| 400 | beatstore_email / beatstore_name / beatstore_free | Invalid email, missing name, or this beat does not offer free download |
| 403 | — | Missing or invalid nonce |
| 429 | beatstore_rate | Too many requests from this IP |
| 500 | beatstore_mail | WordPress could not send the email |
| 503 | beatstore_mailchimp | Mailchimp is not connected |
Setup: Integrations.
PHP hooks
For themes and add-ons on the same WordPress install. These are not a second HTTP API. Each hook’s parameters, return value, and an example live on PHP Hooks.
| Hook | Use |
|---|---|
beatstore_beat_offers | Filter license offers before they hit REST / the player |
beatstore_resolve_preview_url | Resolve or override a preview URL |
beatstore_preview_sources | Register preview source modules |
beatstore_player_footer | Markup after the player |
beatstore_order_line_fulfilled | After a line is delivered (contracts, extra mail) |
beatstore_sold_state_changed | Exclusive marked sold |
beatstore_checkout_before_submit / beatstore_checkout_required_fields | Checkout extras |
beatstore_offer_cart_actions | Extra cart actions per offer |
beatstore_register_modules / beatstore_commerce_adapters | Advanced wiring |
Player vs REST
The player, the off-site iframe, and GET /beats share one catalog. Use the shortcode when you want the player with no extra code:
[beatstore]
Optional attributes (limit, playlist, kind, genre, mood) match the REST filters. Off-site: https://yoursite.com/beatstore-embed/. Iframe Buy is https://yoursite.com/beatstore-buy/?beat=123&offer=1 (top window, then checkout).
Install and first catalog: Getting Started.