Self-delete flow
Each submission returns a one-time delete_key. Users can review and delete via /delete.html or the API:
Body {"key":"..."} — preview metadata. Rate limited.
Body {"key":"..."} — permanent delete. Returns {"deleted":true}.
Getting started
The submission portal has a JSON API for listing shows, reading runtime config, creating submissions (with optional resumable file uploads), and self-service deletion.
Access: Browser calls from another site need your origin allowed, and POST /api/submit requires a Cloudflare Turnstile token. Both come from the admin.
Public endpoints
No auth. Returns the shows currently offered for submission: { shows: [{ slug, title, code, has_phrase, phrase_hint, locked }] }. code is omitted when the show has no short code. A single path segment and ?show= both match a visible show's code or slug, ignoring case. A slug match wins. The page writes the path in lowercase.
{ turnstile_site_key, global_locked }
{ "version": "0.6.0" }
JSON body: show, turnstile_token, handle, segment, links, notes, contact_info, expected_files, files[]. Set expected_files to 0 for link-only. With files, response includes upload_token and upload_url.
Errors: {"error":"..."} — 403 if locked, Turnstile fails, or wrong phrase.
curl -X POST "$ORIGIN/api/submit" -H 'Content-Type: application/json' -d '{
"show": "my-show",
"turnstile_token": "TOKEN",
"handle": "fan",
"segment": "segment name",
"links": ["https://example.com"],
"expected_files": 0
}'
Header X-Upload-Token or body upload_token. Call after TUS uploads finish. Returns {"status":"complete"}.
Resumable uploads (tus.io). Metadata must include upload_token and filename. Standard: 100 MB × 10 files; VIP passphrase: 1 GB × 50.
Server-Sent Events. Public clients receive version, global_lock, and lock events. Keepalive every 30s.
Embed integration
Serve the widget at GET /embed/{slug}. Query ?theme=light or ?theme=dark, optional segment= prefill.
<iframe
src="https://YOUR-HOST/embed/my-show?theme=dark"
title="Submit"
style="width:100%;max-width:480px;border:0;"
id="pd-embed"></iframe>
<script>
window.addEventListener('message', function (e) {
if (!e.data || !e.data.type) return;
if (e.data.type === 'podcast-drop:resize') {
document.getElementById('pd-embed').style.height = e.data.height + 'px';
}
if (e.data.type === 'podcast-drop:submitted') {
console.log('Submitted', e.data.id, e.data.fileCount);
}
if (e.data.type === 'podcast-drop:error') {
console.warn(e.data.message);
}
});
</script>
Parent page origin must be in the CORS allowlist if the embed host differs from the API host (fetch calls inside the iframe).