Cronvoy API
v1 · August 13, 2026
Everything the dashboard does, your code can do: launch a browser run, put one on a schedule, set up a job-application agent, read back what it did. Point an AI assistant at it and it can operate Cronvoy on your behalf.
Authentication
Create a key in the dashboard under Settings → API keys. It is shown once. Send it as a bearer token:
curl https://cronvoy.com/api/v1/status \
-H "Authorization: Bearer cv_live_..."
A key acts as your account and nothing more. Every request is carried out as you against the same database rules your dashboard obeys, so a key can never read or touch another account's data. What a given key may do on your account is decided by the scopes you ticked when you made it:
| Scope | Grants |
|---|---|
runs:read | Run history, transcripts, the live view |
runs:write | Launch, stop and continue runs |
schedules:read | List schedules |
schedules:write | Create, edit, pause and trigger schedules |
schedules:delete | Delete schedules |
agents:read | Agents and their records |
agents:write | Create and configure agents, edit records |
agents:delete | Delete agents and records |
profiles:read | Profiles, which sites they can sign into, document list |
profiles:write | Create profiles, upload documents |
Two things the API will never do: return a saved password (they are encrypted in your browser and only the runner can open them), and accept one. Add logins in the dashboard. Keys also cannot mint or revoke other keys — that takes a signed-in session, so revoking a leaked key actually ends its access.
Quickstart
Launch a cloud run and watch it:
curl -X POST https://cronvoy.com/api/v1/runs \
-H "Authorization: Bearer $CRONVOY_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"Go to news.ycombinator.com and list the top 3 stories"}'
# { "queueId": "…", "target": "cloud", "status": "queued", … }
curl "https://cronvoy.com/api/v1/live" -H "Authorization: Bearer $CRONVOY_KEY"
# { "active": true, "status": "running", "action": "reading the page", "stepNo": 4, "log": [ … ] }
Runs
| Endpoint | What it does |
|---|---|
GET /api/v1/runs | History, newest first. ?limit= (max 100), ?source=cloud|extension|scheduled. |
POST /api/v1/runs | Launch. { prompt, target?, profileId?, agentId? } — target is cloud (default, runs 24/7 on our servers) or extension (runs in your own Chrome, which must be open). |
GET /api/v1/runs/{id} | One run with its transcript and diagnostics. ?transcript=0 to skip the transcript. |
POST /api/v1/runs/{id}/stop | Stop the live run. Takes effect at the next step checkpoint, usually a few seconds. {id} may be current. |
POST /api/v1/runs/{id}/continue | { prompt } — a follow-up that reuses the profile session and the prior run's context, so it is still signed in and remembers what happened. |
POST /api/v1/runs/{id}/respond | { input } — answer a run that paused for you (a 2FA code, an approval). |
GET /api/v1/live | What the current run is doing. ?frame=1 adds a JPEG screenshot as base64 (large — leave it off when polling). |
A run that pauses for you reports status: "waiting" with an awaiting object on /live. Answer it with /respond and it picks up where it left off.
Using curl for an action with no body? Send an empty object: curl -X POST … -d '{}'. A bare curl -X POST sends no Content-Length at all, and the web server's firewall answers that with an HTML 406 before it ever reaches the API. Ordinary HTTP clients (fetch, requests, axios) always send the header, so this only bites at the command line.
Schedules
A schedule is a prompt plus a cadence. Cloud schedules fire on our servers around the clock; extension schedules fire in your Chrome.
curl -X POST https://cronvoy.com/api/v1/schedules \
-H "Authorization: Bearer $CRONVOY_KEY" -H "Content-Type: application/json" \
-d '{
"prompt": "Check my Loops dashboard for a draft campaign and tell me if one is ready",
"every": "daily",
"at": "06:30",
"tz": "America/New_York",
"target": "cloud"
}'
every takes "hourly", "6h", "daily", "weekly", or a number of minutes (60 minimum). at is the local anchor time: an interval schedule is phase-locked to it, so two agents can be set an hour apart and alternate forever. For specific weekdays send days instead: {"days":["mon","wed","fri"],"at":"09:00"} — names or numbers, 0 (Sunday) to 6.
| Endpoint | What it does |
|---|---|
GET /api/v1/schedules | All of them, next-to-run first, with a plain-English cadence and each one's memory (doneCount, progressCursor). |
POST /api/v1/schedules | Create. Also accepts profileId and agentId. |
PATCH /api/v1/schedules/{id} | Edit anything, including {"enabled": false} to pause and true to resume. Changing the cadence recomputes the next run. |
DELETE /api/v1/schedules/{id} | Delete it. Its memory of what it already handled is kept, so a future schedule doesn't redo that work. |
POST /api/v1/schedules/{id}/run-now | Fire it immediately. The regular cadence is untouched — this is an extra run, not a replacement. |
Agents
An agent is a preset with settings it carries into every run — the job-application agent knows your titles, salary, work authorization and EEO answers, and shares one record ledger across all your agents so it never applies to the same job twice.
# what you can create, and every setting it takes
curl https://cronvoy.com/api/v1/agent-templates -H "Authorization: Bearer $CRONVOY_KEY"
# create one
curl -X POST https://cronvoy.com/api/v1/agents \
-H "Authorization: Bearer $CRONVOY_KEY" -H "Content-Type: application/json" \
-d '{
"templateId": "job-apply",
"name": "Product Manager Job Agent",
"settings": {
"titles": ["Product Manager", "Senior Product Manager"],
"locations": "Remote (US)",
"yearsExperience": "8",
"workAuth": "US citizen",
"perRun": 3
}
}'
The response lists missingRequired if anything the forms will ask for is still blank. Then read what it did:
curl "https://cronvoy.com/api/v1/agents/{id}/records?status=applied&limit=20" \
-H "Authorization: Bearer $CRONVOY_KEY"
| Endpoint | What it does |
|---|---|
GET /api/v1/agent-templates | Templates and their full field list. |
GET /api/v1/agents | Your agents. ?full=1 adds the exact instructions a run receives. |
POST /api/v1/agents | { templateId, name?, settings? } |
PATCH /api/v1/agents/{id} | Merges settings — patch one field without clearing the rest. |
GET /api/v1/agents/{id}/records | The ledger. ?status=, ?archived=1, ?limit=. |
PATCH …/records/{recId} | Correct a company or role, change a status, or archive a row. |
Profiles
A profile is the identity a run wears: its own logins, its own browser session, its own documents. Use separate profiles to keep two accounts on the same site from mixing.
| Endpoint | What it does |
|---|---|
GET /api/v1/profiles | All profiles. |
POST /api/v1/profiles | { name, context? } — context is a persona note handed to every run using it. |
POST …/{id}/duplicate | { name?, includeLogins?, includeSession?, includeDocuments? } — a second identity that starts out already signed in. Saved passwords and the browser session are copied as sealed ciphertext (the API cannot read them and never returns them). It is a snapshot: each profile saves its own session from the next run on. Don't run both at once — one account in two browsers simultaneously is what bot detection looks for. |
GET /api/v1/profiles/{id}/connections | Which sites this profile can sign into, and which sessions are live. Usernames only, never passwords. |
GET /api/v1/profiles/{id}/documents | Uploaded files. ?data=1 on a single document returns its base64. |
POST /api/v1/profiles/{id}/documents | { name, type, dataB64 }, up to 700 KB. A run attaches it by name. |
Recipes
Set up a job agent and let it run every morning
POST /api/v1/profiles { "name": "Job hunt" } → profileId
POST /api/v1/profiles/{profileId}/documents { name: "resume.pdf", … }
POST /api/v1/agents { "templateId": "job-apply", "settings": {…} } → agentId
POST /api/v1/schedules { "prompt": "Apply to product roles that fit me",
"agentId": "…", "profileId": "…",
"every": "daily", "at": "08:00", "tz": "America/New_York" }
Pause everything
GET /api/v1/schedules
PATCH /api/v1/schedules/{id} { "enabled": false } # for each
Did last night's run get anywhere?
GET /api/v1/runs?limit=1
GET /api/v1/runs/{id}?transcript=0 # status, summary, and a diagnostics block
Limits
- 120 requests a minute per key.
- Run budgets per key, per day and per month (a new key starts at 50/day, 500/month).
GET /api/v1/statusalways reports what's left. A run costs real browser time and model tokens, which is why launching is budgeted and reading is not. - Every run launched with a key records which key launched it, so revoking one has a visible blast radius.
Errors
Errors are JSON, with a stable error code to branch on and a message to read:
{ "error": "missing_scope", "message": "This API key does not have the `runs:write` scope.",
"needed": "runs:write", "has": ["runs:read"] }
| Code | Meaning |
|---|---|
401 bad_key | Unknown, malformed or revoked key. |
403 missing_scope | The key wasn't granted that permission. |
404 not_found | No such run, schedule, agent or profile on this account. |
409 still_running / paused | The action doesn't apply in that state. |
429 rate_limited | Too many requests; Retry-After says how long. |
429 run_budget_day | This key's run budget is spent. Resets 00:00 UTC. |
Questions or something missing: hello@cronvoy.com.