Going live programmatically
Going live requires three things: an RTMP ingest URL, a stream key, and an HLS playback URL. How you obtain these (and who publishes the kind 30311 Nostr event) depends on the path you take.
You have three options:
- Option A: Bring your own RTMP server — you have an HLS URL from somewhere (Cloudflare Stream, SRS, Owncast, OME, Livepeer, API.video). You sign and publish the 30311 event yourself.
- Option B: Use Shosho Server — Shosho Server (
api.shosho.live/api/v1) gives you an ingest URL and a stream key, accepts your RTMP, and signs and publishes the 30311 event for you. - Option C: Use the Show Chat API — you already have an HLS playback URL from somewhere, and you want Shosho to manage the Nostr side (signing, publishing, monitoring, and ending the 30311 event) on your behalf.
Streaming clients
Any RTMP client works:
- Shosho app — handles RTMP broadcasting from camera, stream key management, Nostr event publishing, and chat. iOS and Android.
- OBS Studio — desktop. Configure the RTMP ingest URL and stream key in Settings → Stream.
- FFmpeg — for testing:
ffmpeg -f lavfi -i testsrc=size=1280x720:rate=30 \
-f lavfi -i sine=frequency=440:sample_rate=44100 \
-vcodec libx264 -preset veryfast -b:v 2500k \
-acodec aac -b:a 128k \
-f flv "rtmp://<ingest-url>/<stream-key>"Option A: Generic RTMP server
If you have your own RTMP server (self-hosted SRS, Owncast, or OME, or a hosted service like Cloudflare Stream, Livepeer, or API.video), you need:
- RTMP Ingest URL — e.g.
rtmp://your-server.com/live - Stream Key — e.g.
abcd-1234 - HLS Playback URL — must end with
.m3u8, e.g.https://your-server.com/stream.m3u8
The server distributes the video, but it doesn’t know about Nostr.
Who publishes the kind 30311 event?
- Shosho app + generic server — the app publishes for you.
- Any other client (OBS, FFmpeg) — you must publish the kind 30311 event yourself. See Live event structure.
Avoid walled gardens. YouTube, Twitch, and Facebook hide their HLS playback URL so viewers can only watch on their platform — incompatible with Nostr streaming. Most other services (SRS, Owncast, OME, Cloudflare Stream, Livepeer, API.video) expose
.m3u8URLs as standard.
Option B: Shosho Server (Nostr streaming server)
Shosho Server is a Nostr-aware streaming server. It provides the ingest URL, stream key, and HLS playback URL via API, and signs and publishes the kind 30311 live event on your behalf.
All Shosho Server API calls require NIP-98 authentication.
Order of operations
- Get account info and streaming endpoints.
- If required, accept terms.
- If required, top up balance.
- If required, update stream metadata.
Users must have accepted the terms and have a sufficient balance of satoshis (0 if the endpoint is free).
Get account info and streaming endpoints
AUTH=$(nak event -k 27235 \
-t u=https://api.shosho.live/api/v1/account \
-t method=GET \
--sec <nsec1...> < /dev/null 2>/dev/null | base64)
curl -H "Authorization: Nostr $AUTH" \
https://api.shosho.live/api/v1/accountResponse:
{
"endpoints": [
{
"name": "Basic",
"url": "rtmp://ingest.shosho.live:1935/Basic",
"key": "<stream-key-uuid>",
"cost": { "rate": 0, "unit": "min" }
}
],
"tos": { "accepted": true, "link": "https://shosho.live/legal/terms" }
}The url field is the RTMP ingest endpoint your encoder should connect to. The key field is the stream key your encoder authenticates with. Stream to <url>/<key> (e.g. rtmp://ingest.shosho.live:1935/Basic/<stream-key-uuid>).
Accept terms of service
If tos.accepted is false, present the link to the user and accept:
AUTH=$(nak event -k 27235 \
-t u=https://api.shosho.live/api/v1/account \
-t method=PATCH \
--sec <nsec1...> < /dev/null 2>/dev/null | base64)
curl -X PATCH -H "Authorization: Nostr $AUTH" \
-H "Content-Type: application/json" \
-d '{"accept_tos": true}' \
https://api.shosho.live/api/v1/accountTop up balance with Lightning
AUTH=$(nak event -k 27235 \
-t u=https://api.shosho.live/api/v1/topup?amount=1000 \
-t method=GET \
--sec <nsec1...> < /dev/null 2>/dev/null | base64)
curl -H "Authorization: Nostr $AUTH" \
"https://api.shosho.live/api/v1/topup?amount=1000"amount is in satoshis. Response contains a bolt11 invoice:
{ "pr": "lnbc10u1p..." }Pay the pr invoice with any Lightning wallet. Balance updates automatically once the payment confirms.
Update stream metadata
AUTH=$(nak event -k 27235 \
-t u=https://api.shosho.live/api/v1/event \
-t method=PATCH \
--sec <nsec1...> < /dev/null 2>/dev/null | base64)
curl -X PATCH -H "Authorization: Nostr $AUTH" \
-H "Content-Type: application/json" \
-d '{"title": "Updated Title", "summary": "New description"}' \
https://api.shosho.live/api/v1/eventCreate a new stream key with metadata
AUTH=$(nak event -k 27235 \
-t u=https://api.shosho.live/api/v1/keys \
-t method=POST \
--sec <nsec1...> < /dev/null 2>/dev/null | base64)
curl -X POST -H "Authorization: Nostr $AUTH" \
-H "Content-Type: application/json" \
-d '{"title": "My Stream", "summary": "Going live!", "image": "https://example.com/thumb.jpg"}' \
https://api.shosho.live/api/v1/keysDelete a stream
Request the server to delete a stream by its d tag. The server removes it and publishes a deletion event on your behalf.
AUTH=$(nak event -k 27235 \
-t u=https://api.shosho.live/api/v1/stream/<d-tag> \
-t method=DELETE \
--sec <nsec1...> < /dev/null 2>/dev/null | base64)
curl -X DELETE -H "Authorization: Nostr $AUTH" \
https://api.shosho.live/api/v1/stream/<d-tag>API delete vs NIP-09 (kind 5)
DELETE /stream/<d-tag>— for streams created via Shosho Server. The server signed the 30311 event with its own key, so only the server can delete it from relays. Your own NIP-09 deletion would not be valid.- NIP-09 (kind 5) — for streams where you published the 30311 event yourself (you used a generic RTMP server and signed with your own key).
Option C: Show Chat API
Use the Show Chat API when you already have an HLS playback URL (from your own server, a third-party service, or anywhere else) and you want Shosho to manage the Nostr side for you. You don’t need to sign or publish the 30311 event — the Shosho backend signs, publishes, and monitors it on your behalf.
The user is tagged as the host. The user’s pubkey is extracted from the NIP-98 signed auth event and used as hostPubkey — no pubkey needs to be sent in the request body.
Base URL: https://shosho.live/api/show-chat
What it does for you:
- Signs and publishes the kind 30311 event with the Shosho service key, p-tagging you as host.
- Polls your HLS URL once a minute to verify the stream is alive.
- Republishes the event every 15 minutes to keep it fresh.
- Automatically marks the event
endedafter 3 consecutive HLS failures (about 2 minutes), so you never end up “stuck live”. - Provides explicit
updateanddeleteendpoints to change metadata or end the stream cleanly.
When to pick this over Option A: if you have an HLS URL and you’d rather not implement signing, publishing, or liveness handling yourself. The trade-off is that the 30311 event is signed by Shosho’s key (with you as host), not your own key — so deletions go through the Show Chat API, not NIP-09.
When NOT to pick this: if you’re streaming to Shosho Server (Option B) — Shosho Server already publishes the 30311 for you. Using both produces duplicate events.
Create a show-chat (go live)
AUTH=$(nak event -k 27235 \
-t u=https://shosho.live/api/show-chat/create \
-t method=POST \
--sec <nsec1...> < /dev/null 2>/dev/null | base64)
curl -X POST -H "Authorization: Nostr $AUTH" \
-H "Content-Type: application/json" \
-d '{
"dTag": "my-stream-id",
"hlsUrl": "https://your-server.com/stream.m3u8",
"title": "My Live Stream",
"summary": "Going live!",
"image": "https://example.com/thumbnail.jpg"
}' \
https://shosho.live/api/show-chat/createRequest body:
| Field | Type | Required | Description |
|---|---|---|---|
dTag | string | Yes | Unique identifier for this show-chat (used as the d tag in the 30311) |
hlsUrl | string | Yes | HLS playback URL (.m3u8) |
title | string | No | Stream title (defaults to “Livestream”) |
summary | string | No | Stream description |
image | string | No | Thumbnail/cover image URL |
Response (200):
{
"success": true,
"dTag": "my-stream-id",
"naddr": "naddr1..."
}The naddr is the NIP-19 address for the live event. Build the stream URL: https://shosho.live/live/<naddr>.
The backend publishes a kind 30311 event with status: live, signed by the Shosho service key, with the user p-tagged as host. It then monitors the HLS URL and republishes the event periodically. If the HLS stream goes offline, the backend publishes status: ended automatically.
List your show-chats
AUTH=$(nak event -k 27235 \
-t u=https://shosho.live/api/show-chat/list \
-t method=GET \
--sec <nsec1...> < /dev/null 2>/dev/null | base64)
curl -H "Authorization: Nostr $AUTH" \
https://shosho.live/api/show-chat/listResponse (200):
{
"success": true,
"showChats": [
{
"d_tag": "my-stream-id",
"host_pubkey": "<your_hex_pubkey>",
"hls_url": "https://your-server.com/stream.m3u8",
"title": "My Live Stream",
"summary": "Going live!",
"image": "https://example.com/thumbnail.jpg",
"status": "live",
"naddr": "naddr1...",
"consecutive_failures": 0,
"last_checked_at": "2026-03-27T05:39:41Z",
"last_published_at": "2026-03-27T05:39:41Z",
"created_at": "2026-03-27T05:39:41Z",
"updated_at": "2026-03-27T05:39:41Z"
}
]
}Returns only show-chats belonging to the authenticated user.
Update a show-chat (go offline or change metadata)
AUTH=$(nak event -k 27235 \
-t u=https://shosho.live/api/show-chat/update \
-t method=POST \
--sec <nsec1...> < /dev/null 2>/dev/null | base64)
curl -X POST -H "Authorization: Nostr $AUTH" \
-H "Content-Type: application/json" \
-d '{"dTag": "my-stream-id", "status": "ended"}' \
https://shosho.live/api/show-chat/updateRequest body:
| Field | Type | Required | Description |
|---|---|---|---|
dTag | string | Yes | The show-chat identifier |
status | "live" or "ended" | No | Update the stream status |
hlsUrl | string | No | Update the HLS URL |
title | string | No | Update the title |
summary | string | No | Update the description |
image | string | No | Update the thumbnail |
The backend publishes an updated kind 30311 event with the new values. Setting status to ended marks the stream offline on Nostr.
Delete a show-chat
AUTH=$(nak event -k 27235 \
-t u=https://shosho.live/api/show-chat/delete \
-t method=POST \
--sec <nsec1...> < /dev/null 2>/dev/null | base64)
curl -X POST -H "Authorization: Nostr $AUTH" \
-H "Content-Type: application/json" \
-d '{"dTag": "my-stream-id"}' \
https://shosho.live/api/show-chat/deleteThe backend publishes a kind 5 deletion event (NIP-09) for the 30311 event and removes the show-chat record from the database.
Show Chat vs Shosho Server
| Shosho Server | Show Chat API | |
|---|---|---|
| Use when | You need a RTMP server to serve video | You are already serving video to an RTMP server |
| User provides | The RTMP stream | The HLS playback URL |
| Who signs the event | Shosho Server | Shosho Show Chat |
| Liveness checks | Yes | Yes |
| Periodic republish | Yes | Yes |
| Ends on disconnect | Yes | Yes (after 3 polls attempts fail) |
Anti-patterns
- Do not publish a 30311 manually when using the Show Chat API. The backend signs and publishes the 30311 for you. A manual publish creates a duplicate live event — one signed by you, one signed by the Shosho service key.
- End the stream cleanly with
update(status: ended) ordelete. If you don’t, the backend will eventually mark it ended after 3 consecutive HLS poll failures (about 2 minutes), but explicit endings are cleaner and immediate. - Do not use Show Chat if you’re streaming to a Nostr streaming server (Option B). Shosho Server already publishes the 30311 itself — stacking Show Chat on top creates duplicate live events.