Use cases
URL features for your customers
Your users paste links; your app reads, previews and imports them. The per-tenant accounting and the safe retries are built in.
By Roger Campos · Last updated: September 2026
TL;DR
To embed URL features in a multi-tenant SaaS, send every request with your tenant's id as a label, retry with an Idempotency-Key so a timeout never bills twice, and keep one project per environment. URLpipe returns the label with every result, totals credits per label each month, and processes pages in the EU on every plan.
Free plan, no credit card. 1,000 credits a month.
The job
What changes when the URLs belong to your customers
A SaaS that reads links for its users — a CRM that imports a company's website, a knowledge tool that ingests help-centre pages, a newsletter app that unfurls every link — has problems a single-tenant script does not. You need to know which tenant spent what. A retried job must not charge twice. A European customer's procurement team will ask where the pages are processed. And one tenant's burst must not starve the others.
Design
Four things to wire in
A label per tenant
Send {"tenant": "t_8812", "feature": "import"} as labels. They come back in the webhook, so a result routes to its tenant without a lookup, and the project's Labels page totals credits per tenant per calendar month — the number you need to pass costs on, or to spot the tenant whose usage doesn't match their plan. Use ids, not names or emails: labels appear in your dashboard and webhooks.
Retries that never bill twice
Your job runner will retry. Send an Idempotency-Key header derived from your job id, and a resend within 24 hours returns the first request's token and result — one charge, one webhook. An identical request that arrives while the first is still running waits for it, for free, with or without a key. See retries & duplicates.
One project per environment
Each project has its own API key, webhook endpoint, signing secret, robots.txt setting and history. Keep production and staging apart, so a test run never shows up on a tenant's usage. Cached results are shared across your organization's projects, so staging reading a page production already fetched is a free hit.
EU processing
Pages are fetched, rendered and stored in Europe on every plan. /markdown and /html never call a model, so they never leave it. Switch the organization to EU processing and the three AI operations — /meta, /summarize, /keywords — run on EU-only providers too, at no extra charge.
Flow
A tenant's import, end to end
- 1A tenant pastes a URL. Your app enqueues a job with its own id.
- 2The worker posts the request async, with the tenant label,
Idempotency-Keyset to the job id, and your webhook asreport_to. - 3If the worker crashes and the job runs again, the same key returns the same token — nothing new runs.
- 4The signed webhook arrives with the labels; your handler writes the result to the tenant's record.
import os, requests
def import_page(job_id: str, tenant_id: str, url: str) -> str:
res = requests.post("https://urlpipe.dev/scrape",
headers={
"Authorization": f"Bearer {os.environ['URLPIPE_API_KEY']}",
"Idempotency-Key": f"import-{job_id}",
},
json={
"url": url,
"operations": ["markdown", "meta"],
"report_to": "https://app.example.com/webhooks/urlpipe",
"labels": {"tenant": tenant_id, "feature": "import"},
}, timeout=30)
if res.status_code == 429: # rate_limited or concurrency_limit
raise RetryLater(res.json()["error"])
res.raise_for_status()
return res.json()["token"]export async function importPage(jobId, tenantId, url) {
const res = await fetch("https://urlpipe.dev/scrape", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.URLPIPE_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `import-${jobId}`,
},
body: JSON.stringify({
url,
operations: ["markdown", "meta"],
report_to: "https://app.example.com/webhooks/urlpipe",
labels: { tenant: tenantId, feature: "import" },
}),
});
if (res.status === 429) {
// rate_limited or concurrency_limit: back off and let the queue retry
throw new RetryLater((await res.json()).error);
}
if (!res.ok) throw new Error(`URLpipe ${res.status}`);
return (await res.json()).token;
}curl -X POST https://urlpipe.dev/scrape \
-H "Authorization: Bearer $URLPIPE_API_KEY" \
-H "Idempotency-Key: import-job-58213" \
-H "Content-Type: application/json" \
-d '{
"url": "https://customer-site.example/about",
"operations": ["markdown", "meta"],
"report_to": "https://app.example.com/webhooks/urlpipe",
"labels": {"tenant": "t_8812", "feature": "import"}
}'Throughput is shared: 60 requests a minute and 15 per 10 seconds per project on every plan, and your plan's parallel limit — 8 on Pro, 20 on Scale — across all of them. Put a queue with per-tenant fairness in front, and treat 429 as "try again shortly": the error field says whether it was the rate, the parallel limit or the month's credits.
Credits
What it costs
Four hundred tenants pasting a hundred links each a month, read and carded, at list price:
| What | A month | Credits each | Credits |
|---|---|---|---|
| Links your users paste, read as Markdown | 40,000 | 1 | 40,000 |
| The same links' metadata, for the card | 40,000 | 5 | 200,000 |
| Repeats inside max_age (cache hits) | 25,000 | 0 | 0 |
240,000 credits; the cheapest way to buy it is Scale ($149, 175,000 credits) plus 65,000 credits of overage at $1.50 per 1,000 credits — $246.50 a month. The cache row is the one to watch: tenants paste the same popular links, and every repeat inside max_age is free while each tenant still gets its own token and labels. Paid plans are never cut off mid-month — past the allowance, credits are billed at the overage rate on a separate invoice.
Limits
What URLpipe does not do for a multi-tenant app
- Sub-accounts or per-tenant keys. Tenants are labels on your requests, not accounts. Your users never see URLpipe.
- Per-tenant quotas. Usage is reported per label; enforcing a tenant's limit is your code.
- Invoice your tenants. The Labels page gives you the numbers; billing them stays in your app.
- Fetch private URLs. Your tenants' intranet pages and anything behind a login are out of reach — only public http(s) addresses.
Endpoints
The endpoints on this page
- Turn any URL into clean Markdown
Paste a link and get the page's main content as tidy Markdown — headings, lists, links and code kept, navigation and cookie banners stripped. The format LLMs and RAG pipelines work best with.
Try it free - Check a page's Open Graph tags and metadata
Paste a link and get the page's metadata — title, description, author, publication date, feed and main image — cleaned into one structured object.
Try it free - Get the rendered HTML of any URL
Paste a link and get the page's HTML after JavaScript has run and redirects have been followed — the DOM a real browser sees, not the empty shell curl returns.
Try it free
FAQ
Frequently asked questions
How do I see what each tenant used?
What happens if my worker retries a request?
Can I keep all processing in the EU?
Do two tenants fetching the same URL pay twice?
Do rate limits go up on bigger plans?
Build it on the free plan.
Free plan, no card. Confirm your email and your API key is live — you'll be making real requests in minutes.