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.
| Field | Type | Required | Description |
|---|---|---|---|
useCase | string[] | Yes (non-empty) | What you'd use PDFops for. Free-text array; conventional values are invoices, contracts, filled-forms, reports, receipts, merging, other. |
currentSolution | string[] | Yes (non-empty) | How you handle PDFs today. Conventional values: self-hosted-lib, hosted-paid, in-house, none. |
expectedVolume | string | Yes (non-empty) | Monthly PDF volume bucket. Conventional values: lt-1k, 1k-10k, 10k-100k, 100k-plus. |
payRange | string | Yes (non-empty) | What you'd pay per month at your expected volume. Conventional values: free-only, lt-20, 20-100, 100-500, 500-plus. |
email | string | No | Optional contact email. If omitted, the waitlist row is anonymous (aggregate signal only, no follow-up channel). |
message | string | No | Free-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 code | Status | Trigger |
|---|---|---|
invalid_json | 400 | Request body wasn't valid JSON. |
invalid_body | 400 | Body decoded to null, an array, or a non-object scalar. |
invalid_useCase | 400 | useCase missing, not an array, or array of non-strings or empty strings. |
invalid_currentSolution | 400 | currentSolution missing, not an array, or array of non-strings or empty strings. |
invalid_expectedVolume | 400 | expectedVolume missing or not a non-empty string. |
invalid_payRange | 400 | payRange missing or not a non-empty string. |
invalid_email | 400 | email 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
- Anonymous submissions are accepted. Omit
emailand the waitlist row contributes to aggregate signal only. - IP is hashed. The client IP from the edge is hashed (SHA-256) and stored as
ip_hash. Used for rate-limiting and de-duplication; not reversible. - Message is the highest-signal field. Aggregate counts of
useCase+payRangeare noisy; specific notes about volume / blocking constraints / missing endpoints drive what ships next. - No webhook back. Submissions are stored to Turso + fire a Discord notification on the operator side; you won't receive a callback. When higher-quota keys ship, contact happens via the email you provided.
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.