API’et fra LavSange laver personlige sange programmatisk: send en anledning og nogle detaljer ind, få færdig sangtekst og en produceret indspilning ud. Ren HTTPS og JSON, intet obligatorisk SDK.
Opdateret: 2026-09-16
Nøgler udstedes manuelt. En kort mail med dit formål, forventet volumen og sprog er nok, og adgangen plejer at tage én hverdag.
API’et gør præcis det samme som hjemmesiden. Du sender en anledning, navnet på den person sangen er til, og en håndfuld konkrete detaljer. Ud af det kommer først en komplet sangtekst, derefter en produceret indspilning med sang, arrangement og mix. Hele forløbet tager normalt fem til ti minutter.
Alle kald går til https://api.lavsange.dk/v1. API’et taler kun HTTPS, modtager JSON og svarer med JSON. Der er intet SDK du skal bruge: ethvert sprog der kan HTTP, er nok. Eksemplerne på denne side bruger curl, Python og Node, fordi det er de tre hyppigste tilfælde.
Hvert domæne har sin egen API-base og sin egen pris i den lokale valuta. En nøgle gælder det domæne den er udstedt til. Betjener du flere markeder, får du flere nøgler eller én nøgle med flere åbne domæner.
Der afregnes pr. færdig sang, aktuelt 249 DKK. Udkast, afbrudte kald og nye generationer koster ingenting.
Der er bevidst ingen selvbetjent oprettelse. Vi udsteder nøgler manuelt, fordi hver sang koster rigtige produktionspenge, og vi gerne vil vide hvad integrationen skal bruges til. I praksis er det en kort mail og én hverdag.
Skriv til songs@maxkuch.com og nævn fire ting:
Du får så to nøgler: en testnøgle med præfikset sk_test_, der er gratis og leverer faste demoindspilninger, og en livenøgle med præfikset sk_live_. Begge virker med det samme, uden at enkelte endepunkter skal åbnes.
Hvert kald bærer nøglen i Authorization-headeren som bearer-token. Kald uden gyldig header får 401 og fejltypen authentication_error.
curl https://api.lavsange.dk/v1/songs \
-H "Authorization: Bearer $SONG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"occasion": "birthday",
"recipient_name": "Anna",
"relationship": "sister",
"language": "da",
"mood": "happy",
"style": "pop",
"voice": "female",
"details": "Climbs every weekend, always ten minutes late, calls everyone chef.",
"callback_url": "https://example.com/hooks/songs"
}'
Behandl nøglen som et kodeord: kun på serversiden, aldrig i frontend-kode, aldrig i et offentligt repository. Går en nøgle tabt, så skriv til os, vi spærrer den straks og udsteder en ny. En konto må have flere aktive nøgler, så udskiftning kan ske uden nedetid.
Test- og livenøgler deler de samme endepunkter. Om et kald kørte i testtilstand står i feltet livemode på hvert objekt.
At lave en sang er ét kald. Svaret kommer med det samme og indeholder et sang-id med status queued. Resten sker i baggrunden.
import os, time, requests
API = "https://api.lavsange.dk/v1"
HEAD = {"Authorization": "Bearer " + os.environ["SONG_API_KEY"]}
song = requests.post(API + "/songs", headers=HEAD, json={
"occasion": "wedding",
"recipient_name": "Lea and Tim",
"relationship": "friends",
"language": "da",
"mood": "romantic",
"details": "Met at a bike repair shop, dog named Miso, both terrible dancers.",
}).json()
while song["status"] not in ("preview_ready", "complete", "failed"):
time.sleep(5)
song = requests.get(API + "/songs/" + song["id"], headers=HEAD).json()
print(song["lyrics"])
print(song["preview_url"])
For enkelhedens skyld poller eksemplet hvert femte sekund. I drift er webhooks den bedre vej, fordi de sparer både den åbne forbindelse og ventetiden. Begge dele understøttes, og webhooks er beskrevet længere nede.
Det vigtigste felt er details. Der hører de konkrete ting om personen hjemme: kælenavnet, vanen, ferien der gik galt. Almindelige sætninger som "hun er et varmt menneske" giver almindelige linjer. Tre til fem konkrete detaljer er nok, og de er forskellen på pæn og virkelig personlig.
| Metode | Sti | Formål |
|---|---|---|
| POST | /v1/songs | Bestil en ny sang. |
| GET | /v1/songs/{id} | Hent én sang med alle aktuelle felter. |
| GET | /v1/songs | List kontoens sange, med filter og sidevisning. |
| GET | /v1/songs/{id}/lyrics | Hent kun sangteksten som ren tekst. |
| GET | /v1/songs/{id}/audio | Signeret download-URL til smagsprøve eller hele indspilningen. |
| POST | /v1/songs/{id}/regenerate | Sæt en gratis nygenerering i gang. |
| POST | /v1/songs/{id}/checkout | Opret en betalingsside til slutkunden. |
| POST | /v1/songs/{id}/unlock | Lås sangen op direkte og afregn over kontoen. |
| GET | /v1/options | Alle gyldige værdier for anledning, stemning, stil, stemme og sprog. |
| GET | /v1/account | Saldo, grænser og åbne domæner. |
| DELETE | /v1/songs/{id} | Afbryd en sang der endnu ikke er færdig. |
POST /v1/songs tager beskrivelsen imod og går i gang med det samme. Kun tre felter er påkrævet, resten har fornuftige standardværdier eller vælges så de passer til anledningen.
| Felt | Type | Beskrivelse |
|---|---|---|
| string | påkrævet | Anledningen. Gyldige værdier kommer fra /v1/options. |
| string | påkrævet | Navnet på den person sangen er til. Bruges i teksten. |
| string | påkrævet | Konkrete detaljer om personen, 40 til 4000 tegn. Feltet afgør kvaliteten af resultatet. |
| string | valgfri | Relationen mellem køberen og modtageren, for eksempel søster, kollega, kæreste. |
| string | valgfri | Sproget der synges på. Standard er da. |
| string | valgfri | Grundstemning. Uden angivelse vælger vi en der passer til anledningen. |
| string | valgfri | Musikstil. Uden angivelse vælger vi en der passer til anledning og stemning. |
| string | valgfri | Sangstemme. Uden angivelse vælger vi en der passer til anledningen. |
| string | valgfri | En hilsen der skal med i sangen. |
| string | valgfri | Fritekst til alt andet, for eksempel ønsker om tempo. |
| string | valgfri | HTTPS-adresse som hændelser sendes til. |
| object | valgfri | Frie nøgle-værdi-par, højst 20. Kommer uændret retur. |
const res = await fetch("https://api.lavsange.dk/v1/songs", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SONG_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
occasion: "anniversary",
recipient_name: "Mara",
relationship: "partner",
language: "da",
mood: "warm",
details: "Ten years, three apartments, one very loud coffee machine.",
callback_url: "https://example.com/hooks/songs",
metadata: { order_id: "A-10423" },
}),
});
const song = await res.json();
console.log(song.id, song.status);
Selve kaldet koster ingenting. Der afregnes først når sangen låses op via /unlock eller en betalt checkout-session.
Ethvert endepunkt der returnerer én sang, returnerer det samme objekt. Felter der endnu ikke ligger fast, er null og fyldes ud undervejs i produktionen.
{
"id": "sng_3n8Kd2ZpQv",
"object": "song",
"status": "queued",
"created_at": "2026-09-16T09:41:02Z",
"occasion": "birthday",
"recipient_name": "Anna",
"relationship": "sister",
"language": "da",
"mood": "happy",
"style": "pop",
"voice": "female",
"lyrics": null,
"preview_url": null,
"audio_url": null,
"duration_seconds": null,
"paid": false,
"price": { "amount": 2999, "currency": "DKK" },
"metadata": {},
"livemode": true
}
| Felt | Type | Beskrivelse |
|---|---|---|
| string | valgfri | Entydigt id, begynder altid med sng_. |
| string | valgfri | Aktuel produktionsstatus, se næste afsnit. |
| string | valgfri | Den fulde sangtekst med markeringer for vers og omkvæd. Gratis, også uden betaling. |
| string | valgfri | De første 45 sekunder som MP3. Altid tilgængelig, ingen betaling nødvendig. |
| string | valgfri | Hele indspilningen som MP3, signeret og gyldig i 24 timer. Sættes først efter betaling. |
| integer | valgfri | Længden af den færdige indspilning i sekunder, typisk mellem 120 og 240. |
| boolean | valgfri | Om sangen er låst op. |
| object | valgfri | Beløb i mindste valutaenhed plus valutakode, her 249 DKK. |
| object | valgfri | Det du sendte med ved oprettelsen, uændret. |
| boolean | valgfri | false, hvis kaldet kørte med en testnøgle. |
{
"id": "sng_3n8Kd2ZpQv",
"object": "song",
"status": "complete",
"created_at": "2026-09-16T09:41:02Z",
"completed_at": "2026-09-16T09:47:35Z",
"lyrics": "[Verse 1]\nAnna, six in the morning, chalk on your hands ...",
"preview_url": "https://cdn.lavsange.dk/preview/sng_3n8Kd2ZpQv.mp3",
"audio_url": "https://cdn.lavsange.dk/full/sng_3n8Kd2ZpQv.mp3?expires=1789412855&sig=...",
"duration_seconds": 184,
"paid": true,
"price": { "amount": 2999, "currency": "DKK" },
"metadata": { "order_id": "A-10423" },
"livemode": true
}
En sang går gennem tilstandene i denne rækkefølge. Den går aldrig tilbage, og complete, failed og cancelled er sluttilstande.
| Status | Værdi | Betydning |
|---|---|---|
| queued | Modtaget, venter på en ledig produktionsplads. Normalt få sekunder. | |
| writing_lyrics | Sangteksten bliver skrevet. | |
| lyrics_ready | Teksten er komplet og kan hentes. Nås typisk efter et til to minutter. | |
| generating_audio | Sang, arrangement og mix bliver produceret. | |
| preview_ready | De første 45 sekunder kan hentes, og hele filen ligger klar. | |
| complete | Betalt og fuldt leveret. | |
| failed | Produktionen er endeligt mislykkedes. Der afregnes ikke, og feltet error nævner årsagen. | |
| cancelled | Afbrudt før færdiggørelse. |
Et enkelt mislykket produktionsforsøg fører ikke straks til failed. Vi prøver internt flere gange og giver først op når alle forsøg fejler. Derfor er failed sjælden, og når den optræder betyder den virkelig: denne sang kommer ikke.
GET /v1/songs/{id} giver en sangs aktuelle tilstand. Endepunktet er billigt og må polles hvert sekund, så længe grænsen overholdes.
curl -G https://api.lavsange.dk/v1/songs \
-H "Authorization: Bearer $SONG_API_KEY" \
-d status=complete \
-d limit=20 \
-d starting_after=sng_3n8Kd2ZpQv
Lister er markørbaserede. Du får højst limit poster, som standard 20 og højst 100, nyeste først. Er has_more sand, sender du next_cursor med som starting_after i næste kald. Du kan filtrere på status, occasion, language, paid samt created_after og created_before.
{
"object": "list",
"data": [
{ "id": "sng_9Wq1LmT4bR", "status": "complete", "recipient_name": "Jonas", "...": "..." },
{ "id": "sng_3n8Kd2ZpQv", "status": "complete", "recipient_name": "Anna", "...": "..." }
],
"has_more": true,
"next_cursor": "sng_3n8Kd2ZpQv"
}
Sangteksten er gratis og komplet, ikke et uddrag. GET /v1/songs/{id}/lyrics giver den som text/plain, med markeringer for vers og omkvæd. Den samme tekst står i feltet lyrics på sang-objektet.
Lyden kommer i to trin. Smagsprøven er de første 45 sekunder af den færdige indspilning, ikke en særskilt demo: samme stemme, samme arrangement, samme tekst. Den er tilgængelig uden betaling og bliver ved med at være det. Hele filen leverer GET /v1/songs/{id}/audio først når sangen er låst op.
Begge adresser er signerede og gyldige i 24 timer. De er tænkt til download, ikke til permanente links. Har du brug for en fil i længere tid, så hent den én gang og gem den hos dig selv. Et nyt kald til endepunktet giver til enhver tid en frisk adresse.
Formatet er MP3 med 320 kbit/s hele vejen. Har du brug for WAV, tilføjer du ?format=wav, hvilket er muligt for konti med studieadgang.
Rammer et resultat ved siden af, koster en ny generering ingenting. POST /v1/songs/{id}/regenerate laver en ny version under samme id og sætter status tilbage til queued. Den hidtidige version bevares under previous_versions.
curl https://api.lavsange.dk/v1/songs/sng_3n8Kd2ZpQv/regenerate \
-H "Authorization: Bearer $SONG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"keep_lyrics": false,
"reason": "voice_not_matching",
"note": "Please try a lower male voice and a slower tempo."
}'
Med keep_lyrics: true bliver teksten stående, og kun indspilningen laves om. Det er den rigtige vej når teksten sidder, og kun stemmen eller tempoet var forkert. Med false skrives teksten også om.
Feltet note går direkte ind i den nye generering, så en konkret sætning betaler sig. "Dybere mandsstemme, langsommere" virker, "gør den bedre" gør ikke. Tre nye genereringer pr. sang er gratis, derefter tal med os.
Der er to måder at låse en sang op på, alt efter hvem der betaler.
POST /v1/songs/{id}/checkout laver en hostet betalingsside i domænets valuta, inklusive de betalingsmåder der er almindelige i landet. Du sender kunden derhen og får hændelsen song.paid når betalingen er gået igennem.
curl https://api.lavsange.dk/v1/songs/sng_3n8Kd2ZpQv/checkout \
-H "Authorization: Bearer $SONG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"success_url": "https://example.com/thanks?song=sng_3n8Kd2ZpQv",
"cancel_url": "https://example.com/cart"
}'
{
"object": "checkout_session",
"song": "sng_3n8Kd2ZpQv",
"url": "https://pay.lavsange.dk/c/cs_live_8Hd2Kq...",
"amount": 2999,
"currency": "DKK",
"expires_at": "2026-09-16T11:41:02Z"
}
På konti med samlet afregning låser POST /v1/songs/{id}/unlock sangen op med det samme og trækker 249 DKK på kontoen. Ingen omvej over en betalingsside, hvilket passer til dit eget kassesystem.
curl https://api.lavsange.dk/v1/songs/sng_3n8Kd2ZpQv/unlock \
-H "Authorization: Bearer $SONG_API_KEY" \
-X POST
Brugsretten er den samme i begge tilfælde: ikke eksklusiv, men udtrykkeligt kommerciel. Du må give den færdige sang videre, sælge den og udgive den som en del af dit eget tilbud.
Listerne over anledning, stemning, stil, stemme og sprog ændrer sig af og til. I stedet for at hardkode dem, kald GET /v1/options og gem svaret i cache et par timer.
{
"object": "options",
"language": "da",
"occasions": ["birthday", "wedding", "anniversary", "farewell", "funeral",
"christening", "graduation", "christmas", "declaration", "other"],
"moods": ["happy", "warm", "funny", "romantic", "gentle", "epic", "surprise_me"],
"styles": ["pop", "rock", "folk", "schlager", "hiphop", "ballad", "country",
"electronic", "jazz", "childrens", "surprise_me"],
"voices": ["female", "male", "duet", "choir", "childrens", "surprise_me"],
"languages": ["de", "en", "dk", "nl", "it", "se", "fr", "es", "no", "pl", "fi", "is", "jp"]
}
Alle disse værdier må også udelades. Værdien surprise_me er ikke en pladsholder, men en rigtig instruks: så vælger vi bevidst noget der passer til anledningen og detaljerne.
Angiv en callback_url ved oprettelsen, så sender vi hver hændelse dertil som POST. Det er den anbefalede vej, fordi den sparer både polling og ventetid.
| Hændelse | Type | Udløses når |
|---|---|---|
| song.lyrics_ready | Sangteksten er komplet. | |
| song.preview_ready | Smagsprøven på 45 sekunder kan hentes. | |
| song.completed | Hele indspilningen er leveret. | |
| song.failed | Produktionen er endeligt mislykkedes. | |
| song.regenerated | En ny generering er færdig. | |
| song.paid | Betalingen er modtaget, og sangen er låst op. |
{
"id": "evt_5Tb7Rn2WqX",
"object": "event",
"type": "song.completed",
"created_at": "2026-09-16T09:47:35Z",
"data": {
"object": {
"id": "sng_3n8Kd2ZpQv",
"object": "song",
"status": "complete",
"audio_url": "https://cdn.lavsange.dk/full/sng_3n8Kd2ZpQv.mp3?expires=1789412855&sig=...",
"...": "..."
}
}
}
Hver levering bærer en header med tidsstempel og HMAC-SHA256 over tidsstempel, punktum og den rå body. Tjek den før du tror på indholdet, og kassér alt der er ældre end fem minutter.
X-Song-Signature: t=1789412855,v1=7f2c1d9a4b6e8035c1f7a29d4e5b0c8371a6d2f94e8b3c07a15d9e2f6b4c8a01
import hashlib, hmac, os, time
from flask import Flask, request, abort
SECRET = os.environ["SONG_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
@app.post("/hooks/songs")
def hook():
header = request.headers.get("X-Song-Signature", "")
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
timestamp, signature = parts.get("t", ""), parts.get("v1", "")
if abs(time.time() - int(timestamp or 0)) > 300:
abort(400) # older than five minutes, treat as replay
expected = hmac.new(
SECRET, (timestamp + "." + request.get_data(as_text=True)).encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(400)
event = request.get_json()
if event["type"] == "song.completed":
store(event["data"]["object"])
return "", 200
Vi forventer et svar med status 2xx inden for ti sekunder. Udebliver det, gentager vi otte gange over 24 timer med voksende mellemrum. Leveringer kan derfor gentage sig og i sjældne tilfælde bytte rækkefølge, så gør dit endepunkt idempotent og ret dig efter created_at, ikke efter ankomsttidspunktet.
Hvert POST accepterer headeren Idempotency-Key med en vilkårlig entydig værdi, typisk en UUID. Kommer den samme nøgle igen inden for 24 timer, returnerer vi det oprindelige svar i stedet for at lave en sang mere.
curl https://api.lavsange.dk/v1/songs \
-H "Authorization: Bearer $SONG_API_KEY" \
-H "Idempotency-Key: 9f1c7c2e-0a3b-4c8d-9e21-5f7a1b6c3d40" \
-H "Content-Type: application/json" \
-d '{ "occasion": "birthday", "recipient_name": "Anna", "language": "da", "details": "..." }'
Det er præcis den beskyttelse man har brug for ved netværksfejl: går et svar tabt, og din kode gentager kaldet, opstår der stadig kun én sang. Sender du den samme nøgle med en anden body, svarer vi 409 med fejltypen conflict.
Fejl kommer altid i samme format, med maskinlæsbar type og code, en læsbar besked og, hvor det giver mening, det berørte felt. request_id hører med i enhver supporthenvendelse, så vi kan finde kaldet i loggen.
{
"error": {
"type": "validation_error",
"code": "details_too_short",
"message": "details must contain at least 40 characters so the song has something to work with",
"param": "details",
"request_id": "req_2Lm9Xc4Kd1"
}
}
| Type | HTTP | Betydning |
|---|---|---|
| 400 | invalid_request | Kaldet er formelt i stykker, for eksempel ugyldig JSON eller et ukendt felt. |
| 401 | authentication_error | Nøglen mangler, er udløbet eller spærret. |
| 403 | permission_error | Nøglen er gyldig, men må ikke bruge dette domæne eller endepunkt. |
| 404 | not_found | Det angivne id hører ikke til denne konto eller findes ikke. |
| 409 | conflict | Handlingen passer ikke til tilstanden, for eksempel oplåsning af en afbrudt sang. |
| 422 | validation_error | Kaldet er formelt i orden, men en værdi er ubrugelig, for eksempel for korte detaljer. |
| 429 | rate_limit | For mange kald eller for mange samtidige produktioner. |
| 500 | api_error | Fejl hos os. Gentag med voksende mellemrum. |
Ved 429 og 5xx giver et nyt forsøg mening, helst med eksponentielt voksende mellemrum og lidt tilfældighed. Ved 4xx ud over 429 gør det ikke: det samme kald fejler igen.
| Grænse | Værdi | Gælder for |
|---|---|---|
| 60 / min | Kald pr. minut pr. nøgle på tværs af alle endepunkter. | |
| 10 | Samtidige produktioner. Flere kald venter i kø. | |
| 64 KB | Største størrelse på en request-body. | |
| 40 - 4000 | Tegn i feltet details, minimum og maksimum. | |
| 90 | Dage vi gemmer sange og input, hvorefter de slettes. | |
| 24 h | Perioden hvor en idempotensnøgle giver det gamle svar. |
Hvert svar bærer den aktuelle status i sine headere, så du ikke skal gætte.
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1789412880
X-Concurrent-Limit: 10
X-Concurrent-Running: 3
Højere grænser er ikke noget problem, de er bare ikke standard. Vokser dit volumen, så skriv kort til os, så hæver vi dem.
Hovedversionen står i stien og er stabil. Inden for v1 kommer der kun additive ændringer: nye felter, nye værdier i opremsninger, nye endepunkter. Eksisterende felter forsvinder ikke og skifter ikke betydning.
Vil du have ekstra sikkerhed, så fastlås en dato i en header. Uden header gælder altid den nyeste opførsel.
X-Song-Version: 2026-09-01
Din kode bør ignorere ukendte felter i svar frem for at bryde sammen over dem. Det er den eneste antagelse vi gør om klienter.
Nøgler med præfikset sk_test_ kører gennem præcis de samme endepunkter, men udløser ingen rigtig produktion og koster ingenting. Efter få sekunder får du en fast demotekst og en demoindspilning, og alle objekter bærer livemode: false.
Det gør det også muligt at øve de ubehagelige tilfælde. Bestemte navne i feltet recipient_name fremtvinger et bestemt udfald: test_fail fører til failed, test_slow til en produktion på cirka ti minutter, test_ratelimit til et 429-svar. Så kan fejlhåndteringen testes uden at vente på et rigtigt nedbrud.
Webhooks virker også i testtilstand, med den samme signaturmetode og en egen hemmelighed.
Med oplåsningen får du en ikke-eksklusiv, men udtrykkeligt kommerciel brugsret til den færdige sang. Du må give den videre, sælge den, spille den offentligt og lægge den ind i dit eget produkt. Ikke eksklusiv betyder: vi beholder retten til selv at bruge indspilningen, for eksempel som eksempel.
Ophavsretten til AI-skabt musik er endnu ikke endeligt afklaret i mange retssystemer. Vi sikrer dig brugen kontraktligt, men kan ikke love at der opstår en selvstændig ophavsret til indspilningen, som du kan håndhæve over for tredjemand. Er du afhængig af det, bør du få det vurderet først.
Input og færdige sange gemmer vi i 90 dage, derefter slettes de. Vil du slette en enkelt sang tidligere, bruger du DELETE /v1/songs/{id}. Oplysningerne fra details bruger vi udelukkende til at producere den pågældende sang og ikke til at træne egne modeller.
Sender du data om dine kunder til os, er du dataansvarlig og vi databehandler. En databehandleraftale får du på forespørgsel.
Spørgsmål, højere grænser, databehandleraftale, særtilfælde: songs@maxkuch.com. Nævn ved tekniske problemer request_id fra fejlsvaret, så finder vi kaldet med det samme.
Til integrationer via AI-agenter findes desuden en Model Context Protocol-server. Den er dokumenteret på /mcp/ og bruger de samme nøgler som REST-API’et.
Nøgler udstedes manuelt. En kort mail med dit formål, forventet volumen og sprog er nok, og adgangen plejer at tage én hverdag.