Skip to content

Logs

Ship your application's log events to Helmdesk and it becomes your error watchdog: events are grouped into issues by fingerprint, the Log Watch agent emails you when a new error appears or a known one keeps recurring, and your AI tools can query everything over the API. It is not a full observability platform — it is the "tell me when something is wrong" layer a small team actually reads.

Which project?

Every log event belongs to one project. A key limited to one project implies it — send nothing extra. A key that reaches several projects (the kind you give a coding agent) names the project on each request with the Helmdesk-Project header (slug or id) or ?project=; without it the API answers 400 project.required with the projects it could have meant. Every object in a response carries projectId and projectSlug, so you can always tell what you are looking at.

curl
curl https://helmdesk.dev/api/v1/log-issues \
  -H "Authorization: Bearer sk_live_..." \
  -H "Helmdesk-Project: ledgerly"
SDK
// Pin the project once — or set HELMDESK_PROJECT and omit it.
const helmdesk = new HelmdeskClient({ apiKey, project: 'ledgerly' })
await helmdesk.logs.issues()

See Authentication for how keys and projects fit together.

Ship events

Send batches of up to 100 events to POST /api/v1/logs with an API key holding the logs:write scope (there is a Log shipper preset when creating keys). Levels are debug, info, warn, error, and fatal; metadata is any JSON object up to 8 KB.

import { HelmdeskClient } from '@helmdesk/sdk'

const helmdesk = new HelmdeskClient({ apiKey: process.env.HELMDESK_API_KEY! })

// Fire-and-forget from your error paths — don't await on the hot path.
void helmdesk.logs.write([
  {
    level: 'error',
    scope: 'checkout',
    message: 'Payment failed: card declined',
    metadata: { orderId: 'ORD-4821', provider: 'stripe' },
  },
])

Or with plain fetch:

await fetch('https://helmdesk.dev/api/v1/logs', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.HELMDESK_API_KEY}`,
  },
  body: JSON.stringify({
    events: [
      { level: 'error', scope: 'worker', message: 'Job import-users failed: timeout' },
    ],
  }),
})

Error grouping (fingerprints)

Every event is fingerprinted from its level, scope, and normalized message — ids, emails, timestamps, and numbers are stripped first, so Order 4821 failed and Order 5133 failed land in the same issue. Issues track first/last seen, a running count, and a status (open → acknowledged → resolved). A resolved issue that recurs is automatically reopened as a regression.

The Log Watch agent

Enable Log Watch on the project's Agents page and it checks your live error and fatal issues every 15 minutes. It alerts — an item in the Agents Activity inbox plus an email to the team (opt-out per member under Notifications) — when either:

  • a new error fingerprint appears for the first time, or
  • a known error recurs past your configured threshold inside the detection window (both tunable in the agent's config).

The same issue re-alerts at most once every 24 hours, at most 5 alerts per sweep, and acknowledged or resolved issues stay quiet. Sandbox logs never trigger the agent.

Query logs

Both endpoints require the logs:read scope — ideal for an AI assistant that checks your errors every morning.

// Raw events, newest first
const { logs, total } = await helmdesk.logs.list({
  level: 'error,fatal',
  search: 'timeout',
  limit: 50,
})

// Grouped issues
const { issues } = await helmdesk.logs.issues({ status: 'open' })
for (const issue of issues) {
  console.log(`${issue.level} ×${issue.eventCount}: ${issue.sampleMessage}`)
}

Acknowledge and resolve

An issue has a status: open (new or recurring), acknowledged("seen it" — it stops looking new but stays in the list), or resolved("fixed" — a resolved issue reopens by itself if the same error recurs, so a regression is visible without any bookkeeping). Setting the status is the reversible alternative to deleting an issue, and it is what the dashboard's own buttons do. Requires the logs:write scope — the same scope your shipper holds, because marking an error handled is not erasing it.

curl -X PATCH https://helmdesk.dev/api/v1/log-issues/<issueId> \
  -H "Authorization: Bearer sk_live_..." \
  -H "Helmdesk-Project: ledgerly" \
  -H "Content-Type: application/json" \
  -d '{"status": "acknowledged"}'
// SDK
await helmdesk.logs.acknowledgeIssue(issue.id) // seen it
await helmdesk.logs.resolveIssue(issue.id)     // fixed — reopens if it recurs
await helmdesk.logs.setIssueStatus(issue.id, 'open') // reopen by hand

// The response is the updated issue, with projectId + projectSlug.

An issue id from another project or environment is a 404 — a sandbox key (or the Helmdesk-Environment: sandbox header) acts on sandbox issues only.

Delete logs

Retention prunes on a schedule. Deletion is for noise you want gone now — a known third-party flake, or a fixed bug still cluttering the list. Available in the dashboard (row checkboxes on both the Errors and Stream views, plus a delete button in the issue reading pane) and over the API.

Deleting an issue also deletes the raw events behind it, in one transaction. Deleting only the rollup would leave your event stream full of events for an error the issues list says no longer exists.

// The issue and every event behind it
const { eventsDeleted } = await helmdesk.logs.deleteIssue(issueId)

// Or prune individual events (max 200 per call)
const { deleted, requested } = await helmdesk.logs.deleteEvents([id1, id2])
// deleted < requested means some ids were not in this project/environment

logs:delete is a separate scope

Deliberately not part of logs:write. The write scope is what the log shipper embedded in your production app holds — and a key that only needs to send errors should not be able to erase the history of them. Grant logs:delete to a separate key, or use the dashboard.

Two things deletion does not do

  • It does not refund quota. log_usage counts what was ingested in a month. Deleting rows afterwards does not un-send them, the same way deleting a sent email does not un-send it.
  • Deleting events does not change an issue's eventCount. That number is a lifetime total of what arrived, not a live count of what is still stored — pruning the stream should not rewrite the history of how often something fired. To make an error go away entirely, delete the issue.

There is no trash: deletion is immediate and irreversible. If the same error happens again it comes back as a new issue with a fresh count.

Retention & quotas

Live log events are pruned automatically after your plan's retention window; issues age out when they stop recurring. Monthly event quotas apply to live events only.

PlanRetentionEvents / month
Free3 days10,000
Starter7 days100,000
Growth30 days1,000,000
Enterprise90 daysUnlimited

Sandbox events (shipped with an sk_sandbox_… key) are quota-exempt and kept for 3 days on every plan — see Sandbox.

Limits

  • Up to 100 events per request.
  • Messages up to 4,000 characters; metadata up to 8 KB per event.
  • Standard API rate limit: 100 requests/minute per key — batch your events.