SpotStud.io Partner API — Developer Guide

Create broadcast-ready radio spots programmatically: place an order, pick from AI-written scripts, produce, review the preview, accept, and download the finished MP3.

  • Base URL: https://spotstud.io/api/v1
  • Version: v1
  • Auth: API key as Authorization: Bearer <key>
  • Format: JSON (UTF-8); audio files as audio/mpeg

1. Quickstart

  1. Register in the Partner Portal (starts as PayAsYouGo).
  2. In the portal, under API key, generate a key — it is shown only once (format sk_live_…).
  3. On PayAsYouGo: top up your balance in the portal (production is only possible once the balance covers a spot).
  4. Test your first call:
curl -H "Authorization: Bearer sk_live_YOUR_KEY" \
     https://spotstud.io/api/v1/me

Response:

{
  "partner": "Your Company Ltd",
  "quota": {
    "hasContract": true,
    "billingModel": "PAYG",
    "spotsUsed": 3,
    "spotsLimit": null,
    "spotsRemaining": null,
    "attemptsPerSpot": 5,
    "overageAllowed": false,
    "canStartSpot": true,
    "spotNetCents": 1900,
    "spotGrossCents": 2261,
    "vatMode": "STANDARD",
    "balanceCents": 8500
  }
}

2. Authentication

Every request needs the API key in the header:

Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • Keys are stored hashed only on the server — if lost, rotate it in the portal (the old key becomes invalid immediately).
  • Without a key, or with an invalid one, the API responds with 401 Unauthorized.
  • Treat the key like a password: use it server-side only, never ship it to a browser/client.

3. The spot lifecycle

An order moves through fixed states. You trigger the transitions with POST requests and track progress by polling (GET /orders/{id}).

POST /orders
   │  status: NEW
   ▼  (AI writes script variants)
status: SCRIPTS_READY ──►  you pick a script
   │
POST /orders/{id}/produce
   │  status: PRODUCING
   ▼  (audio is produced)
status: PREVIEW_READY ──►  review the preview (watermarked)
   │
POST /orders/{id}/accept   ◄── billing happens here
   │  status: COMPLETED
   ▼
GET /orders/{id}/result.mp3   (final MP3, no watermark)

Status values: NEW, SCRIPTS_READY, PRODUCING, PREVIEW_READY, COMPLETED, ERROR.

Billing only happens at accept. Scripts, production and the preview are free; a spot is only charged (or the balance debited) when you accept the preview.

Webhooks: the webhookUrl field on order creation is stored, but outbound webhook delivery is currently not active. Please use polling (see section 7).


4. Attempts, quotas & billing

  • Attempts per spot (attemptsPerSpot, usually 5): how often you may call produce for one order (e.g. choose another script, tweak the text) before accepting. Once attempts are used up, create a new order.
  • PayAsYouGo (PAYG): no monthly limit. Each accept costs €19 net (plus VAT depending on country). The gross amount is debited from your balance; if it isn't enough → 402 insufficient_funds. Top up the balance via PayPal in the portal.
  • Monthly plans (PRO/BUSINESS): a fixed number of spots per month. Accepted spots count against the monthly limit; once exhausted → 402 quota_exceeded. Manage plans in the portal.
  • Invoices: every accept or subscription payment produces an invoice with a sequential number and correct VAT treatment (domestic 19% / EU reverse charge / non-EU). Download as PDF in the portal.

GET /me returns the current state at any time (canStartSpot, balanceCents, spotsRemaining, …).


5. Endpoint reference

GET /me — account & quota

Partner name + quota/balance status. Amounts in cents. On monthly plans the PAYG fields (spotNetCents, spotGrossCents, vatMode, balanceCents) are null, and vice versa.

GET /languages — allowed spot languages

Returns the languages a spot can be produced in (all ElevenLabs-supported languages). Use a name or code value for the language field when creating an order.

[
  { "name": "German",  "code": "de" },
  { "name": "English", "code": "en" },
  { "name": "Spanish", "code": "es" }
]

GET /orders — list your orders

Returns the partner's own orders, newest first. Optional query params: clientRef (filter to your own reference, e.g. one of your customers) and limit (1–200, default 50). Compact form (no scripts): orderId, status, clientRef, companyName, language, lengthSeconds, created, previewUrl, resultUrl.

curl -H "Authorization: Bearer sk_live_YOUR_KEY" \
     "https://spotstud.io/api/v1/orders?clientRef=customer-123&limit=20"

POST /orders — create an order

Body:

Field Type Required Default Description
companyName string Advertised company / brand
productInfo string What is advertised (product/offer)
targetAudience string Target audience (free text)
tone string Free text. Suggested: friendly & warm, funny & cheeky, serious & trustworthy, emotional & moving, energetic & loud — or your own.
callToAction string Call to action (free text)
language string German Any ElevenLabs-supported language, by name or ISO code (case-insensitive), e.g. German, English, Spanish, es. Full list: GET /api/v1/languages. Unknown value → 400 invalid_request.
lengthSeconds int 30 Allowed: 20, 30, 45, 60. Any other value → 400 invalid_request. (Price is flat per spot regardless of length.)
wantsSfx bool true Include sound effects/music
voicePreference string Free text. E.g. one female voice, two voices (dialogue), deep male narrator.
extraWishes string Free text: a slogan that must appear, no-gos, style references …
clientRef string Your own reference (echoed in responses)
webhookUrl string Reserved (no active delivery, see above)

Response 202 Accepted — order with status: NEW. Then poll until SCRIPTS_READY.

`GET /orders/

Returns the current state. At SCRIPTS_READY, scripts contains the variants:

{
  "orderId": "4f3c…",
  "status": "SCRIPTS_READY",
  "clientRef": "campaign-42",
  "scripts": [
    { "id": "a1…", "variant": 1, "title": "…", "concept": "…", "script": "Voice-over text …" }
  ],
  "previewUrl": null,
  "resultUrl": null,
  "error": null
}

At PREVIEW_READY/COMPLETED previewUrl is set; at COMPLETED also resultUrl. On ERROR, the cause is in error.

`POST /orders/

Body:

Field Type Required Description
scriptId GUID ID of a variant from scripts
editedText string Optionally adjusted voice-over text

Response 202 with status: PRODUCING. Then poll until PREVIEW_READY. Allowed only in state SCRIPTS_READY (or ERROR to retry), as long as attempts remain.

`POST /orders/

Charges the spot: on PAYG the balance is debited by the gross price and an invoice is created; on monthly plans one spot counts against the quota. Idempotent — an already accepted order simply returns the result. Response 200 with status: COMPLETED and resultUrl.

`GET /orders/

Watermarked MP3. Available from PREVIEW_READY. For review before accepting.

`GET /orders/

MP3 without watermark. Available only after accept (status COMPLETED).


6. Error format

All errors are returned as JSON:

{ "error": { "code": "insufficient_funds", "message": "Balance does not cover a spot. Please top up." } }
HTTP code Meaning
400 invalid_request Missing required field or invalid script
401 Key missing/invalid
402 insufficient_funds PAYG balance does not cover a spot
402 quota_exceeded Monthly quota exhausted
402 no_contract No active contract
404 Order not found (or not owned by this key)
409 invalid_state Action not allowed in the current status
409 attempts_exhausted Attempts for this spot used up → create a new order
409 spot_charged A spot was already charged for this order

7. Best practices

  • Polling: query GET /orders/{id} every 2–5 seconds; production usually takes some tens of seconds depending on length. Use a timeout (e.g. 5 minutes).
  • accept is the payment point — only call it after reviewing the preview. Idempotency protects against accidental double billing.
  • Use clientRef to map orders to your own records.
  • Check balance/quota before creating an order via GET /me (canStartSpot).
  • Retries: on 5xx/network errors, retry with backoff; GET is safe to repeat.

8. Full examples (end-to-end)

The flow is the same everywhere: create order → wait for SCRIPTS_READY → pick a script & produce → wait for PREVIEW_READYaccept → download result.mp3.

Python

import time, requests

BASE = "https://spotstud.io/api/v1"
KEY  = "sk_live_YOUR_KEY"
H    = {"Authorization": f"Bearer {KEY}"}

def wait_for(order_id, target, timeout=300):
    deadline = time.time() + timeout
    while time.time() < deadline:
        r = requests.get(f"{BASE}/orders/{order_id}", headers=H)
        r.raise_for_status()
        data = r.json()
        if data["status"] == target:
            return data
        if data["status"] == "ERROR":
            raise RuntimeError(data.get("error"))
        time.sleep(3)
    raise TimeoutError(f"Status {target} not reached")

# 1) Create the order
order = requests.post(f"{BASE}/orders", headers=H, json={
    "companyName": "Sample Ltd",
    "productInfo": "Spring sale: 20% off all bikes",
    "targetAudience": "commuters 25–45",
    "tone": "fresh, motivating",
    "callToAction": "Come by the shop today",
    "language": "English",
    "lengthSeconds": 30,
    "clientRef": "campaign-42",
}).json()
oid = order["orderId"]

# 2) Wait for scripts and pick the first
ready  = wait_for(oid, "SCRIPTS_READY")
script = ready["scripts"][0]

# 3) Produce
requests.post(f"{BASE}/orders/{oid}/produce", headers=H,
              json={"scriptId": script["id"]}).raise_for_status()

# 4) Wait for the preview, review it (optionally download preview.mp3)
wait_for(oid, "PREVIEW_READY")
with open("preview.mp3", "wb") as f:
    f.write(requests.get(f"{BASE}/orders/{oid}/preview.mp3", headers=H).content)

# 5) Accept (chargeable) and download the final MP3
done = requests.post(f"{BASE}/orders/{oid}/accept", headers=H).json()
with open("spot.mp3", "wb") as f:
    f.write(requests.get(f"{BASE}/orders/{oid}/result.mp3", headers=H).content)
print("Done:", done["status"])

Java

Requires Java 11+ (java.net.http) and Jackson (com.fasterxml.jackson.core:jackson-databind).

import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import com.fasterxml.jackson.databind.*;

public class SpotStudioClient {
    static final String BASE = "https://spotstud.io/api/v1";
    static final String KEY  = "sk_live_YOUR_KEY";
    static final HttpClient HTTP = HttpClient.newHttpClient();
    static final ObjectMapper JSON = new ObjectMapper();

    static HttpRequest.Builder req(String path) {
        return HttpRequest.newBuilder(URI.create(BASE + path))
                .header("Authorization", "Bearer " + KEY)
                .header("Content-Type", "application/json");
    }

    static JsonNode send(HttpRequest r) throws Exception {
        HttpResponse<String> res = HTTP.send(r, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() >= 400) throw new RuntimeException("HTTP " + res.statusCode() + ": " + res.body());
        return JSON.readTree(res.body());
    }

    static JsonNode waitFor(String id, String target) throws Exception {
        long deadline = System.currentTimeMillis() + 300_000;
        while (System.currentTimeMillis() < deadline) {
            JsonNode d = send(req("/orders/" + id).GET().build());
            String s = d.get("status").asText();
            if (s.equals(target)) return d;
            if (s.equals("ERROR")) throw new RuntimeException("Order error: " + d.get("error"));
            Thread.sleep(3000);
        }
        throw new RuntimeException("Status " + target + " not reached");
    }

    public static void main(String[] args) throws Exception {
        String body = """
            {"companyName":"Sample Ltd",
             "productInfo":"Spring sale: 20% off all bikes",
             "language":"English","lengthSeconds":30,"clientRef":"campaign-42"}""";

        JsonNode order = send(req("/orders").POST(HttpRequest.BodyPublishers.ofString(body)).build());
        String oid = order.get("orderId").asText();

        JsonNode ready = waitFor(oid, "SCRIPTS_READY");
        String scriptId = ready.get("scripts").get(0).get("id").asText();

        send(req("/orders/" + oid + "/produce")
                .POST(HttpRequest.BodyPublishers.ofString("{\"scriptId\":\"" + scriptId + "\"}")).build());

        waitFor(oid, "PREVIEW_READY");

        send(req("/orders/" + oid + "/accept").POST(HttpRequest.BodyPublishers.noBody()).build());

        byte[] mp3 = HTTP.send(req("/orders/" + oid + "/result.mp3").GET().build(),
                HttpResponse.BodyHandlers.ofByteArray()).body();
        Files.write(Path.of("spot.mp3"), mp3);
        System.out.println("Done, " + mp3.length + " bytes");
    }
}

C#

.NET 6+ (HttpClient + System.Text.Json).

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

const string Base = "https://spotstud.io/api/v1";
const string Key  = "sk_live_YOUR_KEY";

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Key);

async Task<JsonElement> Get(string path) =>
    (await http.GetFromJsonAsync<JsonElement>($"{Base}{path}"));

async Task<JsonElement> WaitFor(string id, string target, int timeoutSec = 300)
{
    var deadline = DateTime.UtcNow.AddSeconds(timeoutSec);
    while (DateTime.UtcNow < deadline)
    {
        var d = await Get($"/orders/{id}");
        var status = d.GetProperty("status").GetString();
        if (status == target) return d;
        if (status == "ERROR") throw new Exception("Order error: " + d.GetProperty("error"));
        await Task.Delay(3000);
    }
    throw new TimeoutException($"Status {target} not reached");
}

// 1) Create
var create = await http.PostAsJsonAsync($"{Base}/orders", new
{
    companyName    = "Sample Ltd",
    productInfo    = "Spring sale: 20% off all bikes",
    targetAudience = "commuters 25–45",
    language       = "English",
    lengthSeconds  = 30,
    clientRef      = "campaign-42"
});
create.EnsureSuccessStatusCode();
var order = await create.Content.ReadFromJsonAsync<JsonElement>();
var oid   = order.GetProperty("orderId").GetString()!;

// 2) Wait for scripts, pick the first
var ready    = await WaitFor(oid, "SCRIPTS_READY");
var scriptId = ready.GetProperty("scripts")[0].GetProperty("id").GetString();

// 3) Produce
(await http.PostAsJsonAsync($"{Base}/orders/{oid}/produce", new { scriptId }))
    .EnsureSuccessStatusCode();

// 4) Wait for the preview
await WaitFor(oid, "PREVIEW_READY");

// 5) Accept + download the final MP3
(await http.PostAsync($"{Base}/orders/{oid}/accept", null)).EnsureSuccessStatusCode();
var mp3 = await http.GetByteArrayAsync($"{Base}/orders/{oid}/result.mp3");
await File.WriteAllBytesAsync("spot.mp3", mp3);
Console.WriteLine($"Done, {mp3.Length} bytes");

9. Support

Questions about integration, quotas or invoices: info@digitalanna.de. Manage keys, top up your balance and download invoices in the Partner Portal.

Cette page est générée depuis api-guide.md · brut : api-guide.md