Skip to main content
Runs are asynchronous. A production integration starts a run, gets told when it finishes, checks the result, and decides what to do on failure. This page covers each part.

Webhooks

Pass webhook_url when you start a run. Skyvern sends one POST to that URL when the run reaches a terminal status: completed, failed, terminated, timed_out, or canceled. Job recipe runs (the Apply and Extract recipe endpoints) take the same top-level webhook_url, and proxy_location too. A recipe request without webhook_url sends no webhook; blank or null counts as absent.
To give an agent a default webhook for every run, set webhook_callback_url in the agent definition. A webhook_url on a run overrides it.
The names differ by place. webhook_url goes on a run request. webhook_callback_url goes inside the agent definition. Putting webhook_url in an agent definition returns no error, and no webhook is ever sent.

Payload

The body is the same run object that get_run returns. The fields you use most:
Agent runs also carry failure_category, described in Error handling. The full schema is in the API reference.

Verify the signature

Skyvern signs every webhook and TOTP request with your API key using HMAC-SHA256, so you can verify the request actually came from Skyvern before acting on it. Headers sent with every request:
  • x-skyvern-signature: HMAC-SHA256 signature of the payload, hex-encoded
  • x-skyvern-timestamp: Unix timestamp when the request was sent
  • Content-Type: application/json
Copy the verifier for your language and reject the request whenever it returns false.
If you copied a verifier from this page before August 2026, re-check it. Earlier versions of these examples were permissive: the TypeScript example called crypto.timingSafeEqual() without using its return value, so any signature of the right length was accepted, and the TOTP examples compared signatures with ==. Replace them with the verifiers below.
Act on the comparison result, and reject a missing signature. A constant-time comparison only protects you if you branch on what it returns. Two ways to get this wrong: calling crypto.timingSafeEqual() for its side effects and treating “it did not throw” as success — it only throws on a length mismatch, so every wrong-but-64-character signature passes — or computing a verdict and never checking it. A request that arrives with no x-skyvern-signature header at all must be rejected, not crash your handler.
Use constant-time comparison to prevent timing attacks:
  • Python: hmac.compare_digest()
  • TypeScript: crypto.timingSafeEqual()
  • Go: hmac.Equal()
Never use simple equality operators (== or ===) for signature comparison as they are vulnerable to timing attacks.
Always validate against the raw request body bytes. Skyvern normalizes JSON before signing: it removes whitespace (using compact separators) and converts whole-number floats to integers (3.0 becomes 3). If you parse the JSON and re-serialize it, the byte representation will differ and signature validation will fail.

Respond fast

  • Return a 2xx within 30 seconds. Slower responses count as a failed attempt even if you process the payload. Queue the work and reply right away.
  • The URL must be reachable from the internet. localhost does not work from Skyvern Cloud.
  • Treat duplicates as normal. Skyvern can deliver the same payload more than once (see below).

Retries and replay

When delivery fails

Delivery and the run are independent. Each attempt times out after 30 seconds. Skyvern makes up to 3 attempts, waiting about 1 second, then 2 seconds, in between. If you return a Retry-After header, Skyvern waits that long instead, up to 30 seconds. For an agent run with a retry policy, the final webhook gets up to 15 of these 3-attempt deliveries. If every attempt fails, the run keeps its status and output. Nothing is lost. Keep a fallback: if you expect a run to be done and have no webhook, call get_run.

Replay a webhook

After you fix the endpoint, send the payload again with retry_run_webhook. Pass a different webhook_url to send it somewhere else.
A replay makes one attempt and waits up to 60 seconds. The response reports status_code, latency_ms, response_body, and error. It does not retry, so check status_code and call again if needed. A replay is signed with the API key you call it with.

Retry an agent run automatically

Add retry_policy to an agent definition to re-run it on chosen outcomes. The run keeps its run_id and records each try in attempts.
  • max_retries is 1 to 5. delay_seconds is 0 to 3600.
  • webhook_on_retry is final_only (default, one webhook after the last attempt) or every_attempt.
  • While retry_pending is true, the result can still change. Act on the payload with retry_pending: false.
  • retry_run_webhook returns 409 while a retry is pending.
Use run_id, attempt, and retry_pending together as your idempotency key. Every attempt shares one run_id, so keying on run_id alone drops the final attempt. A run canceled during its retry delay sends two payloads for the same attempt, first with retry_pending: true, then false.

Error handling

Statuses

created, queued, running, and paused (waiting for a person at a Human Interaction block) are not terminal.

Map failures to your own codes

failure_reason is free text written by the AI. To branch in code, pass error_code_mapping: your code as the key, a plain-language description as the value. When the run fails, Skyvern matches the page against your descriptions and puts the matching code in errors[].error_code.
Each mapped error looks like {"error_code": "login_failed", "reasoning": "...", "confidence_float": 0.95}. In an agent, set error_code_mapping on a block instead. A Validation block with complete_criterion, terminate_criterion, and error_code_mapping works like an assertion: it stops the agent with your code as soon as the page shows the wrong state. See blocks. Skyvern also adds its own codes to errors: REACH_MAX_STEPS, REACH_MAX_RETRIES, OTP_ERROR, OTP_TIMEOUT, OTP_EXPIRED, MISSING_TOTP_SOURCE, CREDENTIAL_LOGIN_UNCONFIRMED, and CREDENTIAL_ACCOUNT_LOCKED. When the organization-scoped credential-verdict confirmation flag is enabled, a credential rejection can instead be reported as CREDENTIAL_LOGIN_UNCONFIRMED or CREDENTIAL_ACCOUNT_LOCKED. Handle these system-defined codes explicitly anywhere you branch, retry, or filter by the original credential code. The login-request-status check is not available yet; keep this flag disabled until that check, the follow-up evaluation, and the human audit are complete.

Failure categories

Agent runs that end in failed, terminated, or timed_out get a failure_category list. Each entry has a category, a confidence_float, and reasoning. The first entry is the most likely cause. The app shows it as a badge on the run. The troubleshooting page lists every category.

Requests rejected before a run starts

Some agent run requests can never start. The API answers 400 with a detail message and creates no run, so there is nothing to poll and no webhook. Causes: a required input with no value, a credential ID that doesn’t exist in your organization, a value of the wrong type, or an undeclared key that looks like a misspelled credential input. Fix the request and send it again.

Anti-bot and CAPTCHA

Skyvern Cloud solves common CAPTCHAs during a run: reCAPTCHA, hCaptcha, Cloudflare Turnstile, FunCaptcha, MTCaptcha, and text CAPTCHAs. Solving is not guaranteed. Every browser also launches with automation flags removed, and its timezone follows the proxy location so the IP and the clock agree. When a site still blocks runs, work down this list:
  1. Use a residential proxy. Datacenter IPs are the most common block signal. RESIDENTIAL is the default on Cloud. For logins, use RESIDENTIAL_ISP, which keeps one static IP.
  2. Keep the same browser. A browser session keeps cookies and IP across steps. A browser profile brings back a browser that already passed the check.
  3. Raise max_steps. Some challenges, Cloudflare especially, loop through several pages.
  4. Slow down. Add a Wait block between rapid actions.
  5. Hand it to a person. Open the run in the app, click Take control on the live browser, solve the challenge, then click Stop controlling. The run continues where it was.
Live browser view with the Take control button over the stream To count blocked runs, look for ANTI_BOT_DETECTION in failure_category.
Self-hosted Skyvern does not solve CAPTCHAs. When the agent sees one, it waits 30 seconds so you can solve it in the browser window, then continues. Self-hosted servers can load Chrome extensions with the EXTENSIONS and EXTENSIONS_BASE_PATH settings. See browser settings.

Proxies and geo-targeting

Set proxy_location on a task, an agent run, an agent definition (the default for its runs), or a browser session (every run in the session shares the IP). Proxies are a Skyvern Cloud feature.
US-CA, US-NY, US-TX, US-FL, and US-WA are deprecated and route through RESIDENTIAL_ISP. For a US state or a city, pass a GeoTarget object instead: country (ISO code, required), subdivision (for example CA), and city (English name). Geo targets accept the same 21 countries: US plus the country codes above. City and state pools are smaller. If one is unavailable, widen the target from city to state to country.
A browser profile does not keep the proxy it was created with. To reach a site from the same IP on every run, pin a proxy on the profile. See Browsers.

Reliability tips

  • Say when it’s done. End every goal with a completion rule, such as “COMPLETE when the confirmation number is shown,” and a stop rule, such as “TERMINATE if the login is rejected.” Name the submit step: agents sometimes stop after filling a form.
  • Describe what’s visible. “The blue Submit button under the form” beats an element ID. The agent works from screenshots.
  • Use the narrowest block. An Action block does one thing. A Navigation block reaches one goal. A Task block keeps going until it thinks the whole goal is met, so it may click Next on every page.
  • Spell out tricky inputs. For autocomplete fields: “Type ‘Toronto’, pick the first matching suggestion, and move on.” For dates: give the exact format.
  • Assert at checkpoints. Add a Validation block after login and before submitting. It reads the current page, not earlier block outputs. To compare extracted values, use a Code block or check output in your own code.
  • Make loops recoverable. Set continue_on_failure: true on loops, and start each iteration with a step that returns to the list page. A While loop needs an exit condition that the page itself can show.
  • Load lazy pages. Set max_screenshot_scrolls so the agent scrolls before it reads the page.

Cost control

  • Cap steps. max_steps on a task, or max_steps_override on an agent run (x-max-steps-override in TypeScript), limits steps for each task in the run. A run that hits the cap ends failed with “Reached the maximum steps” in failure_reason. Compare step_count with your cap and lower it if you have room.
  • Cap time. max_elapsed_time_minutes on an agent run times it out (timed_out) after that many minutes. The default is 240 and the maximum is 480.
  • Run cached code. After a successful agent run, Skyvern can replay the recorded actions as code instead of calling the AI at every step. Pass run_with="code" to use it, or "agent" to force the AI. If the code fails and ai_fallback is on, the AI takes over.
  • Skip the login. A browser profile that is already signed in saves the steps a login takes on every run.
  • Start on the right page. Pass the deepest url you can and a data_extraction_schema so the agent doesn’t wander.
Plans and credit prices are on skyvern.com/pricing. Self-hosted servers set the default step limit with MAX_STEPS_PER_RUN and choose the model with LLM_KEY. See configuration.

Observability

Artifacts explain one run (see troubleshooting). For trends across runs, such as failure rate, latency, and slow tasks, send traces to Laminar. With the SDK. Initialize Laminar before you create the Skyvern client so it can trace your SDK calls.
On a self-hosted server. Install lmnr in the server’s Python environment and set LMNR_PROJECT_API_KEY. The server then traces every LLM call (prompt, response, tokens), every browser action, and every block. For a self-hosted Laminar, also set LMNR_BASE_URL, LMNR_GRPC_PORT (default 8001), and LMNR_HTTP_PORT (default 8000).

Next