---
title: Heartbeats
description: Single-shot run shortcut for webhook integrations, cron jobs, and anything without an SDK loop.
section: api
order: 4
---

# Heartbeats

A heartbeat is a single-call run: started and finished in one request, no separate events. Lands in the same dashboard as full SDK runs; upgrading later doesn't migrate data.

For shell one-liners, see [GET /v1/ping](/docs/api/ping). This page covers the JSON `POST` variant.

## POST /v1/heartbeats

`https://eu.ingest.agentping.io/v1/heartbeats`

### Request

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | No | Client-generated. Format `run_<region>_<32 hex>`. Omit to let the server mint one. |
| `agent` | string | Yes | Slug. New agents are auto-created. |
| `status` | string | No | `ok`, `fail`, `start`, `timeout`. Default `ok`. `start` opens a run without finishing it. |
| `started_at` | string (ISO 8601, UTC) | No | Default: server receive time. |
| `finished_at` | string (ISO 8601, UTC) | No | Default: server receive time. |
| `cost_usd` | number | No | Total cost in US dollars. Overrides token pricing when both are sent. |
| `model` | string | No | Model id (e.g. `gpt-4o`). With token counts and no `cost_usd`, the run is priced from your rate card. |
| `provider` | string | No | Provider (`openai`, `anthropic`, ...). Pairs with `model` for pricing. |
| `input_tokens` | integer | No | Prompt/input tokens. Priced against the rate card when `model` is set. |
| `output_tokens` | integer | No | Completion/output tokens. Priced against the rate card when `model` is set. |
| `cached_input_tokens` | integer | No | Cache-read tokens, billed at the cached rate. |
| `duration_ms` | integer | No | If absent, computed from `started_at`/`finished_at` or from a prior `start` with the same `id`. |
| `metadata` | object | No | Flat key/value JSON. Paid feature, limits below. |

:::note
**Metadata is a Team and Business feature.** Structured `metadata` is available on Team (up to 3 keys per request) and Business (up to 10 keys per request); Enterprise is custom. On Starter, any `metadata` you send is silently dropped. Within an allowed request, keys are capped at 20 characters and values at 50 characters. Over-cap keys (in the order sent), over-length keys, and over-length values are dropped individually; the request still succeeds. See [Tiers](/docs/billing/tiers).
:::

### Response

`202 Accepted` on first write. `200 OK` on idempotent retry. `409 Conflict` if `id` exists with mismatching `agent` or `started_at`.

```json
{
  "id": "run_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b9",
  "agent": "daily-summary",
  "status": "ok",
  "started_at": "2026-05-15T14:32:00.000Z",
  "finished_at": "2026-05-15T14:32:12.300Z",
  "received_at": "2026-05-15T14:32:12.450Z",
  "cost_usd": 0.084,
  "duration_ms": 12300
}
```

### Example

:::codetabs
```bash
curl -X POST https://eu.ingest.agentping.io/v1/heartbeats \
  -H "Authorization: Bearer ping_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "run_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b9",
    "agent": "daily-summary",
    "status": "ok",
    "started_at": "2026-05-15T14:32:00.000Z",
    "finished_at": "2026-05-15T14:32:12.300Z",
    "cost_usd": 0.084,
    "metadata": {"items_processed": 47}
  }'
```
```python
import agentping
agentping.init(api_key="apk_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6")
agentping.heartbeat(
    "daily-summary",
    status="ok",
    cost_usd=0.084,
    metadata={"items_processed": 47},
)
```
```typescript
import * as agentping from "@agentping/sdk";
agentping.init({ apiKey: "apk_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6" });
agentping.heartbeat("daily-summary", {
  status: "ok",
  costUsd: 0.084,
  metadata: { itemsProcessed: 47 },
});
```
```go
import "github.com/agent-ping/agent-ping-go"
agentping.Init(agentping.WithAPIKey("apk_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6"))
agentping.Heartbeat("daily-summary",
    agentping.WithStatus("ok"),
    agentping.WithCost(0.084),
    agentping.WithMetadata(map[string]any{"items_processed": 47}),
)
```
```php
use AgentPing\Laravel\Facades\AgentPing;
AgentPing::heartbeat('daily-summary',
    status: 'ok',
    costUsd: 0.084,
    metadata: ['items_processed' => 47],
);
```
:::

### Let AgentPing price it from tokens

When the caller has the model and token usage (an n8n AI node, a Make or Zapier step, a script that read the provider's `usage` block) send those instead of computing a cost:

```bash
curl -X POST https://eu.ingest.agentping.io/v1/heartbeats \
  -H "Authorization: Bearer ping_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "lead-enrichment",
    "status": "ok",
    "provider": "openai",
    "model": "gpt-4o",
    "input_tokens": 1240,
    "output_tokens": 380
  }'
```

AgentPing prices the run from your rate card. An unpriced model records at zero cost and shows in the unpriced-models banner on the Spend page.

## Heartbeat vs full run

| Aspect | Full run | Heartbeat |
|--------|----------|-----------|
| Number of HTTP calls | 2 to N (start, events, finish) | 1 |
| Events recorded | One row per event | None |
| Cost computation | Server-side from `llm_call` events | Client `cost_usd`, or server-side from `model` + token counts |
| Per-step latency | Yes, per event | Run-level only |
| Best for | Agent loops with multiple LLM or tool steps | Webhooks, cron, n8n flows, single-action jobs |

:::note
A heartbeat has no `llm_call` events, so its cost comes from what you send: either a pre-computed `cost_usd`, or a `model` plus token counts that AgentPing prices from your rate card (the same rate card that prices full runs). When both are present, `cost_usd` wins. Full runs always price from the rate card and ignore any client `cost_usd`.
:::

## When to use heartbeats

Heartbeats fit when:

- The job is a webhook (n8n, Zapier, Make, Vercel cron) and the SDK can't load.
- The job is a shell script or Lambda with one LLM call wrapped in retries.
- A team wants monitoring today; the SDK can be added later without losing history.

Full runs (`POST /v1/runs` + events + finish) fit when:

- An agent makes more than one LLM call.
- Tool calls or guardrail outcomes matter for debugging.
- Per-step latency is interesting.
- The SDK is already loaded.
