---
title: Runs
description: Start, append events to, and finish a run on the AgentPing ingest API.
section: api
order: 2
---

# Runs

A run is one invocation of an agent. The lifecycle is three calls: `POST /v1/runs` to open, zero or more `POST /v1/runs/{id}/events`, `POST /v1/runs/{id}/finish` to close. The SDK constructs the run ID locally as a UUIDv7 with the `run_` prefix and the team's region segment, then dispatches all three without waiting on prior responses.

All three are idempotent: retrying with the same ID returns `200 OK`, retrying with conflicting fields returns `409 Conflict`. Events may be posted before the run record.

Base URL: `https://eu.ingest.agentping.io/v1/...`

## POST /v1/runs

Start a new run.

### Request

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Client-generated. Format `run_<region>_<32 hex>`. See [Identifiers](/docs/concepts/identifiers). |
| `agent` | string | Yes | Agent slug. New agents are auto-created on first appearance. |
| `started_at` | string (ISO 8601, UTC) | Yes | Client clock, millisecond precision. |
| `customer_id` | string | No | Used for per-customer cost attribution. |
| `feature` | string | No | Free-form feature label, indexed for filters. |
| `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. AgentPing never rejects a request over a metadata limit: extra keys (beyond your tier's cap, in the order sent), over-length keys, and over-length values are dropped individually and the rest is stored. See [Tiers](/docs/billing/tiers).
:::

### Response

`202 Accepted` on first write, `200 OK` on idempotent retry, `409 Conflict` if the ID exists with a different `agent` or `started_at`.

```json
{
  "id": "run_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6",
  "agent": "support-triage",
  "started_at": "2026-05-15T14:32:01.123Z",
  "received_at": "2026-05-15T14:32:01.156Z",
  "status": "running"
}
```

### Example

:::codetabs
```bash
curl -X POST https://eu.ingest.agentping.io/v1/runs \
  -H "Authorization: Bearer apk_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "run_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6",
    "agent": "support-triage",
    "started_at": "2026-05-15T14:32:01.123Z",
    "customer_id": "acme-corp",
    "feature": "ticket-routing"
  }'
```
```python
import agentping
agentping.init(api_key="apk_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6")
run = agentping.run("support-triage", customer_id="acme-corp", feature="ticket-routing")
```
```typescript
import * as agentping from "@agentping/sdk";
agentping.init({ apiKey: "apk_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6" });
const run = agentping.run("support-triage", { customerId: "acme-corp", feature: "ticket-routing" });
```
```go
import "github.com/agent-ping/agent-ping-go"
agentping.Init(agentping.WithAPIKey("apk_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6"))
run := agentping.NewRun("support-triage",
    agentping.WithCustomerID("acme-corp"),
    agentping.WithFeature("ticket-routing"),
)
```
```php
use AgentPing\Laravel\Facades\AgentPing;
$run = AgentPing::run('support-triage', customerId: 'acme-corp', feature: 'ticket-routing');
```
:::

## POST /v1/runs/{id}/events

Append one or more events to an in-flight run. Each event has its own client-generated `id` for deduplication; the batch is processed atomically.

### Request

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `events` | array | Yes | Up to 50 per batch. Larger returns `400 Bad Request`. |
| `events[].id` | string | Yes | Client-generated. Format `evt_<region>_<32 hex>`. |
| `events[].type` | string | Yes | `llm_call`, `log`, `tool_call`, or custom. See [Events](/docs/api/events). |
| `events[].ts` | string (ISO 8601, UTC) | Yes | Client clock. |
| `events[].data` | object | Yes | Payload. See [Events](/docs/api/events). |

### Response

`202 Accepted` with batch summary.

```json
{ "accepted": 2, "duplicates": 0 }
```

Events with existing `id`s are silently dropped and counted under `duplicates`. `cost_usd` on `llm_call` events is server-computed; clients don't supply it.

:::note
Events may be posted before the parent run record; AgentPing holds them until the run lands.
:::

### Example

```bash
curl -X POST https://eu.ingest.agentping.io/v1/runs/run_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6/events \
  -H "Authorization: Bearer apk_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "id": "evt_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b7",
        "type": "llm_call",
        "ts": "2026-05-15T14:32:02.456Z",
        "data": {
          "provider": "anthropic",
          "model": "claude-opus-4-7",
          "input_tokens": 1200,
          "output_tokens": 340,
          "latency_ms": 4500
        }
      },
      {
        "id": "evt_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b8",
        "type": "log",
        "ts": "2026-05-15T14:32:03.001Z",
        "data": {"message": "Routed to billing department"}
      }
    ]
  }'
```

## POST /v1/runs/{id}/finish

Close out a run with a terminal status.

### Request

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | Yes | `success`, `failed`, `timeout`, or `cancelled`. |
| `finished_at` | string (ISO 8601, UTC) | Yes | Client clock. |
| `output` | object | No | Run result, arbitrary JSON. |
| `scores` | object | No | Self-reported scores keyed by metric. Numeric or boolean. |

### Response

`202 Accepted` on first write, `200 OK` on idempotent retry. A run already finished with a mismatching payload returns the original finish record unchanged. Downstream (cost finalisation, checks, alerts, judge enqueue) runs asynchronously.

### Example

:::codetabs
```bash
curl -X POST https://eu.ingest.agentping.io/v1/runs/run_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6/finish \
  -H "Authorization: Bearer apk_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "success",
    "finished_at": "2026-05-15T14:32:05.789Z",
    "output": {"department": "billing", "severity": 2},
    "scores": {"confidence": 0.92, "auto_resolved": true}
  }'
```
```python
run.finish(status="success", output={"department": "billing", "severity": 2},
           scores={"confidence": 0.92, "auto_resolved": True})
```
```typescript
run.finish({
  status: "success",
  output: { department: "billing", severity: 2 },
  scores: { confidence: 0.92, autoResolved: true },
});
```
```go
run.Finish(agentping.FinishOptions{
    Status: "success",
    Output: map[string]any{"department": "billing", "severity": 2},
    Scores: map[string]any{"confidence": 0.92, "auto_resolved": true},
})
```
```php
$run->finish(
    status: 'success',
    output: ['department' => 'billing', 'severity' => 2],
    scores: ['confidence' => 0.92, 'auto_resolved' => true],
);
```
:::

## GET /v1/runs/{id}

Retrieve a run's full record: events, scores, check results, judge results, parent/child trace links. Served by the control API. Response shape matches the dashboard's run detail page.
