How it works
| 1. Connect once | RUN reads your accounts and their royalty statements from your backend, read-only. Nothing to build on your side. |
| 2. We price ahead | Every account is priced before anyone asks, and marked eligible or not. |
| 3. You ask | When an artist opens your dashboard, your backend asks if their account is eligible, and for how much. |
| 4. You show the page | Your backend opens an offer page for that account and your page shows it in an iframe. |
| 5. The artist chooses | They read how it works, see their offers and pick one, without leaving your product. |
The page carries your product name, colours, language and currency. To the artist, the offer comes from you.
Quickstart
Three steps, once RUN has connected your data and given you a key.
Ask whether the account is eligible. Open the page for it. Embed the URL you get back.
account is your own id for the artist or label, the same one your backend already uses.
Base URL:
# 1. Is this account eligible?
curl {BASE}/accounts/label_48213/offer \
-H "Authorization: Bearer {KEY}"
# 2. Open the offer page for it
curl -X POST {BASE}/embed/sessions \
-H "Authorization: Bearer {KEY}" \
-H "Content-Type: application/json" \
-d '{"account": "label_48213", "locale": "es"}'
# 3. Put the "url" from step 2 in an iframe
Authentication
Send your secret key as a Bearer token on every /v1 request.
RUN issues your keys: sk_live_… for production and sk_test_… for testing. A key is shown once, when it is made.
Keep keys on your server. The browser never needs one: the iframe gets its own one-hour pass.
A missing, wrong or revoked key gets 401. Over your requests per minute you get 429 with a Retry-After header.
You're signed in, so the samples on this page use your key. They run as pasted.
Sign in with your key and the samples fill it in. No key yet? Ask RUN for one.
Authorization: Bearer {KEY}
Is this account eligible?
The newest open offer for one of your accounts. It answers at once, because the pricing was done ahead of time.
eligible | Whether to show this account the advance. |
ineligible_reason | When not eligible: too_small or too_little_history. |
options | One entry per offer: years, artist_share and the advance. |
currency | The currency the artist is shown. |
expires | When the offer stops being valid. |
selected_years | The option the artist chose, once they have. |
signed_at | When the artist signed the agreement. Empty until then. |
No open offer for the account: 404. Use the biggest advance for a teaser such as "Get up to €205,000".
{
"id": "ofr_4e6287d71f6146e2b1bc",
"object": "offer",
"account": "label_48213",
"account_name": "Luna Roja Records",
"eligible": true,
"ineligible_reason": null,
"currency": "EUR",
"options": [
{ "years": 1, "artist_share": 0.8, "advance": 47000 },
{ "years": 3, "artist_share": 0.75, "advance": 135000 },
{ "years": 5, "artist_share": 0.7, "advance": 205000 }
],
"created": "2026-11-12T02:00:00Z",
"expires": "2026-11-26T02:00:00Z",
"expired": false,
"selected_years": null,
"selected_at": null,
"source": "precalc"
}
Open the offer page
Call this from your backend when the artist clicks through. You get a URL for an iframe.
| Field | Description |
|---|---|
account | Your id for the artist or label. Opens their newest open offer. |
offer | Or an offer id, ofr_…. Send one of the two. |
locale | The page's language, one of those set up for you. Default: your first. |
signer | Who signs: name and email. With it, the artist signs the agreement in the page right after choosing. Without it, the page stops after choosing. |
The URL works for one hour and for that offer only. Make a new one each time the artist opens the page.
An account that is not eligible cannot be opened: 400 not_eligible.
{
"object": "embed_session",
"id": "ems_0c3b1e4f5a6d7b8c9d0e",
"offer": "ofr_4e6287d71f6146e2b1bc",
"url": "https://…/embed?s=ems_…#es_…",
"expires": "2026-11-12T15:04:05Z"
}
Embed it
Put the URL in an iframe on one of the sites registered for your account. The browser refuses to show it anywhere else.
The page tells your page what is happening with postMessage. Every message has source: "music-embed" and a type:
ready | The page has loaded. |
resize | With height in pixels. Set the iframe's height to it. |
selected | With years. The artist chose an offer. |
signed | With years. The artist signed the agreement. |
close | The artist pressed "Not now" or "Close". Hide the iframe. |
After the artist chooses, GET /v1/accounts/{account}/offer shows selected_years, and signed_at once they have signed. Trust that call, not the message: a message comes from the browser.
Try it with your key.
<iframe id="advance" src="URL"
style="width:100%;border:0"></iframe>
<script>
const frame = document.getElementById("advance");
addEventListener("message", (e) => {
if (e.source !== frame.contentWindow) return;
if (e.data?.source !== "music-embed") return;
if (e.data.type === "resize")
frame.style.height = e.data.height + "px";
if (e.data.type === "selected")
console.log("chose", e.data.years, "years");
if (e.data.type === "close") frame.hidden = true;
});
</script>
What the artist sees
| How it works | Six short points: priced from their royalties, repaid first, catalog locked for the term, unlocked at the end. |
| The offers | One card per offer, with the advance and what they keep. They pick one. |
| Their choice | A summary of the advance, the term and the split. |
Signing the agreement in the same page comes next. Today the page stops after the choice and says the agreement is being prepared.
Set per partner
The offers (years and split), who is eligible, the product name, colours, languages, currency and how long an offer stays open. RUN sets these up with you.
Errors
Normal HTTP status codes and one JSON shape.
| Status | Type | Codes |
|---|---|---|
| 400 | invalid_request_error | invalid_parameter, invalid_body, not_eligible, offer_expired, embed_not_configured |
| 401 | authentication_error | invalid_api_key |
| 404 | invalid_request_error | not_found |
| 429 | rate_limit_error | rate_limited |
| 500 | api_error | internal |
The full contract is in the API reference and as OpenAPI.
{
"error": {
"type": "invalid_request_error",
"code": "not_eligible",
"message": "That account is not eligible for an advance right now."
}
}
Versions
The version is in the path: /v1. Every reply carries Music-Version: v1.
We add things at any time: new routes, new optional request fields, new response fields, new codes, new page messages. So ignore fields, codes and message types you do not know.
We never do these inside a version: remove or rename a route or field, change a field's type or meaning, add a required request field, tighten what a field accepts, change how you sign in.
A change like that ships as /v2, next to /v1. We tell your technical contact, and /v1 keeps working for at least 12 months after that.
The page in the iframe is ours and improves without notice. Until your first live key, /v1 can still change; we tell you before it does.
HTTP/2 200
content-type: application/json
music-version: v1