API Documentation

Public submit API and embed widget

← Portal

Self-delete flow

Each submission returns a one-time delete_key. Users can review and delete via /delete.html or the API:

POST /api/delete/lookup

Body {"key":"..."} — preview metadata. Rate limited.

POST /api/delete/confirm

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

GET /shows.json

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.

GET /api/config

{ turnstile_site_key, global_locked }

GET /api/version

{ "version": "0.6.0" }

POST /api/submit

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
}'
POST /api/submit/{id}/complete

Header X-Upload-Token or body upload_token. Call after TUS uploads finish. Returns {"status":"complete"}.

TUS /api/upload/*

Resumable uploads (tus.io). Metadata must include upload_token and filename. Standard: 100 MB × 10 files; VIP passphrase: 1 GB × 50.

GET /api/events

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).