Skip to main content

Documentation Index

Fetch the complete documentation index at: https://markdown2pdf.ai/llms.txt

Use this file to discover all available pages before exploring further.

Rather than using the SDKs we provide, you can also use the REST API directly using regular HTTP requests. This is useful if you want to integrate the markdown to PDF conversion into your own applications or scripts without relying on a specific programming language SDK. The overall process is similar to using the SDKs, but you will need to handle the HTTP requests and responses yourself. The POST /markdown endpoint accepts both L402 (Lightning) and X402 (USDC on Solana) payments — the 402 response advertises both offers and the client picks. Below is a step-by-step guide on how to use the REST API directly:
1

/markdown endpoint

Call the markdown endpoint with your markdown content. If a payment is required, you receive a 402 whose body contains an offers[] array. Expect two entries: one with payment_methods: ["lightning"] (L402) and one with payment_methods: ["x402"]. The X-Accept-Payment response header advertises the same list. Pick whichever rail your agent has a wallet for.
2

Pay — L402 (Lightning)

For the L402 offer, call /payment_request with the payment_context_token and offer_id to fetch a BOLT11 Lightning invoice, pay it, then re-submit the same POST /markdown body.
3

Pay — X402 (USDC on Solana)

For the X402 offer, read the native accepts[] payload from offers[].metadata.x402 (or call /payment_request/x402). Sign an X-PAYMENT base64 header using any x402-aware signer, then re-POST /markdown with that header. The server verifies + settles via the facilitator and returns 200 with X-Payment-Response carrying the settlement proof.
4

/job/{job_id}/status endpoint

Call the status endpoint repeatedly to check the status of your document generation job. It will return the current status and, once complete, provide a URL to download the generated PDF.
5

/job/{job_id}/output endpoint

Call the output endpoint to retrieve the URL from which you can download the generated PDF once the job is complete.
Some sample python code is provided below to help you get started with the REST API directly. The example picks the L402 (Lightning) offer; see the X402 page for an x402-native variant.
import httpx
import time
from datetime import datetime
from urllib.parse import urljoin

DEFAULT_API_URL = "https://api.markdown2pdf.ai"
POLL_INTERVAL = 3
MAX_DOC_GENERATION_POLLS = 10

def build_url(path, base_url):
    if path.startswith("http://") or path.startswith("https://"):
        return path
    return urljoin(base_url, path)

def pay(offer):
    print("⚡ Lightning payment required")
    print(f"Amount: {offer['amount']} {offer['currency']}")
    print(f"Description: {offer['description']}")
    print(f"Invoice: {offer['payment_request']}")
    input("Press Enter once paid...")

def convert(markdown, title="Markdown2PDF.ai converted document", date=None, download_path=None, return_bytes=False, on_payment_request=pay, api_url=DEFAULT_API_URL):
    if not date:
        dt = datetime.now()
        date = f"{dt.day} {dt.strftime('%B %Y')}"

    payload = {
        "data": {
            "text_body": markdown,
            "meta": {
                "title": title,
                "date": date,
            }
        },
        "options": {
            "document_name": "converted.pdf"
        }
    }

    with httpx.Client() as client:
        while True:
            print("Sending initial request to convert markdown...")
            response = client.post(f"{api_url}/markdown", json=payload)

            if response.status_code == 402:
                print("Received 402 Payment Required response, fetching payment offer...")
                l402_offer = response.json()
                # Select the Lightning offer explicitly. The 402 body may also list an
                # x402 offer (payment_methods=["x402"]) — see /x402 for that flow.
                offer_data = next(
                    o for o in l402_offer["offers"] if "lightning" in o["payment_methods"]
                )
                offer = {
                    "offer_id": offer_data["id"],
                    "amount": offer_data["amount"],
                    "currency": offer_data["currency"],
                    "description": offer_data.get("description", ""),
                    "payment_context_token": l402_offer["payment_context_token"],
                    "payment_request_url": l402_offer["payment_request_url"]
                }

                invoice_resp = client.post(offer["payment_request_url"], json={
                    "offer_id": offer["offer_id"],
                    "payment_context_token": offer["payment_context_token"],
                    "payment_method": "lightning"
                })

                if not invoice_resp.is_success:
                    raise Exception(f"Failed to fetch invoice: {invoice_resp.status_code}")

                invoice_data = invoice_resp.json()
                offer["payment_request"] = invoice_data["payment_request"]["payment_request"]

                if not on_payment_request:
                    raise Exception("Payment required but no handler provided.")

                print("Prompting for payment...")
                on_payment_request(offer)

                time.sleep(POLL_INTERVAL)
                continue

            if not response.is_success:
                raise Exception(f"Initial request failed: {response.status_code}, {response.text}")

            response_data = response.json()
            path = response_data["path"]
            break

        status_url = build_url(path, api_url)
        attempt = 0
        print("Polling for document generation status...")
        while attempt < MAX_DOC_GENERATION_POLLS:
            poll_resp = client.get(status_url)
            if poll_resp.status_code != 200:
                raise Exception(f"Polling error (status {poll_resp.status_code})")

            poll_data = poll_resp.json()
            if poll_data.get("status") == "Done":
                final_metadata_url = poll_data.get("path")
                if not final_metadata_url:
                    raise Exception("Missing 'path' field pointing to final metadata.")

                metadata_resp = client.get(final_metadata_url)
                if not metadata_resp.is_success:
                    raise Exception("Failed to retrieve metadata at final path.")

                final_data = metadata_resp.json()
                if "url" not in final_data:
                    raise Exception("Missing final download URL in metadata response.")

                final_download_url = final_data["url"]
                break

            time.sleep(POLL_INTERVAL)
            attempt += 1
        else:
            raise Exception(f"Polling exceeded max attempts ({MAX_DOC_GENERATION_POLLS}) without completion.")

        print("Downloading final PDF...")
        pdf_resp = client.get(final_download_url)
        if not pdf_resp.is_success:
            raise Exception("Failed to download final PDF.")

        pdf_content = pdf_resp.content

    if return_bytes:
        return pdf_content

    if download_path:
        with open(download_path, "wb") as f:
            f.write(pdf_content)
        return download_path

    return final_download_url

# Example usage
if __name__ == "__main__":
    url = convert(
        markdown="# Hello markdown2pdf REST APIs",
        title="My document title",
        date="5th June 2025"
    )
    print("PDF URL:", url)