# For AI coding agents

This page is written for coding assistants (and the people prompting them) that add monitoring to a project. Machine-readable entry points: [llms.txt](https://dev.tickingora.com/llms.txt), [llms-full.txt](https://dev.tickingora.com/llms-full.txt), [openapi.json](openapi.json). Every guide is also available as Markdown: add `.md` to its URL.

## Rules

1. **Ping after success, never before.** The Heartbeat call is the job's last step and runs only when the job succeeded (`&&`, `set -e`, `onSuccess`, `ExecStartPost`). A ping at the start of a job reports work that has not happened.
   **A failed ping must never fail the job** (`… || echo "ping failed" >&2`, systemd `ExecStartPost=-…`, `rescue(..., report: false)`, `try/except`), and error messages must not contain the ping URL. See [Heartbeat](heartbeat#send-it-after-success-never-before).
2. **Give every monitor an `external_id`** derived from the project and job (`acme-shop:invoice-export`). It is unique in the account and permanent: running the set-up again — even months later — returns `409 MONITOR_ALREADY_EXISTS` with `details.monitor_id` instead of a duplicate. Check first with `GET /monitors?external_id=…`.
3. **Also send an `Idempotency-Key`** on `POST /monitors` so a retried request (timeout, crash) is safe. It is remembered for 24 hours only — `external_id` is what prevents duplicates long-term.
4. **Store the `ping_url` as a secret** right after creation — it is shown only once (tokens are stored hashed); if the response got lost, retrying the same request with the same `Idempotency-Key` within 24 hours returns it again. Never commit it, never print it in logs. If the monitor already exists and the job has no URL, call `rotate-token` (the previous URL stops working).
5. **Make the URL available where the job runs.** Cron does not inherit interactive shell variables: use an env file with mode 600 owned by the job user (see [Heartbeat](heartbeat#cron-make-the-url-available-to-the-job)), systemd `EnvironmentFile`, or the platform's secret store.
6. **Never put the API key in the job.** The job needs only its ping URL. The API key belongs to your automation, with the minimum scopes.
7. **Match the window to reality.** The window must contain the time the job *finishes*. A job starting 02:00 and taking up to 40 minutes needs e.g. `02:00–03:00`.
8. **Verify with `?test=1`.** It proves the job host can reach Tickingora without counting for a window.
9. **Read errors.** On `422` fix the fields listed in `error.details`; on `409 MONITOR_ALREADY_EXISTS` use `details.monitor_id` (`GET`, then `PATCH` if the configuration differs); on `403 PLAN_LIMIT_REACHED` stop and tell the user; on `429` wait for `Retry-After`.
10. **Check the result**: `next_runs` in the response must show the expected windows.

## Recipe

```bash
# 1. Plan: is there room on the plan?
curl -sS https://dev.tickingora.com/api/v1/account -H "Authorization: Bearer $TICKINGORA_API_KEY"

# 2. Already there?
curl -sS "https://dev.tickingora.com/api/v1/monitors?external_id=acme-shop:invoice-export" -H "Authorization: Bearer $TICKINGORA_API_KEY"

# 3. Create (safe to repeat: 409 MONITOR_ALREADY_EXISTS if it exists)
curl -sS -X POST https://dev.tickingora.com/api/v1/monitors \
  -H "Authorization: Bearer $TICKINGORA_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: acme-shop:invoice-export:$(date +%F)" \
  -d '{"type":"heartbeat","external_id":"acme-shop:invoice-export","name":"Invoice export","timezone":"Europe/Warsaw",
       "schedule":{"preset":"daily","days":[1,2,3,4,5],"start":"20:00","end":"21:00"},
       "calendar":{"skip_holidays":true}}'

# 4. Save data.heartbeat.ping_url as TICKINGORA_PING_URL where the job runs
#    (env file 600 / systemd EnvironmentFile / platform secret store) — shown only once

# 5. Add to the job, after the work:
#    php artisan invoices:export && { curl -fsS -m 10 --retry 3 "$TICKINGORA_PING_URL" > /dev/null || echo "Tickingora ping failed" >&2; }

# 6. Verify from the job host, in the job's own environment
curl -fsS "$TICKINGORA_PING_URL?test=1"
```

## Prompt snippet

> Add Tickingora monitoring for the `<job>` job. Use the Tickingora API (https://dev.tickingora.com/api/v1, key in `TICKINGORA_API_KEY`) to create a Heartbeat monitor with `external_id: "<repo>:<job>"` (reuse it if it already exists) and an `Idempotency-Key`, a schedule window that covers when the job finishes, in the `Europe/Warsaw` time zone, skipping Polish holidays if the job does not run on them. Store `data.heartbeat.ping_url` as the `TICKINGORA_PING_URL` secret where the job runs (cron does not see shell variables; use an env file with mode 600) — do not commit it. Call the ping URL with `curl -fsS -m 10 --retry 3` only after the job succeeded. Verify with `?test=1` and show me `next_runs`. Follow https://dev.tickingora.com/docs/agents.md.

## Out of scope today

There is no dedicated MCP server yet — use the REST API and the OpenAPI document.
