> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.taktoria.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.taktoria.com/_mcp/server.

# Metric webhooks

> Post a number to an OKR Hub key result's metric webhook, and it becomes the key result's check-in: the body, the secret, test posts and every answer.

A key result in OKR Hub can score itself from the number it measures. Whoever
may update the key result connects a **metric** on its page: a name, a
**baseline** (which scores 0.0), a **target** (1.0) and, for a webhook, an
address with a secret. Each number posted there becomes a grade between the
baseline and the target, linear and kept between 0 and 1, recorded as a
check-in by whoever set the metric up and marked as the metric's.

The webhook sits outside the versioned API: no API key, no scopes, just the
metric's own secret. Metrics are set up, paused and given new secrets on the
key result's page, not through the API, which reads them: a key result's
`metric` says which one drives it, and a check-in's `metric` which one
recorded it.

## Posting a number

```bash
curl -X POST "$METRIC_WEBHOOK_URL" \
  -H "Authorization: Bearer $METRIC_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "value": 42 }'
```

The address (`…/api/okrhub/metrics/<sourceId>`, on Taktoria's own domain) and
the secret (it starts `tkm_`) are on the key result's page.
The secret is shown once, when the metric is connected or given a **New
secret**; keep it like a password, since it lets anyone who holds it move the
key result's score.

| Field   | Type             | Meaning                                                                                                                         |
| ------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `value` | number or string | The number. A string may carry thousands separators or a trailing `%` (`"1,234"`, `"87.5%"`). Required.                         |
| `at`    | ISO 8601 date    | When the number stood there, if not now. A late one goes into the history at its date; more than five minutes ahead is refused. |
| `note`  | string           | Shown on the check-in, up to 500 characters.                                                                                    |
| `test`  | `true`           | Check the secret and the value and answer the grade it would make, recording nothing.                                           |

A good answer is `200`:

```json
{ "ok": true, "recorded": true, "value": 42, "score": 0.42 }
```

`recorded` is `false` when the grade is the one the metric last made: the
number is remembered but adds nothing to the history, so posting a flat metric
every day is fine. A test answers `"test": true` and `"recorded": false`.

## How it sits beside people's check-ins

A person may still check in on a key result a metric drives. Their check-in
stands in the history beside the metric's, and is the key result's latest
until the metric next moves. A **Final** grade on the scorecard is the score
whatever the metric does.

The metric writes as whoever set it up, and only while they could have
written it themselves: still in the workspace, holding an OKR Hub seat, not a
Viewer, and allowed to update the key result. When they no longer are, the
webhook answers `403` until somebody who may update the key result saves its
settings and so takes it over.

## Answers

Errors are a JSON body with one code, `{ "error": "paused" }`, rather than the
versioned API's `error` object.

| Status | Code                       | Meaning                                                                                                                 |
| ------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| 200    |                            | Recorded, or remembered with nothing to record (`recorded: false`), or a test                                           |
| 400    | `invalid_json`             | The body is not JSON                                                                                                    |
| 400    | `value_invalid`            | No number in `value`, or a body that is not an object                                                                   |
| 400    | `at_invalid`               | `at` is not a date, or is more than five minutes ahead                                                                  |
| 401    | `unauthorized`             | No secret, a wrong one, or no such webhook: the three answer alike, so the address says nothing about whether it exists |
| 403    | `configurer_cannot_update` | Whoever set the metric up may no longer update the key result                                                           |
| 409    | `paused`                   | The metric is paused; resume it on the key result's page. A test still answers                                          |
| 413    | `too_large`                | The body is over 16 KB                                                                                                  |
| 429    | `rate_limited`             | A limit is used up: wait `Retry-After` seconds                                                                          |

## Limits

`metric-webhook-ip` allows 600 requests per 10 minutes from one address,
counted before the secret is checked, and `metric-webhook` 60 an hour per
metric once it is. A metric moves daily, not by the second: post when the
number changes. Answers past the secret carry the rate limit headers described
under [Errors and pagination](/getting-started/errors-and-pagination).