Concepts
Jobs, batches & webhooks
Every scrape is a job underneath. The synchronous endpoints are a convenience wrapper that waits for a fast one, so nothing here is bolted onto a request/response design that cannot take it.
When to use which
- Synchronous GET: you need one thing and you need it now.
- Batch job: you have a list. One call, up to 500 targets, results as they land.
- Async job: one piece of work that will take longer than a request should. A full-channel pull, a deep comment tree.
- Subscription: you would otherwise run the same call on a cron. Do this instead.
| POST/v1/jobs | One long-running call, queued. Returns 202 with a job id. |
| POST/v1/jobs/batch | Up to 500 targets against one endpoint, in a single request. Also 202. |
| GET/v1/jobs/{jobId} | Status, per-target progress, and results as they land. |
| GET/v1/jobs | Your team’s 50 most recent jobs. |
| POST/v1/subscriptions | Watch a target and take a webhook when it changes. Returns 201. |
| GET/v1/subscriptions | What you are currently watching. |
| DELETE/v1/subscriptions/{subId} | Stop watching. |
Batches
A batch is one request that fans out across workers. There is no request timeout above it, so the size of the list is a throughput question rather than a design constraint.
curl -X POST "$API/v1/jobs/batch" \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{
"endpoint": "youtube.transcript",
"params": { "language": "en" },
"targets": [
{ "url": "8XkPqR2nLvE" },
{ "url": "dQw4w9WgXcQ" },
{ "url": "..." }
],
"webhook_url": "https://you.example/hooks/batch"
}'{
"success": true,
"data": {
"jobId": "job_01JB8Z3M6QW2K9",
"status": "queued",
"endpoint": "youtube.transcript",
"totalTargets": 412,
"estimatedCredits": 412,
"note": "Failed and empty targets are refunded automatically, and a target repeated within one batch is fetched and charged once.",
"pollUrl": "https://api.example/v1/jobs/job_01JB8Z3M6QW2K9"
}
}Each entry in targets is an object of parameters, not a bare string. It is merged over the shared params and validated against the endpoint’s own schema at creation, so a batch that is half-invalid is refused now rather than refunding half of itself later.
estimatedCredits is the worst case. The number that gets charged is lower whenever a target fails, returns nothing, or is served from cache. Each of those is refunded as the batch runs, not reconciled afterwards. The same target repeated inside one batch is fetched once and charged once.
{
"success": true,
"data": {
"jobId": "job_01JB8Z3M6QW2K9",
"status": "running",
"progress": { "total": 412, "completed": 287, "failed": 9, "percent": 70 },
"creditsCharged": 278
}
}In that example 287 targets have completed and 278 credits have been charged: nine of those completions came back empty and were refunded. The nine counted under failed are separate again, and cost nothing either. completed and failed never overlap, so a batch is done when they add up to total.
Subscriptions
A subscription is a standing instruction to watch one target. We run the checks; you get called when the data moves. Each check is a normal request at the endpoint’s price, and a check that fails or comes back empty is refunded like any other call. You pay for the checks, and you hear from us only when something changed. Creating, listing and deleting a watch cost nothing.
curl -X POST "$API/v1/subscriptions" \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{
"endpoint": "youtube.channel",
"params": { "handle": "@mkbhd" },
"webhook_url": "https://you.example/hooks/social",
"interval_seconds": 86400
}'interval_seconds runs from the endpoint’s minimum up to 604800, seven days. The minimum is set per endpoint: every operation in openapi.json publishes it as x-min-interval-seconds, and a shorter interval is refused with invalid_request naming that minimum. Endpoints that re-rank the same query on every request (search, trending, popular) are refused here rather than sold: every check would register as a change and call your webhook for nothing having happened.
Leave interval_seconds out and the watch uses the endpoint’s suggested interval, published as x-suggested-interval-seconds. It follows how often that kind of data moves: prices, stock and news hourly; new posts and comments every six hours; profiles and companies daily; music catalogues and transcripts weekly. It is a suggestion, not a limit, and any interval in range is accepted. It matters because every check is billed, so checking data more often than it moves costs money for nothing.
A team has a limit on how often its watches check in total, per hour. It is weighted by interval rather than counting watches, so an hourly watch uses more of it than a daily one. A create past that limit is refused, and the error states the limit. Paused watches count toward the limit, since they resume on their own; watches stopped after repeated failures do not.
Delivery
- Every delivery carries
x-webhook-signatureandx-webhook-timestamp, plusx-correlation-id, the id of the watch that fired. - Failed deliveries retry with backoff.
- A delivery that never succeeds is given up on and recorded on our side. There is no queue you can drain later, so treat a webhook as a notification and not as the record; the job or watch can always be read back over the API.
- Your endpoint should return quickly and do the work afterwards.
Idempotency
Delivery is at-least-once, so you will occasionally see the same change twice. There is no per-event id to deduplicate on. x-correlation-id identifies the watch, not the delivery, and is the same on every one it sends. Deduplicate on something from the payload instead: for a subscription.changed, the data it carries is the changed object, so its own id and timestamp are the stable pair to key against.
When your balance cannot cover a check
A check is charged before it fetches. If your balance cannot cover the endpoint’s price, the check is refused: nothing is fetched, nothing is charged, and no webhook is sent for it.
After the second refused check the watch pauses, and your webhook receives subscription.paused with reason: "unpaid". A paused watch keeps its schedule. Top up and it resumes on its own at its next scheduled check once the balance covers the price. That is not the moment the payment lands, and there is nothing to create again.
GET /v1/subscriptions tells the two kinds of stopped watch apart. pausedReason: "unpaid" is paused for credit and resumes after a top-up. active: false with pausedReason: null was stopped after repeated failed checks and does not resume.
A daily credit cap is not a pause. Checks past your team’s cap are skipped until the next day, and the watch carries on from there.
{
"type": "subscription.paused",
"subscriptionId": "sub_01JB9A4N7RX3L0",
"endpoint": "youtube.channel",
"params": { "handle": "@mkbhd" },
"reason": "unpaid"
}Doing it without writing any code
Everything above has a form behind it. Jobs & watches in the dashboard runs the same handlers as the routes in the table: one code path, so billing cannot differ between what you click and what you curl.
Run a job
- 01
Open Jobs & watches in the dashboard. Jobs are on the left, watches on the right.
- 02
Under "Run a job", pick an endpoint. Every endpoint can be a job: a job runs once, so nothing is excluded from this list.
- 03
Fill in the parameters. Anything you leave blank is dropped rather than sent as an empty string.
- 04
A webhook URL is optional here. Leave it blank and follow the job in the table below; supply one and we call it when the job finishes.
- 05
Press "Run job". The button states the estimate before you click, and the confirmation links straight to the job.
- 06
The table underneath shows endpoint, status, progress as completed-plus-failed over total, credits charged so far, and when it was created.
Create a watch
- 01
Same page, right-hand panel, "Create a watch".
- 02
The endpoint list is shorter than the jobs one. Search, trending and similar endpoints return a different result set on every request by design, so a watch on one would fire its webhook on noise. They are withheld rather than sold, and the count of what is missing is stated under the picker.
- 03
A webhook URL is required. A watch with nowhere to deliver is not a watch.
- 04
"Check every" offers only the intervals the endpoint you picked accepts, from its minimum up to 7 days. The endpoint's suggested interval is selected and marked "suggested". Any other interval in the list is yours to pick.
- 05
"Review watch" shows the disclosure before anything is created: how often it checks, what each check costs, that the webhook fires only on a change, and what the checks add up to per day at the interval you picked, or per week for an interval longer than a day. Nothing exists until you confirm.
- 06
The list shows interval, status, last checked and last changed. A watch that ran out of credits says so, and so does one stopped after repeated failed checks, because the fix differs: top up and it resumes on its own at its next scheduled check, or check the target still exists. Deleting takes two clicks, and a delete that fails says so instead of quietly leaving the watch billing.
A job you started in the dashboard and a job you started with a key are the same job, on the same team, spending the same balance. The dashboard is a second door, not a second system.