Endpoint reference

POST /api/waitlist

Submit a waitlist entry for higher-quota PDFops keys (1k–10k requests/month, free during beta). Aggregate signal drives endpoint prioritization and post-beta pricing.

Request

Method: POST. URL: https://pdfops.dev/api/waitlist. Body: application/json.

FieldTypeRequiredDescription
useCasestring[]Yes (non-empty)What you'd use PDFops for. Free-text array; conventional values are invoices, contracts, filled-forms, reports, receipts, merging, other.
currentSolutionstring[]Yes (non-empty)How you handle PDFs today. Conventional values: self-hosted-lib, hosted-paid, in-house, none.
expectedVolumestringYes (non-empty)Monthly PDF volume bucket. Conventional values: lt-1k, 1k-10k, 10k-100k, 100k-plus.
payRangestringYes (non-empty)What you'd pay per month at your expected volume. Conventional values: free-only, lt-20, 20-100, 100-500, 500-plus.
emailstringNoOptional contact email. If omitted, the waitlist row is anonymous (aggregate signal only, no follow-up channel).
messagestringNoFree-text. Specific volume requirements, missing endpoints, integration constraints. Highest-signal field for influencing the roadmap.

Response

On success: 200 OK, Content-Type: application/json, body is { "ok": true }.

On validation failure: 400 Bad Request, Content-Type: application/json, body is { "error": "<code>", "details": "<explanation>" }.

Error codes

Error codeStatusTrigger
invalid_json400Request body wasn't valid JSON.
invalid_body400Body decoded to null, an array, or a non-object scalar.
invalid_useCase400useCase missing, not an array, or array of non-strings or empty strings.
invalid_currentSolution400currentSolution missing, not an array, or array of non-strings or empty strings.
invalid_expectedVolume400expectedVolume missing or not a non-empty string.
invalid_payRange400payRange missing or not a non-empty string.
invalid_email400email was present but didn't match a basic email-shape regex.

Example — curl

curl -X POST https://pdfops.dev/api/waitlist \
  -H "Content-Type: application/json" \
  -d '{
    "useCase":         ["invoices","contracts"],
    "currentSolution": ["hosted-paid"],
    "expectedVolume":  "1k-10k",
    "payRange":        "20-100",
    "email":           "you@example.com",
    "message":         "Need this in a Stripe webhook handler within 2 weeks."
  }'

Example — fetch

const res = await fetch('https://pdfops.dev/api/waitlist', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    useCase:         ['invoices', 'contracts'],
    currentSolution: ['hosted-paid'],
    expectedVolume:  '1k-10k',
    payRange:        '20-100',
    email:           'you@example.com',
    message:         'Need this in a Stripe webhook handler within 2 weeks.',
  }),
});
const json = await res.json();
// json === { ok: true } on success

Code examples — other stacks

The fetch example above is the canonical JS/TS form. Equivalents in Python and Go:

Python (3.10+) — httpx
import httpx

payload = {
    "useCase":         ["invoices", "contracts"],
    "currentSolution": ["hosted-paid"],
    "expectedVolume":  "1k-10k",
    "payRange":        "20-100",
    "email":           "you@example.com",
    "message":         "Need this in a Stripe webhook handler within 2 weeks.",
}

r = httpx.post(
    "https://pdfops.dev/api/waitlist",
    json=payload,
    timeout=10,
)
if r.status_code != 200:
    err = r.json()
    raise RuntimeError(f"PDFops waitlist failed: {err['error']} — {err['details']}")
Go (1.21+) — net/http
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

type WaitlistEntry struct {
    UseCase         []string `json:"useCase"`
    CurrentSolution []string `json:"currentSolution"`
    ExpectedVolume  string   `json:"expectedVolume"`
    PayRange        string   `json:"payRange"`
    Email           string   `json:"email,omitempty"`
    Message         string   `json:"message,omitempty"`
}

func submit() error {
    entry := WaitlistEntry{
        UseCase:         []string{"invoices", "contracts"},
        CurrentSolution: []string{"hosted-paid"},
        ExpectedVolume:  "1k-10k",
        PayRange:        "20-100",
        Email:           "you@example.com",
        Message:         "Need this in a Stripe webhook handler within 2 weeks.",
    }
    body, _ := json.Marshal(entry)

    resp, err := http.Post(
        "https://pdfops.dev/api/waitlist",
        "application/json",
        bytes.NewReader(body),
    )
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        var apiErr struct {
            Error   string `json:"error"`
            Details string `json:"details"`
        }
        json.NewDecoder(resp.Body).Decode(&apiErr)
        return fmt.Errorf("waitlist failed: %s — %s", apiErr.Error, apiErr.Details)
    }
    return nil
}

Behavioral notes

FAQ

Do I need to submit via the form, or can I post JSON directly?

Either works. The landing-page form at https://pdfops.dev/#waitlist posts the same JSON shape. Posting directly is useful for programmatic submission.

Are use case and current solution free-text or enums?

Free-text arrays at the API level. The landing-page form sends conventional values, but any strings work. Aggregated post-hoc with json_each on the read side.

What goes in the message field?

Free text. Specific volume requirements, missing endpoints, integration constraints. Highest-signal input for influencing the roadmap during the discovery window.

Is the email field required?

Optional at the API. Anonymous submissions contribute to aggregate signal but there's no way to follow up.

Is the IP address stored?

Hashed (SHA-256) at the edge; only the hash lands in storage. Used for rate-limiting and de-duplication.

Common error: invalid_email

The email field is present but doesn't match a basic email-shape regex (the validator looks for something like user@domain.tld — intentionally loose, not RFC-strict). Fix: omit the field entirely if you don't have one (anonymous submissions are accepted), or supply a real address. Common gotchas: sending an empty string (use null or omit), sending the literal word "undefined" from a JS client that didn't coerce a missing value, or sending an array (the field is a single string).

Common error: invalid_useCase (or any invalid_*)

One of the required fields is missing, the wrong type, or empty. Specifically: useCase and currentSolution must be non-empty arrays of non-empty strings; expectedVolume and payRange must be non-empty strings. The error field tells you exactly which validator failed (e.g., invalid_useCase, invalid_payRange). Most-common cause from client code: forgetting to wrap a single value as an array ("invoices" vs ["invoices"]); the API explicitly requires arrays so the read-side json_each aggregation works.

Common error: invalid_json / invalid_body

invalid_json means the request body didn't parse as JSON — check your Content-Type: application/json header and that you're sending a JSON string body, not a form-encoded body. invalid_body means the body parsed but decoded to null, an array, or a primitive scalar; the endpoint requires a JSON object. Common gotcha: some HTTP clients (especially older Python requests) default to form-encoding unless you explicitly pass json=payload instead of data=payload.