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:
- Sign in at /login with a magic link.
- Visit /dashboard/keys → click + New key → save the
mvp_sk_live_*value (shown once). - POST your intake to
/v1/generate. You'll get back a 202 with a project_id. - Either set a
webhook_urland 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:
mvp_sk_live_*— production. Counts against your subscription quota and triggers Stripe metering.mvp_sk_test_*— non-production. Same surface, separate billing flag for future test-mode work.
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:
- Monthly / Annual: 20 generations included per month. Overage is $3 each, metered to Stripe at completion.
- Founders: same 20/mo + $3 overage, but the subscription itself never recurs.
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
| Field | Type | Required | Notes |
|---|---|---|---|
intake.niche | string | yes | min 2 chars. e.g. "productivity coaching" |
intake.audience | string | yes | min 2 chars. e.g. "busy remote managers" |
intake.transformation | string | yes | min 2 chars. The outcome your client offers. |
intake.business_name | string | no | Appears on the PDF cover and in some copy. |
intake.offer | string | no | The specific product/program being sold. |
intake.tone | string | no | Voice descriptor. e.g. "direct, expert" |
intake.notes | string | no | Constraints, proof points, audience pains, pricing. |
webhook_url | string (URL) | no | We POST the final asset bundle here on completion. |
metadata | object | no | Echoed 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.
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)project_id.Asset schema
The same shape is used in webhook payloads and /assets responses.
| Field | Type | Notes |
|---|---|---|
key | string | Stable identifier: ebook, ebook_pdf, course, landing_page, emails, strategy |
name | string | Human label for UI rendering. |
type | string | "markdown" (default) or "pdf" |
content_type | string | MIME type. text/markdown; charset=utf-8 or application/pdf |
storage_path | string | The path inside our Supabase bucket. Use this if you cache locally. |
download_url | string (URL) | null | Signed URL, 24h TTL. Null if signing fails (rare — re-call /assets to retry). |
size_bytes | integer | File size at upload time. |
Errors & status codes
| Status | When | Body |
|---|---|---|
202 | Generation queued successfully. | Project + polling URLs. |
401 | Missing, malformed, or unknown API key. | {"detail": "Invalid API key"} |
402 | Free-tier limit reached or subscription suspended. | {"detail": "Free tier limit reached..."} |
404 | Project doesn't exist or doesn't belong to your key. | {"detail": "Project not found"} |
422 | Validation failed (missing required intake fields, etc.). | Pydantic error array with field paths. |
500 | Our 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
- 2026-05-22 — Sprint 2C: Stripe metered billing (free tier, monthly, annual, founders), super-admin tools, projects history page.
- 2026-05-21 — Sprint 2B: Customer dashboard (magic-link auth, self-serve API keys, usage events).
- 2026-05-21 — Sprint 2A: BaaS API key auth, /v1/generate endpoint, webhook delivery.
- 2026-05-20 — PDF sprint: branded cover, WeasyPrint rendering.
- 2026-05-19 — Initial LangGraph pipeline: 6-asset generation, Supabase storage.