Docsend to PDF API Documentation

This API allows you to convert Docsend documents to downloadable PDFs programmatically.

API Endpoint

POST https://docsend2pdf.com/api/convert

Request Headers

Content-Type: application/json

Request Body

{
  "url": "https://docsend.com/view/abcdefg",       // Required
  "email": "user@example.com",                     // Optional; required for email-gated and email-verified documents
  "passcode": "document-password",                 // Optional, for password-protected documents
  "async": true,                                   // Optional; omit to wait ~25 s for the PDF, true = 202 at once, false = wait up to 5 min
  "searchable": false                              // Deprecated - always false
}

Response

By default the request waits up to about 25 seconds for the conversion. Most documents finish inside that window and the PDF comes back on the POST; otherwise you receive a task to poll:

  • 200 with Content-Type: application/pdf - the conversion finished; the body is the PDF (with Content-Disposition: attachment; filename=*.pdf)
  • 202 with JSON - the conversion is still running (or you sent "async": true):
{
  "task_id": "1c0e4f6a-...",
  "status": "processing",                 // last status seen; "queued" with "async": true
  "poll_url": "/api/task/1c0e4f6a-..."
}

Poll GET https://docsend2pdf.com/api/task/{task_id} every 1-2 seconds:

  • 200 JSON with status: "queued" or "processing" - keep polling
  • 200 with Content-Type: application/pdf - done; the result stays available for about 5 minutes after completion, so retried downloads are safe
  • 200 JSON with status: "email_verification_required" - see below
  • 400 JSON - the conversion failed (see error format below)
  • 404 JSON - unknown or expired task (details.error_code: "storage_not_found"); start a new conversion

Choosing how long to wait:

  • "async" omitted - wait up to about 25 seconds, then 202 with a poll_url if the conversion is still running. Always check the Content-Type of the POST response.
  • "async": true - return 202 as soon as the task is accepted. Best for callers with their own job queue.
  • "async": false - stay open until the PDF, a verification handoff, or an error is returned (up to 5 minutes; 504 if the window runs out).

The API also includes rate limiting headers:

  • X-RateLimit-Limit: Maximum requests allowed in the time window
  • X-RateLimit-Remaining: Remaining requests in the current time window
  • X-RateLimit-Reset: Timestamp when the rate limit window resets

Email-verified documents: some documents make DocSend email a verification link to the address you supplied. The task status (or, when the handoff happens inside the POST's wait, the POST response itself as a 202) then reports JSON instead of a PDF, so always check the Content-Type response header before treating a 2xx response as a PDF:

{
  "status": "email_verification_required",
  "session_id": "3f9a...",
  "message": "Please check your email to verify access",
  "details": { "email": "user@example.com", "document_owner": "..." },
  "verify_url": "/api/verify-email",
  "verify_method": "POST",
  "verify_payload": {
    "session_id": "3f9a...",
    "verification_url": "<link behind the 'Verify email address' button in the DocSend email>"
  },
  "expires_at": "2026-09-26T09:00:00Z",
  "expires_in_seconds": 86400,
  "next_step": "..."
}

To finish, have the inbox owner copy the link behind the "Verify email address" button in the DocSend email without opening it (the link is single-use) and POST it within 24 hours:

POST https://docsend2pdf.com/api/verify-email
Content-Type: application/json

{ "session_id": "3f9a...", "verification_url": "https://track.pstmrk.it/3s/..." }

// 202
{
  "status": "processing",
  "task_id": "7c1d...",
  "poll_url": "/api/task/7c1d...",
  "message": "Processing verified document",
  "session_id": "7c1d...",           // legacy: same value as task_id
  "details": { "task_id": "7c1d..." }
}

Poll poll_url like any other task. Repeating the POST with the same link returns the same task. 400 means the pasted link is not a verification link. The button link normally starts with https://track.pstmrk.it/ (or https://docsend.com/presentation_users/); a docsend.com/view/… address copied after the link was opened is answered with details.error_code: "opened_verification_link" and details.recovery_action: "restart_verification", because opening the link used it up. 404 means the session expired or was already used, so start a new conversion. If the request had no email, a verification-gated document fails with details.verification_required: true instead; there is no inbox to hand off to.

Error Responses

Every error from /api/convert, /api/task/{task_id} and /api/verify-email is JSON in this one shape, with the HTTP status repeated in errorCode:

{
  "error": "Technical error message",
  "user_message": "Human-friendly message suitable for display",
  "details": {
    "error_code": "document_not_found",   // stable machine-readable reason
    "retryable": false                    // whether the same request may succeed later
  },
  "errorCode": 400
}

user_message is always safe to show to end users; error may contain technical detail intended for logs. Branch on details.error_code rather than on message text, and use details.retryable to decide whether to resubmit: false means the link, document, or credentials themselves are the problem. Codes you will see most often:

  • Not retryable: document_not_found, link_broken, space_access_required, space_document_required, space_unavailable (a Space link that cannot be resolved to one document; send the individual document's link), missing_email (email-gated document, no email supplied), auth_rejected (wrong or missing passcode), link_expired, opened_verification_link, invalid_verification_link, invalid_request
  • Retryable: rate_limited, timeout, network_error, pdf_integrity, download_failed, storage_unavailable, storage_not_found (task unknown or expired; submit a new conversion), internal_error, unknown

New codes may be added over time; treat an unrecognised code as unknown and rely on retryable.

Common error status codes:

  • 400 - Bad Request (missing or invalid parameters, or the conversion failed: the document could not be accessed or converted)
  • 404 - Not Found (unknown or expired conversion task, or expired verification session)
  • 429 - Too Many Requests (rate limit exceeded)
  • 500 - Internal Server Error
  • 504 - Gateway Timeout (conversion exceeded the 5-minute limit)

Rate Limits

To ensure fair usage and service stability, the following rate limits apply:

  • Maximum 5 requests per second per IP address

If you exceed this limit, requests will be rejected with a 429 status code and include a Retry-After header indicating when you can retry.

Example Usage

cURL

# Default: waits up to ~25 s. Usually the PDF; a 202 JSON task if it is still running
curl -s -X POST https://docsend2pdf.com/api/convert \
  -H "Content-Type: application/json" \
  -d '{"url": "https://docsend.com/view/abcdefg"}' \
  --output response.bin -w '%{http_code} %{content_type}\n'
# => 200 application/pdf                (response.bin is the PDF)
# => 202 application/json               {"task_id": "...", "status": "processing", "poll_url": "/api/task/..."}

# Queue only - 202 immediately, then poll every second or two until the response is the PDF itself
curl -s -X POST https://docsend2pdf.com/api/convert \
  -H "Content-Type: application/json" \
  -d '{"url": "https://docsend.com/view/abcdefg", "async": true}'
curl -sL --output document.pdf https://docsend2pdf.com/api/task/TASK_ID

# Blocking mode - one request that waits for the PDF (up to 5 minutes)
curl -L -X POST https://docsend2pdf.com/api/convert \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://docsend.com/view/abcdefg",
    "passcode": "document-password",
    "async": false
  }' \
  --output document.pdf

JavaScript/Node.js

// Using fetch (browser or Node.js 18+)
async function convertDocsend(url) {
  const start = await fetch('https://docsend2pdf.com/api/convert', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ url })
  });

  if (!start.ok) {
    if (start.status === 429) {
      const retryAfter = start.headers.get('Retry-After');
      console.error(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
      return null;
    }
    const errorData = await start.json();
    const { error_code, retryable } = errorData.details || {};
    console.error('API Error:', errorData.user_message || errorData.error, { error_code, retryable });
    return null;
  }

  // The default wait usually returns the PDF on the POST itself
  if ((start.headers.get('content-type') || '').includes('application/pdf')) {
    return await start.blob();
  }

  const started = await start.json();
  if (started.status === 'email_verification_required') {
    // See "Email-verified documents" above; the handoff can arrive here too.
    console.error(started.next_step);
    return null;
  }

  const { task_id } = started;
  const deadline = Date.now() + 5 * 60 * 1000;

  while (Date.now() < deadline) {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    const poll = await fetch(`https://docsend2pdf.com/api/task/${task_id}`);

    if (poll.ok && (poll.headers.get('content-type') || '').includes('application/pdf')) {
      return await poll.blob();
    }

    const data = await poll.json();
    if (!poll.ok || data.status === 'failed') {
      console.error('API Error:', data.user_message || data.error);
      return null;
    }
    if (data.status === 'email_verification_required') {
      // Ask the inbox owner for the emailed link, then POST data.verify_payload
      // (with the link as verification_url) to data.verify_url and poll the
      // returned poll_url. See "Email-verified documents" above.
      console.error(data.next_step);
      return null;
    }
    // queued / processing - keep polling
  }

  console.error('Conversion timed out');
  return null;
}

// Example usage
const pdfBlob = await convertDocsend('https://docsend.com/view/abcdefg');
if (pdfBlob) {
  // Do something with the PDF blob
  // e.g., save to file, display in browser, etc.
}

Python

import time

import requests

def convert_docsend(url, email=None, passcode=None):
    """
    Convert a Docsend document to PDF, polling until it completes.

    Args:
        url (str): The DocSend URL
        email (str, optional): Email for email-gated documents
        passcode (str, optional): Password for password-protected documents

    Returns:
        bytes: The PDF file content or None if conversion failed
    """
    api = 'https://docsend2pdf.com'

    payload = {'url': url}
    if email:
        payload['email'] = email
    if passcode:
        payload['passcode'] = passcode

    # The default wait keeps the POST open for up to ~25 s
    start = requests.post(f'{api}/api/convert', json=payload, timeout=60)

    if not start.ok:
        if start.status_code == 429:
            retry_after = start.headers.get('Retry-After')
            print(f"Rate limit exceeded. Retry after {retry_after} seconds")
            return None
        try:
            error_data = start.json()
            message = error_data.get('user_message') or error_data.get('error', 'Unknown error')
            details = error_data.get('details') or {}
            print(f"API Error: {message} (code={details.get('error_code')}, retryable={details.get('retryable')})")
        except ValueError:
            print(f"API Error: {start.status_code} - {start.text}")
        return None

    # The default wait usually returns the PDF on the POST itself
    if 'application/pdf' in start.headers.get('Content-Type', ''):
        return start.content

    started = start.json()
    if started.get('status') == 'email_verification_required':
        # See "Email-verified documents" above; the handoff can arrive here too.
        print(started.get('next_step'))
        return None

    task_id = started['task_id']
    deadline = time.time() + 300

    while time.time() < deadline:
        time.sleep(1)
        poll = requests.get(f'{api}/api/task/{task_id}', timeout=30)

        if poll.ok and 'application/pdf' in poll.headers.get('Content-Type', ''):
            return poll.content

        data = poll.json()
        if not poll.ok or data.get('status') == 'failed':
            message = data.get('user_message') or data.get('error', 'Unknown error')
            print(f"API Error: {message}")
            return None
        if data.get('status') == 'email_verification_required':
            # Ask the inbox owner for the emailed link, then POST
            # data['verify_payload'] (with the link as verification_url) to
            # data['verify_url'] and poll the returned poll_url.
            print(data.get('next_step'))
            return None
        # queued / processing - keep polling

    print('Conversion timed out')
    return None

# Example usage
pdf_data = convert_docsend('https://docsend.com/view/abcdefg')
if pdf_data:
    # Save the PDF to a file
    with open('document.pdf', 'wb') as f:
        f.write(pdf_data)

Back to Home