Skip to content
Documentation

Make your endpoint ready for us.

What a Cronvella request looks like, how to prove it is really from us, and how success, retries and limits work.

What we send

Each call is a plain HTTP request using the method, headers and body you configured, plus these headers added by Cronvella:

HeaderMeaning
User-AgentCronvella/1.0 (+https://cronvella.com)
X-Cronvella-Job-IdThe numeric ID of the job being run.
X-Cronvella-Scheduled-ForThe time the run was meant to happen, in UTC (ISO 8601).
X-Cronvella-SignatureA timestamp and an HMAC signature, described below.

If your job has a body and you have not set a Content-Type header, we send application/json when the body is valid JSON and text/plain otherwise. The headers in the table above are always set by us: a header with the same name in your job is ignored.

Verify the signature

Anyone can send a request to a public URL, so check the signature before you act on one. Every job has its own signing secret, shown on the job's page. Keep it private.

The signature header looks like this:

X-Cronvella-Signature: t=1767225600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  1. Read t (a Unix timestamp) and v1 from the header.
  2. Build the signed string: the timestamp, a full stop, then the raw request body. For a request with no body, the body part is empty.
  3. Compute HMAC-SHA256 of that string using your signing secret, as lowercase hex.
  4. Compare it with v1 using a constant-time comparison.
  5. Reject the request if t is more than a few minutes old, which stops someone replaying a captured request.

Use the raw body. Verify against the exact bytes received, before any JSON parsing or reformatting, or the signature will not match.

Example Laravel route

Put the secret in your .env file and read it through config. In routes/web.php (or routes/api.php), exempt the route from CSRF protection, since it is called by a server and not a browser.

use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; Route::post('/hooks/cronvella', function (Request $request) { $header = $request->header('X-Cronvella-Signature', ''); // "t=1767225600,v1=abcdef..." becomes ['t' => '1767225600', 'v1' => 'abcdef...'] parse_str(str_replace(',', '&', $header), $parts); $timestamp = $parts['t'] ?? ''; $signature = $parts['v1'] ?? ''; $expected = hash_hmac( 'sha256', $timestamp.'.'.$request->getContent(), config('services.cronvella.secret'), ); abort_unless(hash_equals($expected, $signature), 401); abort_if(abs(time() - (int) $timestamp) > 300, 401); // older than 5 minutes // Verified. Do the scheduled work here. return response()->noContent(); });

In bootstrap/app.php, add the route to the CSRF exceptions with $middleware->validateCsrfTokens(except: ['hooks/cronvella']);.

Success and retries

  • A run succeeds when the status code matches the job's expected status. By default that is any 2xx. You can set specific codes, such as 200,204.
  • Anything else is a failure: another status code, a timeout, or a connection, DNS or blocked-address error.
  • Failed calls are retried up to the job's attempt limit, waiting 10 seconds, then 1 minute, then 5 minutes. We do not retry blocked addresses, or 4xx responses other than 408 and 429.
  • Each attempt is recorded on its own. After the final failed attempt we email you, and we email once more when the job recovers.
  • Redirects are not followed. A 3xx response is recorded as the result, so give us the final URL.
  • With overlap set to skip, a run that is due while the previous one is still going is recorded as skipped.

Alerts and heartbeats

  • We email you once when a job starts failing and once when it recovers, not for every failed run. You can turn these emails off on your account page.
  • A job can have a heartbeat URL. After each run we send a plain GET to it, or to the same URL with /fail added to the path if the run failed. This matches Pingvella and healthchecks.io. Retries and skipped runs do not ping. The heartbeat URL gets the same address checks as the job URL, and no signature or job data is sent to it.

Network and addresses

  • We call only http and https URLs on ports 80 and 443, without credentials in the URL.
  • Hostnames must resolve to public internet addresses. Localhost, private ranges, link-local addresses and cloud metadata addresses are blocked.
  • Identify us by the User-Agent above and the signature.
  • We do not publish fixed source IP addresses, so rely on the signature rather than the address. If you need to allow-list us, get in touch.

Limits

These depend on your plan. See pricing for the current values.

  • Number of jobs, and the shortest interval between runs.
  • The longest time we wait for a response (the timeout).
  • The size of the request body.
  • How long run history is kept. We store only the first 2 KB of each response.