Public API
Copy page as Markdown

Turn any text into audio — pay per narration.

PodRead converts articles and text into audio. Machine Payments Protocol (MPP) lets you pay per narration with on-chain stablecoin on the Tempo network — no PodRead account required. Pricing is tiered: Standard voices cost 75¢, Premium voices cost $1.00. Authenticated users whose free tier is exhausted can also pay via MPP instead of subscribing.

Tier Price Voices
Standard $0.75 wren, felix, sloane, archer, gemma, hugo, quinn, theo
Premium $1.00 elara, callum, lark, nash
Currency
pathUSD
Network
Tempo
TTL
24 h

Quick start

The fastest path uses the mppx CLI. It handles the 402 challenge, on-chain payment, and credential construction automatically.

bash one-time setup + paid request
# Create an MPP wallet (one-time setup)
npx mppx account create

# Fund the wallet with testnet pathUSD
npx mppx account fund --rpc-url https://rpc.testnet.tempo.xyz

# Create a narration — mppx handles the 402 flow automatically
npx mppx http://your-podread-host/api/v1/mpp/narrations \
  -X POST -H "Content-Type: application/json" \
  -d '{"source_type":"url","url":"https://www.worksinprogress.news/p/the-secret-behind-japans-railways"}' \
  --rpc-url https://rpc.testnet.tempo.xyz

Anonymous MPP calls (this example) hit POST /api/v1/mpp/narrations and return a nar_-prefixed Narration id you can poll at GET /api/v1/mpp/narrations/:id. Authenticated users who want to pay per episode hit POST /api/v1/mpp/episodes instead and get an ep_-prefixed Episode attached to their primary podcast. See Authentication paths for the full matrix.

Omitting voice defaults to felix (Standard, 75¢). Request a Premium voice (elara, callum, lark, nash) and the 402 challenge arrives at $1.00 instead — see Pricing.

Protocol flow

Five steps: request, receive challenge, pay, retry with credential, poll for audio.

  1. Step 1

    Request episode creation

    curl -X POST https://podread.app/api/v1/mpp/narrations \
      -H "Content-Type: application/json" \
      -d '{"source_type":"url","url":"https://www.worksinprogress.news/p/the-secret-behind-japans-railways"}'

    Without a Payment credential, the server returns 402 Payment Required. The amount reflects the resolved voice's tier — 75 cents for Standard (the example below), 100 cents if the request asked for a Premium voice.

    response headers
    HTTP/1.1 402 Payment Required
    WWW-Authenticate: Payment
      id="a1b2c3d4e5f6…",
      realm="podread.app",
      method="tempo",
      intent="charge",
      request="<base64>",
      expires="2026-04-10T12:05:00+00:00"
    response body
    {
      "error": "Payment required",
      "challenge": {
        "id": "a1b2c3d4e5f6…",
        "amount": 75,
        "currency": "usd",
        "methods": ["tempo"],
        "realm": "podread.app",
        "expires": "2026-04-10T12:05:00+00:00",
        "deposit_address": "0x1234abcd…"
      }
    }

    Fields in the outer challenge object:

    • id string

      HMAC-SHA256 hex over realm|method|intent|request|expires signed with the server's secret — doubles as challenge identifier and tamper-check. Clients should echo it back unchanged in the credential.

    • amount integer

      In cents (75 = $0.75 for Standard, 100 = $1.00 for Premium). Distinct from request.amount below, which is in token base units.

    • currency string

      The ISO 4217 code ("usd"). Distinct from request.currency below, which is an ERC-20 contract address.

    • request string

      Standard base64 (with padding, +// alphabet) — not base64url. Decodes to the object below.

    {
      "amount": "750000",
      "currency": "0x20c0000000000000000000000000000000000000",
      "recipient": "0x1234abcd…"
    }
    • amount string

      Token base units — pathUSD has 6 decimals, so 750000 = $0.75 (Standard) and 1000000 = $1.00 (Premium).

    • currency string

      The pathUSD token contract address on Tempo.

    • recipient string

      The one-time Stripe-provisioned deposit address.

  2. Step 2

    Pay

    Send a pathUSD transfer on the Tempo network to the deposit_address from the challenge. The transfer must be:

    • To the exact deposit_address returned in the 402 response
    • For the exact request.amount from the challenge — 750000 base units for Standard voices ($0.75), 1000000 for Premium ($1.00)
    • Of the pathUSD token at contract 0x20c0…0000
    • Completed before the challenge expires timestamp
  3. Step 3

    Retry with credential

    Construct a credential and retry the original request with it in the Authorization header. The credential is a base64url-encoded JSON object:

    {
      "challenge": {
        "id": "a1b2c3d4e5f6…",
        "realm": "podread.app",
        "method": "tempo",
        "intent": "charge",
        "request": "<base64-encoded-request-json>",
        "expires": "2026-04-10T12:05:00+00:00"
      },
      "payload": {
        "hash": "0xabc123…"
      }
    }

    The payload contains either hash (tx hash of an already-submitted transfer, shown above) or signature (a signed raw tx the server will submit on your behalf via eth_sendRawTransaction). The signature must be an RLP-encoded signed Ethereum transaction, hex-prefixed — any valid transaction type (legacy, EIP-2930, EIP-1559) is accepted as long as it transfers the right amount of pathUSD to the challenge's deposit address.

    {
      "challenge": { /* same as above */ },
      "payload": {
        "signature": "0x02f875…"
      }
    }

    Encode this JSON as base64url (no padding, -/_ instead of +//) and send it:

    curl -X POST https://podread.app/api/v1/mpp/narrations \
      -H "Content-Type: application/json" \
      -H "Authorization: Payment <base64url-encoded-credential>" \
      -d '{"source_type":"url","url":"https://www.worksinprogress.news/p/the-secret-behind-japans-railways"}'

    On success, the server returns 201 Created:

    { "id": "nar_aB3xK9mQ2pL7" }
    
    // Payment-Receipt header:
    // tx=0xabc123…, payment=mpp_…, sig=<hmac-hex>
    • id is the created resource's public identifier. The prefix (ep_ vs nar_) tells you whether this was an Episode (from /api/v1/episodes or /api/v1/mpp/episodes) or a Narration (from /api/v1/mpp/narrations).
    • Payment-Receipt is a comma-separated triplet: tx is the on-chain transaction hash, payment is the opaque MppPayment public id (useful for support inquiries), sig is an HMAC-SHA256 over the receipt body signed with the server's secret so clients can verify authenticity.
  4. Step 4

    Poll for status

    Audio generation is asynchronous. Poll the narration endpoint:

    curl https://podread.app/api/v1/mpp/narrations/nar_aB3xK9mQ2pL7

    Responses progress through pendingpreparingprocessingcomplete.

  5. Step 5

    Get audio

    When status is complete, the response includes an audio_url:

    {
      "id": "nar_aB3xK9mQ2pL7",
      "status": "complete",
      "title": "Example Article",
      "author": "Jane Doe",
      "duration_seconds": 342,
      "audio_url": "https://storage.googleapis.com/…/narrations/abc123.mp3"
    }

    Narrations expire 24 hours after creation. After expiration, GET /api/v1/mpp/narrations/:id returns 404. Episodes from /api/v1/mpp/episodes persist in the authenticated user's account and are retrieved via the Episodes API instead.

Authentication paths

There are three POST endpoints, one per intent. The endpoint you call determines what credentials the server demands and what resource it creates.

Path 1

Subscriber / credits

POST /api/v1/episodes

Authenticated users with an active subscription or remaining credits. No MPP. Free-tier users who are out of credits get a 402 pointing at the billing upgrade URL — not an MPP challenge.

Authorization:
Bearer <token>

Returns {"id":"ep_…"}

Path 2

Anonymous MPP

POST /api/v1/mpp/narrations

No account required. Pay per call via MPP. Creates a Narration — standalone audio, no user, 24h TTL, polled at /api/v1/mpp/narrations/:id.

Authorization:
Payment <credential>

Returns {"id":"nar_…"}

Path 3

Authenticated MPP

POST /api/v1/mpp/episodes

Authenticated user who wants to pay per episode instead of subscribing. Creates an Episode attached to the user's primary podcast. Both credentials are required.

Authorization:
Bearer <token>,
Payment <credential>

Returns {"id":"ep_…"}

Request reference

Each of the three POST endpoints accepts the same source-and-metadata parameters. The differences are which Authorization scheme is required and whether the voice param is honored.

POST /api/v1/episodes

Path 1 — Bearer-authenticated with active subscription or credits. The voice param is ignored; Episodes use the user's stored voice preference.

  • Parameter
    source_type
    Type
    string
    Required
    Yes
    Description
    One of "url", "text", "extension"
  • Parameter
    url
    Type
    string
    Required
    For url/extension
    Description
    URL of the article
  • Parameter
    text
    Type
    string
    Required
    For text
    Description
    Raw text to convert
  • Parameter
    content
    Type
    string
    Required
    For extension
    Description
    HTML of the article body, typically Readability-extracted (not full page, not already-sanitized plain text). The server runs its own extractor and truncates the result to 20,000 characters before synthesis.
  • Parameter
    title
    Type
    string
    Required
    No
    Description
    Episode title (defaults to "Untitled")
  • Parameter
    author
    Type
    string
    Required
    No
    Description
    Author name
  • Parameter
    description
    Type
    string
    Required
    No
    Description
    Episode description

POST /api/v1/mpp/narrations

Path 2 — anonymous, Payment credential only. The voice param selects which voice (and therefore which tier price) the 402 challenge quotes.

  • Parameter
    source_type
    Type
    string
    Required
    Yes
    Description
    One of "url", "text", "extension"
  • Parameter
    url
    Type
    string
    Required
    For url/extension
    Description
    URL of the article
  • Parameter
    text
    Type
    string
    Required
    For text
    Description
    Raw text to convert
  • Parameter
    content
    Type
    string
    Required
    For extension
    Description
    HTML of the article body, typically Readability-extracted (not full page, not already-sanitized plain text). The server runs its own extractor and truncates the result to 20,000 characters before synthesis.
  • Parameter
    title
    Type
    string
    Required
    No
    Description
    Episode title (defaults to "Untitled")
  • Parameter
    author
    Type
    string
    Required
    No
    Description
    Author name
  • Parameter
    description
    Type
    string
    Required
    No
    Description
    Episode description
  • Parameter
    voice
    Type
    string
    Required
    No
    Description
    Voice catalog key. Defaults to "felix" (Standard, 75¢). See Voices.

POST /api/v1/mpp/episodes

Path 3 — Bearer + Payment. An explicit voice param wins; otherwise the user's saved preference is used, falling back to the catalog default (felix, Standard).

  • Parameter
    source_type
    Type
    string
    Required
    Yes
    Description
    One of "url", "text", "extension"
  • Parameter
    url
    Type
    string
    Required
    For url/extension
    Description
    URL of the article
  • Parameter
    text
    Type
    string
    Required
    For text
    Description
    Raw text to convert
  • Parameter
    content
    Type
    string
    Required
    For extension
    Description
    HTML of the article body, typically Readability-extracted (not full page, not already-sanitized plain text). The server runs its own extractor and truncates the result to 20,000 characters before synthesis.
  • Parameter
    title
    Type
    string
    Required
    No
    Description
    Episode title (defaults to "Untitled")
  • Parameter
    author
    Type
    string
    Required
    No
    Description
    Author name
  • Parameter
    description
    Type
    string
    Required
    No
    Description
    Episode description
  • Parameter
    voice
    Type
    string
    Required
    No
    Description
    Voice catalog key. Overrides the user's stored voice preference for this request only. See Voices.

Voices

MPP callers may select a voice by passing a voice catalog key. All catalog voices are available on the MPP paths. Voice tier determines price: Standard voices cost 75¢, Premium voices cost $1.00. The default when voice is omitted is felix (British male, Standard, 75¢). Invalid keys return 422.

Voice resolution on MPP endpoints follows a three-step hierarchy: explicit voice param → user's saved preference (authenticated only) → catalog default (felix).

Key Name Accent Gender Tier Default
wren Wren British Female Standard
felix Felix British Male Standard
sloane Sloane American Female Standard
archer Archer American Male Standard
gemma Gemma British Female Standard
hugo Hugo British Male Standard
quinn Quinn American Female Standard
theo Theo American Male Standard
elara Elara British Female Premium
callum Callum British Male Premium
lark Lark American Female Premium
nash Nash American Male Premium

★ default when voice is omitted

Pricing

MPP pricing is tiered by voice. The resolved voice's tier determines the challenge amount issued in the 402 response.

  • Tier
    Standard
    Price (USD)
    $0.75
    Base units (pathUSD)
    750,000
    Voices
    wren, felix, sloane, archer, gemma, hugo, quinn, theo
  • Tier
    Premium
    Price (USD)
    $1.00
    Base units (pathUSD)
    1,000,000
    Voices
    elara, callum, lark, nash

The default voice when voice is omitted is felix (Standard), so the cheapest successful call costs $0.75. Requesting a Premium voice shifts the entire 402 → pay → retry flow to $1.00.

Token
0x20c0…0000
Decimals
6 (1 USD = 1,000,000)
Testnet RPC
rpc.testnet.tempo.xyz

The amount in the 402 response body is in cents (75 = $0.75, 100 = $1.00). The amount in the WWW-Authenticate header's request field is in token base units (750,000 for Standard, 1,000,000 for Premium).

A challenge is bound to a specific voice tier by an HMAC-signed request blob. A Standard-priced credential cannot be redeemed against a Premium voice (or vice versa) — the server re-issues a fresh 402 at the correct price when the tiers don't match. If narration processing fails after payment succeeds, the payment is automatically refunded.

Rate limits

  • Endpoint
    POST /api/v1/episodes
    Limit
    20
    Window
    1 hour
    Key
    Bearer token
  • Endpoint
    POST /api/v1/mpp/narrations
    Limit
    10
    Window
    1 minute
    Key
    IP address
  • Endpoint
    GET /api/v1/mpp/narrations/:id
    Limit
    60
    Window
    1 minute
    Key
    IP address

Throttled requests return 429 Too Many Requests with a Retry-After header.

POST /api/v1/mpp/episodes has no rack-level throttle at this time — the endpoint is Bearer-gated and payment-gated, so cost is the self-limit. A per-user throttle is on the roadmap.

The anonymous MPP create limit (10/minute per IP) exists to block abusers who churn through the 402-challenge flow without paying — every challenge costs a Stripe PaymentIntent and a pending DB row. A legitimate client making one paid narration burns at most 2 requests (initial 402, retry with credential), so 10/min leaves 5× headroom.

Error codes

  • Status
    201
    Meaning
    Created
    When
    Episode or narration created successfully
  • Status
    400
    Meaning
    Bad Request
    When
    Malformed JSON body or missing required headers
  • Status
    401
    Meaning
    Unauthorized
    When
    Bearer token is invalid, expired, or revoked
  • Status
    402
    Meaning
    Payment Required
    When
    See the callout above — either an MPP challenge or an upgrade prompt
  • Status
    403
    Meaning
    Forbidden
    When
    Authenticated user exists but lacks permission for the requested action
  • Status
    404
    Meaning
    Not Found
    When
    Narration (GET /api/v1/mpp/narrations/:id) does not exist or has expired past its 24h TTL
  • Status
    422
    Meaning
    Unprocessable Entity
    When
    Invalid or missing source_type, invalid voice, or content error
  • Status
    429
    Meaning
    Too Many Requests
    When
    Rate limit exceeded (see Rate limits); includes a Retry-After header
  • Status
    500
    Meaning
    Internal Server Error
    When
    Unexpected server failure (not caused by any of the above)
  • Status
    503
    Meaning
    Service Unavailable
    When
    Stripe failed to provision a deposit address, or Tempo RPC is unreachable

Environment

For self-hosting, the following environment variables configure MPP. Stripe must also be configured (standard STRIPE_SECRET_KEY etc.) since deposit addresses are provisioned via Stripe's crypto PaymentIntent API.

  • Variable
    MPP_SECRET_KEY
    Default
    random hex
    Description
    HMAC key for signing challenges and receipts. Must be stable across deploys.
  • Variable
    MPP_PRICE_STANDARD_CENTS
    Default
    75
    Description
    Price for a Standard-voice narration, in USD cents
  • Variable
    MPP_PRICE_PREMIUM_CENTS
    Default
    100
    Description
    Price for a Premium-voice narration, in USD cents
  • Variable
    MPP_CURRENCY
    Default
    usd
    Description
    Currency code
  • Variable
    MPP_CHALLENGE_TTL_SECONDS
    Default
    300
    Description
    How long a 402 challenge is valid
  • Variable
    MPP_STRIPE_API_VERSION
    Default
    2026-03-04.preview
    Description
    Stripe API version pinned to MPP preview channel
  • Variable
    TEMPO_RPC_URL
    Default
    rpc.testnet.tempo.xyz
    Description
    Tempo JSON-RPC endpoint
  • Variable
    TEMPO_CURRENCY_TOKEN
    Default
    0x20c0…0000
    Description
    pathUSD token contract address
  • Variable
    TEMPO_TOKEN_DECIMALS
    Default
    6
    Description
    Decimal places for the stablecoin
  • Variable
    TEMPO_RPC_OPEN_TIMEOUT_SECONDS
    Default
    5
    Description
    TCP connect timeout
  • Variable
    TEMPO_RPC_READ_TIMEOUT_SECONDS
    Default
    10
    Description
    Read timeout
  • Variable
    APP_HOST
    Default
    localhost:3000
    Description
    Used as the realm in challenges

Start listening free

2 episodes/month, no credit card required

By signing up, you agree to our Terms and Privacy Policy.