Signals
Re-run a batch on a schedule: watch profiles, companies or posts and get a batch.completed webhook per run.
#What a signal is
A signal is a saved POST /v1/batch body plus a schedule. On every tick Up2Data submits that body as a batch job on your behalf, bills it at the normal batch rates from your balance at that moment, and sends a batch.completed webhook to the signal's webhook_url when the job finishes. Use it to watch a list of profiles or companies for changes, re-pull engagement on a set of posts every day, or run the same search every week.
Scheduling, reading, pausing, resuming and deleting a signal are all free. Only the runs cost credits, exactly what the same batch would cost if you submitted it yourself.
#Schedule a signal
curl -X POST https://api.up2data.ai/v1/signals \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Watch key accounts",
"schedule": { "interval_ms": 86400000, "start_at": "2026-11-01T08:00:00Z", "max_runs": 30 },
"request": {
"items": [
{ "type": "profile", "url": "https://linkedin.com/in/a" },
{ "type": "company", "url": "https://linkedin.com/company/acme" }
],
"webhook_url": "https://your-server.com/hooks/up2data"
}
}'{
"data": {
"signal_id": "9d1f4c2e-7b3a-4e8f-a1c5-2f6b8d9e0a11",
"status": "active",
"schedule": { "interval_ms": 86400000, "start_at": "2026-11-01T08:00:00Z", "ends_at": null, "max_runs": 30 },
"run_count": 0,
"next_run_at": "2026-11-01T08:00:00Z"
},
"meta": { "creditsUsed": 0, "creditsRemaining": 98413, "billed": false, "reason": "signal_created", "requestId": "req_t4sk0001" }
}request is the exact batch body: every item shape, flag and operation that POST /v1/batch accepts works here. When you create the signal only items (1 to 1,000 objects) and webhook_url (https) are checked; the rest is validated when a run submits it.
#Pick a schedule
| You want | schedule |
|---|---|
| Every day, until you stop it | { "interval_ms": 86400000 } |
| Tomorrow, the day after, and the day after that | { "interval_ms": 86400000, "start_at": "<tomorrow 08:00>", "max_runs": 3 } |
| Every 6 hours until year end | { "interval_ms": 21600000, "ends_at": "2026-12-31T00:00:00Z" } |
| Once, at a point in time | { "interval_ms": 60000, "start_at": "<when>", "max_runs": 1 } |
interval_ms is required and must be at least 60000. start_at defaults to now. Omit both ends_at and max_runs for an indefinite signal. A signal completes when run_count reaches max_runs or the next run would fall after ends_at; completed signals keep their history and can be revived by raising max_runs or moving ends_at with PATCH /v1/signals/{signal_id}.
After downtime on our side a missed tick runs once and the schedule realigns to now + interval_ms. There is no catch-up burst.
#Read the results
Each run creates one batch job. The signal carries the latest one as last_job_id, and GET /v1/signals/{signal_id}/runs lists every run with its job_id:
curl https://api.up2data.ai/v1/signals/9d1f4c2e-7b3a-4e8f-a1c5-2f6b8d9e0a11/runs -H "X-API-Key: YOUR_API_KEY"Then read a job like any other batch: GET /v1/batch/{job_id} for the summary and accumulated results, or GET /v1/batch/{job_id}/results for the incremental feed.
If you set request.webhook_url, every run's job sends batch.completed there. The payload names only the job_id, so either keep one webhook URL per signal or resolve the job through the runs list.
#When a run fails
| Run outcome | What happens to the signal |
|---|---|
| Job accepted (202) | run_count increments, last_job_id updates, next tick scheduled |
402 insufficient_credits |
Signal pauses with pause_reason insufficient_credits. The step is not consumed. Top up and resume; the same step runs on the next tick |
400 invalid_request |
Signal pauses with pause_reason invalid_request, because the same body would fail every time. Fix request with PATCH and resume |
429 rate_limited |
Run is deferred and retried after Retry-After under the same run_number. The step is not consumed |
500, 504, network error |
Retried three times with backoff; then the run is failed, last_error is set, and the schedule moves on |
Pausing a signal yourself with POST /v1/signals/{signal_id}/pause sets pause_reason to manual. A paused signal keeps its place: resuming it fires immediately if next_run_at is already past, otherwise at the next tick.
#Endpoints
| Call | Purpose |
|---|---|
GET /v1/signals |
List signals, filter by status |
POST /v1/signals |
Schedule a signal |
GET /v1/signals/{signal_id} |
One signal |
PATCH /v1/signals/{signal_id} |
Change name, schedule or request |
DELETE /v1/signals/{signal_id} |
Delete a signal and its history |
POST /v1/signals/{signal_id}/pause |
Pause |
POST /v1/signals/{signal_id}/resume |
Resume |
GET /v1/signals/{signal_id}/runs |
Run history with each run's job_id |
All of them are free and count toward your per-key request limit like any other call. See Errors for the 409 conflict a completed signal returns on pause and resume.