Crontab curl: call an API daily and escape the % sign

A crontab line runs curl with /bin/sh, and a bare % becomes a newline. Escape it as \%, use full paths, log the output, and key the request by date.

5 min readSume
All posts

To call an API from crontab with curl, put the whole command on one crontab line and write it for cron, not for your terminal: cron runs the line with /bin/sh and turns every unescaped % into a newline, so date +%F must be written date +\%F. Use full paths such as /usr/bin/curl, append the output to a log file, and keep the API key in a file that curl reads with -H @file.

Cron facts come from cronie's crontab(5) and cron(8) man pages as published on man7.org; other cron implementations can differ. curl facts come from the curl man page, and Sume facts from Create a run, Runs and results and Errors and spend. All were read on 2026-09-28. Sume has no crontab integration: the job makes one plain HTTPS call.

What does a working crontab curl line look like?

This entry starts one Sume Format run at 6:00 a.m. every day. \% keeps the date intact, cron sets $HOME from /etc/passwd, and --fail-with-body (curl 7.76.0 and later) makes curl exit with an error on a 4xx or 5xx while still writing Sume's JSON error to the log. The header file holds one line: Authorization: Bearer followed by the key.

# m h dom mon dow  command  (crontab -e; one entry per line)
0 6 * * * /usr/bin/curl -sS --fail-with-body --max-time 30 --retry 3 -H @$HOME/.config/sume/auth-header -H 'Content-Type: application/json' -H "Idempotency-Key: daily-recap-$(date -u +\%F)" -d '{"input":{"feed_url":"https://example.com/daily.json"}}' https://api.sume.com/v1/formats/acme/daily-recap/runs >> $HOME/sume-cron.log 2>&1

Why does curl work in my terminal but not in crontab?

Because cron's environment is not your shell's. Most failures fall into one of these rows.

From crontab(5), cron(8), the curl man page and Sume's Errors and spend, read 2026-09-28.
SymptomCauseFix
The command stops at date +cron turns a bare % into a newline and sends the rest to the command's standard inputWrite every % as \%
A tool that runs in your terminal isn't foundcronie sets its own PATH unless started with -PUse full paths such as /usr/bin/curl
Sume answers 401 unauthorizedThe key you exported in your shell isn't set: cron itself sets SHELL, LOGNAME and HOMERead the key from a file with -H @file
Sume answers 415 unsupported_media_type-d sends application/x-www-form-urlencodedAdd -H 'Content-Type: application/json'
The job exits 0 but nothing startedBy default curl does not treat HTTP error codes as a failureAdd --fail-with-body and read the log
No output anywherecron mails command output to the crontab's ownerAppend >> file 2>&1
The last entry misbehavesEvery entry must end in a newline, or cron considers the crontab at least partially brokenEnd the file with a newline

How do I keep the API key out of the crontab?

Put the header in a file that only your user can read. -H @file makes curl add a header for each line of the file, so the key appears neither in the crontab nor on the command line; curl's manual, writing about passwords, says such data should be read from a file and never used in clear text in a command line. curl bearer token: how to send the Authorization header covers the header itself. Sume's docs keep API keys on trusted servers and say to rotate a key that appears in logs or chat history.

How do I stop a double run from paying twice?

Build the Idempotency-Key from the date, as the line above does, and keep the body the same all day. A second run on the same date, or a curl retry, then gets 200 with the original run and idempotency_hit: true: no second run, no second charge. Kubernetes CronJob concurrency policy for paid API jobs lists the other replay cases.

  • --retry 3 resends after a timeout or an HTTP 408, 429, 500, 502, 503, 504, 522 or 524. It waits 1 second and then doubles the wait, and it follows Retry-After. The date key makes those resends safe.
  • Changing the body under the same key is 409 idempotency_conflict, and nothing runs. A deliberate second video on the same day needs a new key.

Should the cron job wait for the video?

No. The create answers immediately with a receipt, and the run itself takes minutes. Add communication.webhook_url to the body and Sume POSTs one signed format.run.terminal receipt when the run completes or fails; otherwise poll status_url from a separate job, doubling the gap up to a minute. If nothing but the clock triggers the work, Sume's own Scheduled runs a saved automation on a five-field cron in an IANA time zone: see Scheduled AI video agent runs.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume