Capturesinstant feedback on one URL
Capture one URL now, wait for it and summarize it in SQL.
Capture one URL for instant feedback (waits and summarizes); crawl for coverage (any list of URLs, optionally following links).
Choose the right resource
Periplus has four resources. Each answers one question:
| Resource | Use it when | What it does |
|---|---|---|
| Capture | You want instant feedback on one URL | Fetches it once, now; can wait and summarize it |
| Crawl | You want coverage: a longer list, or links from a starting point | Fetches any list of URLs, optionally following links within limits and rules |
| Pin | You need captures for longer than seven days | Keeps exact captures until a deadline |
| Schedule | You need the same pages again later | Captures a fixed URL list at an interval |
Every capture is kept for seven days after collection, then scheduled for deletion unless a pin keeps it.
Check coverage first
A capture always fetches the page again. It never returns an existing capture, even when the corpus already has a recent one. Query the corpus first and capture only what is missing or too old for your question.
SELECT url, max(observed_at) AS latest
FROM captures
WHERE url IN (?, ?)
GROUP BY urlCapture a URL
A capture request takes one HTTP(S) URL. It is fetched once, with no link following. Optionally choose to pin the capture for a number of days if it succeeds; pinning requires permission to create pins.
To look at several pages now, create one capture per URL; they run in parallel. For a list of URLs, a crawl without follow fetches exactly up to 1,000 URLs in one request. The API refuses a urls list on a capture with HTTP 422 validation_error (field code capture_one_url) and a message pointing to crawls; nothing is accepted, and an earlier accepted request with the same ID remains unchanged.
Capture completion and SQL availability are separate steps. A request is finished when the page is captured or has failed; it is queryable once its capture is in published SQL.
Two IDs: request and capture
A capture request names two things:
request_idis the work you ordered. Retry an uncertain create with it, poll it, cancel it. It is not an SQL key.capture_idis the capture the work produced: one observation of the page, which succeeded or failed. It is the key for SQL and for capture inspection, and it is null until the page is attempted. A failed capture keeps itscapture_id, has afailure, andqueryableis false: there is no page to query (failed captures never appear in the publiccapturesrelation). Requests for the same URL at the same time may share one capture, so two request IDs can show the samecapture_id.
The capture request view holds url, status, capture_id, queryable, failure (code, title, explanation and suggestion), capture_assessment (the capture service's own verdict), effective_url, http_status, waiting, finish_reason, pin_id, resolve_bot_challenges, project_id, name, description, version, created_at, finished_at and guidance. When the page's result cannot be read for a moment, result_unavailable says so: capture_id, failure and queryable are then unknown, not absent, and the guidance asks you to read the request again.
Wait and summarize
POST /captures takes two optional query parameters that shape only its response; neither is part of the request's identity, so a retry with the same ID and body returns the same request whatever they are.
wait_seconds(0–60, default 40) is the total time the server may spend on this call: it waits for the page to be captured and queryable first, then produces the view with whatever time remains. There is no fixed split. The capture is accepted either way;wait_seconds=0returns at once.view=summaryalso returnssummary: the same bounded summary of the captured page asGET /api/v1/captures/{capture_id}/summary, within 24,000 bytes (over it, sample rows are dropped and their SQL kept). When there is no summary,summary_reasonsays why: still running, not captured (seefailure), not yet queryable, orwait_secondsspent. The summary gets what remains ofwait_seconds(best effort: each statement is limited to the remaining time; a stalled server or slow database can extend it). It needsorganization:sql:exec; without it the request is refused before any work is accepted. A failed summary never hides the accepted request.
The API, the SDK and MCP capture_create share these defaults: wait_seconds=40 and no view. For instant feedback pass view=summary. In production a single capture took a median of about 12.8 s from start to queryable (p95 about 13.5 s), and about 1.1 s from finish to queryable, so the 40 s default usually returns the capture with its summary. If the page is still running, poll the request (GET /captures/{request_id}?wait_seconds=…, capture_status in MCP).
Follow a request
A capture request's status is queued, running, paused, done, or cancelled. When nothing is progressing, the request explains why, for example website pacing or a retry; any time shown is the earliest observed eligibility, not a promised start. A failed capture keeps its capture_id and has failure with a short explanation and suggestion; queryable is false. A request that has not attempted the page has no capture_id yet. Cancelling stops the request; a page already captured stays.
A bot_challenge failure describes this capture. It does not establish whether other pages on the host are accessible. Record the coverage gap and consider challenge resolution for the failed URL when that page matters to the task.
Captures are made from Japan. Regional storefronts and geo-aware sites may serve Japan-specific language, prices or assortment; note the capture region when comparing markets.
Bot challenges
Some websites show a bot check instead of the page; such pages fail with failure.code=bot_challenge. Captures, crawls and schedules can ask for paid bot-challenge resolution with resolve_bot_challenges: true (Resolve bot challenges in the app). It is off by default, and a request that does not ask never uses it, even when your organization allows it.
- Allowance. An organization Owner or Admin allows paid resolution in Settings → Budgets (
allow_challenge_resolution, off by default). A request asking for resolution without that allowance is refused with HTTP 403challenge_resolution_not_allowedand nothing is accepted. Without the allowance, the app shows the option disabled with this explanation. - Usage and consent. Each successfully resolved capture counts in usage as a challenge-resolution page, attributed to the request that asked for it. Customer charges are disabled during the research preview. Resolution can use paid external providers; agents should ask the user before turning it on.
- Rerun only what failed. Create a new capture of the failed URL (or a crawl without follow for several) with
resolve_bot_challenges: true. When a capture request, a crawl's captures or a schedule's captures report bot_challenge and the source did not ask for resolution,next_stepssuggest exactly that call (captures_createfor one URL,crawls_createfor several, with a stable newid). If the source already asked, the response advises checking execution with support before another paid retry. In the app, a bot_challenge failure links to a new capture with the option on. - Requested does not mean attempted. The flag permits resolution; it does not prove that a resolver ran. Capture-service account settings, allowance or availability can prevent solving. If an opted-in request still fails, contact support with the request ID so they can check what ran. A
bot_blockedresult reports a detected refusal for this capture; a plain HTTP 403 alone does not prove IP rejection, a failed resolver or that other pages on the site are inaccessible. Use the reported failure and attempt evidence, and disclose what remains unknown. - Part of the input. The flag is part of the request's identity: retrying an ID with a different value is a conflict (422). Requests with and without resolution never share or reuse each other's captures, so a resolved capture is never charged to, or served from, a request that did not ask.
- Withdrawal. If the allowance is withdrawn while a crawl is running, pages admitted afterwards run without resolution.
from periplus_sdk import Client
with Client() as client:
retry = client.captures.create(url="https://example.com/challenged", resolve_bot_challenges=True,
wait_seconds=30)Capture from a script
Supply your own stable UUID as id when you need to recover a create request after losing its response. The same ID with identical input returns the existing request; different input conflicts. An optional name and Markdown description only label the capture: they are not part of its input, so a retry keeps the first ones. Change them later with PATCH /captures/{id} (captures.update() in Python, or the capture's page in the app) and the view's version as expected_version. In the API, get with wait_seconds (0–60, default 0) returns as soon as the request is finished and queryable, or when the time runs out; create's wait_seconds (default 40) also covers the view.
Python
from periplus_sdk import Client
with Client() as client:
capture = client.captures.create(url="https://example.com/pricing", name="Pricing page",
description="Baseline before the **Q3** change.",
view="summary") # wait_seconds defaults to 40
if capture.capture_id is None and not capture.finished: # still running: poll the request
capture = client.captures.get(capture.request_id, wait_seconds=30)
print(capture.request_id, capture.status, capture.capture_id, capture.queryable)
print(capture.failure or capture.summary or capture.summary_reason)cURL
curl --fail-with-body "${PERIPLUS_API_URL:-https://api.periplus.dev}/captures?wait_seconds=40&view=summary" \
-H "Authorization: Bearer $PERIPLUS_API_KEY" \
-X POST \
-H 'Content-Type: application/json' \
--data '{"url":"https://example.com/pricing"}'Find earlier captures
Search the captures list by name, URL or site: each row shows the URL, its status and the capture ID. The box at the top of Captures matches any part of the name or of a capture URL, ignoring case, and combines with the project switcher and the active/finished filter. ⌘K also finds captures by name or URL. A search needs at least three consecutive letters or digits. To list only captures captured on one site, use host, an exact host where a leading www. is ignored (host=example.com also matches www.example.com). Search covers the name and the URL you supplied. Agents use capture_list with search or host.
from periplus_sdk import Client
with Client() as client:
for item in client.captures.list(search="pricing", finished=True).items:
print(item.request_id, item.url, item.capture_id)
by_site = client.captures.list(host="example.com")Projects
Every capture belongs to exactly one project, and projects are how you organize captures. Your organization starts with a Default Project, and a capture created without a project lands there, so nothing changes for work that never mentions projects. Create projects for separate pieces of work, such as one per product or customer, and keep each capture with the queries, pins and schedules around it. Projects organize your work, not your data: SQL reads every capture your organization can see, whichever project a query is in.
Choose a project with the switcher in the sidebar: Captures lists that project's captures, and new captures go into it. To move a capture, use Move… on its page, or select several and choose Move to project. Moving never changes what a capture does. Like the name, the project is presentation: a create retry with the same ID keeps the project it was first created with. From the API, pass project_id when creating a capture, or move it later with project_id and its current version as expected_version. Agents find project IDs with projects_list and pass project_id to capture_create and capture_list, or move captures with project_move_items; there is no active project, so name it on every call.
from periplus_sdk import Client
with Client() as client:
default_project = next(client.projects.iter()) # listed first
project = client.projects.create(name="Product A")
client.captures.create(url="https://example.com/pricing", project_id=project)
for capture in client.captures.list(project_id=default_project).items:
client.captures.move(capture, project_id=project)
print(client.projects.get(project).item_counts.captures)