API Docs

The MVP Generation Engine API turns a single intake form into six production-ready assets — ebook, branded PDF, course outline, landing page copy, sales emails, and strategy summary — delivered async to your webhook. Base URL: https://api-mvp.truthjblue.dev

Quickstart

Four steps to your first generation:

  1. Sign in at /login with a magic link.
  2. Visit /dashboard/keys → click + New key → save the mvp_sk_live_* value (shown once).
  3. POST your intake to /v1/generate. You'll get back a 202 with a project_id.
  4. Either set a webhook_url and wait for our POST to your endpoint, or poll /v1/projects/{id} every ~10 seconds.

Minimal end-to-end example:

curl -X POST https://api-mvp.truthjblue.dev/v1/generate \
  -H "Authorization: Bearer mvp_sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "intake": {
      "niche": "productivity coaching",
      "audience": "remote managers",
      "transformation": "async-first team rituals"
    },
    "webhook_url": "https://your-app.com/mvp-webhook",
    "metadata": { "client_ref": "ACME-001" }
  }'

Authentication

Every request to /v1/* requires an API key in the Authorization header:

Authorization: Bearer mvp_sk_live_<your-key>

Keys are minted at /dashboard/keys and are shown in full only once. We store a SHA-256 hash; we cannot show you the raw key again. Lost keys must be revoked and replaced.

Two key prefixes are minted:

Security: treat keys like passwords. Never commit them, never expose them in client-side code. If a key leaks, revoke it from /dashboard/keys — revocation is immediate.

Free tier & billing

Without a subscription, every account gets 3 free generations for life. The 4th call returns 402 Payment Required with a link to /pricing.

With an active subscription:

See your real-time usage at /dashboard/usage and current plan at /dashboard/billing.

POST /v1/generate

Queue a generation. Returns immediately with a project ID and two polling URLs. The actual work runs in our Celery worker and takes ~3 minutes.

Request body

FieldTypeRequiredNotes
intake.nichestringyesmin 2 chars. e.g. "productivity coaching"
intake.audiencestringyesmin 2 chars. e.g. "busy remote managers"
intake.transformationstringyesmin 2 chars. The outcome your client offers.
intake.business_namestringnoAppears on the PDF cover and in some copy.
intake.offerstringnoThe specific product/program being sold.
intake.tonestringnoVoice descriptor. e.g. "direct, expert"
intake.notesstringnoConstraints, proof points, audience pains, pricing.
webhook_urlstring (URL)noWe POST the final asset bundle here on completion.
metadataobjectnoEchoed back unchanged in the webhook payload. Useful for client_ref tracking.

Response (202 Accepted)

{
  "project_id": "a3a77937-7174-44dc-872a-a0e3126b9e56",
  "status": "queued",
  "status_url": "https://api-mvp.truthjblue.dev/v1/projects/a3a77937-...",
  "assets_url": "https://api-mvp.truthjblue.dev/v1/projects/a3a77937-.../assets"
}

Idempotency

Network retries are unavoidable. Send an Idempotency-Key header (any unique string per logical request — a UUID is ideal) and a repeat with the same key returns the original project unchanged, with X-Idempotent-Replay: true on the response. No second generation starts and no second meter event is billed. Keys are scoped to your API key, so you choose their format freely.

Without an Idempotency-Key, every POST is treated as a new request — a double-submit produces two projects and (on a paid plan) two billable generations.

GET /v1/projects/{id}

Poll this to track progress. Returns the current phase (0–7) and a human-readable name. Phases progress: intake → ebook → ebook_pdf → course → landing_page → emails → strategy → finalized.

Response

{
  "project_id": "a3a77937-7174-44dc-872a-a0e3126b9e56",
  "status": "generating",
  "current_phase": 3,
  "current_phase_name": "course",
  "error": null,
  "assets": []
}

Statuses: queued, generating, completed, failed. Once completed or failed, the response stops changing — stop polling.

GET /v1/projects/{id}/assets

Returns the asset bundle for a completed project. Each asset includes a fresh download_url (signed, 24-hour TTL). Re-call this endpoint anytime to refresh expired URLs without paying for re-generation.

For projects still in queued or generating status, this returns an empty assets array.

Webhooks

If you include webhook_url in your /v1/generate request, we POST the asset bundle to that URL on completion. Delivery uses exponential backoff (2s, 4s, 8s) on 4xx/5xx responses, then gives up.

The payload your endpoint receives:

{
  "project_id": "a3a77937-7174-44dc-872a-a0e3126b9e56",
  "status": "completed",
  "assets": [
    {
      "key": "ebook",
      "name": "Ebook Manuscript",
      "type": "markdown",
      "storage_path": "projects/a3a77937.../ebook.md",
      "download_url": "https://aagqvfwuixpxtdcrdxmv.supabase.co/storage/v1/object/sign/...",
      "size_bytes": 18432
    },
    {
      "key": "ebook_pdf",
      "name": "Ebook PDF",
      "type": "pdf",
      "content_type": "application/pdf",
      "storage_path": "projects/a3a77937.../ebook_pdf.pdf",
      "download_url": "https://aagqvfwuixpxtdcrdxmv.supabase.co/storage/v1/object/sign/...",
      "size_bytes": 142016
    },
    { "key": "course", "name": "Course Outline", ... },
    { "key": "landing_page", "name": "Landing Page Copy", ... },
    { "key": "emails", "name": "Email Sequence", ... },
    { "key": "strategy", "name": "Strategy Summary", ... }
  ],
  "errors": null,
  "metadata": { "client_ref": "ACME-001" }
}

Your endpoint should respond with a 200 within 30 seconds. Slower than that and we'll retry.

Verifying signatures

Every delivery includes an X-MVP-Signature header so you can confirm the request really came from us. The header has the form t=<unix_timestamp>,v1=<hex> where v1 is HMAC-SHA256(webhook_secret, "{t}.{raw_body}"). Your webhook_secret (prefix whsec_) is shown once when you mint the API key. Recompute the HMAC over the raw request body and compare in constant time; optionally reject timestamps older than ~5 minutes to bound replay.

import hashlib, hmac, time

def verify(raw_body: bytes, signature_header: str, secret: str,
           max_age_s: int = 300) -> bool:
    # signature_header looks like "t=1700000000,v1=abc123..."
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    t, v1 = parts["t"], parts["v1"]
    if abs(time.time() - int(t)) > max_age_s:
        return False  # stale -> possible replay
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(v1, expected)
Idempotency: we may retry on transient network errors, and retries carry the same signature. Your endpoint should handle the same payload arriving twice — dedupe by project_id.

Asset schema

The same shape is used in webhook payloads and /assets responses.

FieldTypeNotes
keystringStable identifier: ebook, ebook_pdf, course, landing_page, emails, strategy
namestringHuman label for UI rendering.
typestring"markdown" (default) or "pdf"
content_typestringMIME type. text/markdown; charset=utf-8 or application/pdf
storage_pathstringThe path inside our Supabase bucket. Use this if you cache locally.
download_urlstring (URL) | nullSigned URL, 24h TTL. Null if signing fails (rare — re-call /assets to retry).
size_bytesintegerFile size at upload time.

Errors & status codes

StatusWhenBody
202Generation queued successfully.Project + polling URLs.
401Missing, malformed, or unknown API key.{"detail": "Invalid API key"}
402Free-tier limit reached or subscription suspended.{"detail": "Free tier limit reached..."}
404Project doesn't exist or doesn't belong to your key.{"detail": "Project not found"}
422Validation failed (missing required intake fields, etc.).Pydantic error array with field paths.
500Our bug. Should be rare; auto-retried by the worker.Error name + class.

Rate limits

We currently run with worker concurrency = 1, so generations process sequentially. There are no hard per-key rate limits today — practically, you can submit jobs as fast as you like; they'll queue.

At scale (50+ active customers, expected in Sprint 2F+) we'll add a per-key rate limit, likely 10 generations per hour. We'll announce via email and webhook headers before enforcing.

Changelog