---
title: "Monitoring HTTP Ingest API"
description: "Send metrics, errors, events and heartbeats to Silly Development monitoring from any language with plain JSON over HTTPS, without one of the SDKs."
url: "https://sillydev.co.uk/docs/monitoring/ingest-api"
updated: "2026-10-14"
---

Monitoring

# HTTP ingest API

Everything the SDKs do is one JSON request every few seconds. Send the same from any language, with a server key.

[← Monitoring docs](https://sillydev.co.uk/docs/monitoring) · Updated 14 October 2026

**Browse the docs**

## Heartbeats from a cron job

The simplest use: tell the project a job ran. An alert can then tell you when it stops. The body is optional and names the service.

curl

```
curl -fsS -X POST "https://ingest1.sillydev.co.uk/v1/heartbeat" \
  -H "Authorization: Bearer $SILLYDEV_MONITORING_KEY" \
  -H "Content-Type: application/json" \
  -d '{"service": "backup-job"}'
```

## Sending a batch

`POST /v1/batch` takes a `resource` naming the app and arrays of `metrics`, `errors`, `events` and `slow` operations, any of which can be left out. Timestamps (`ts`) are Unix seconds and must be within the last hour. Use the address for your key's node: `sdevm_1_…` sends to `https://ingest1.sillydev.co.uk`.

A **counter** item has the amount it went up in a window, a **gauge** its latest value, and a **histogram** a `count`, `sum`, `min`, `max` and 16 `buckets` counting values up to 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000 and 60000 ms, and above. An **error** has a `type`, a `message` and a `stack` of frames, innermost first, each with `file`, `function`, `line` and `in_app`.

curl

```
curl -X POST "https://ingest1.sillydev.co.uk/v1/batch" \
  -H "Authorization: Bearer $SILLYDEV_MONITORING_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "resource": {"service": "orders-bot", "environment": "production", "release": "1.4.2"},
    "heartbeat": true,
    "metrics": [{"name": "orders.created", "type": "counter", "ts": 1760390400, "value": 3, "tags": {"plan": "free"}}],
    "errors": [{"ts": 1760390405, "type": "KeyError", "message": "user_id", "handled": false,
                "stack": [{"file": "bot/orders.py", "function": "on_order", "line": 88, "in_app": true}]}],
    "events": [{"ts": 1760390410, "name": "deploy", "attributes": {"version": "1.4.2"}}]
  }'
```

## Responses and limits

A `202` says what was accepted and what was dropped and why, for example `{"accepted": {"metrics": 1}, "dropped": {"series_limit": 2}}`. A malformed item is dropped without failing the rest.

`401` means the key is unknown or revoked, and `403` that the project was disabled: stop sending. `413` means the body was over 256 KiB as sent (gzip is welcome) or 1 MiB unpacked, or held more than 500 items. `429` means the project is over its requests a minute; wait for the `Retry-After` seconds. `503` means storage is briefly unavailable; keep the data and try again later. Errors come as `{"error": "…", "code": "…"}`.

Read next

- [OpenTelemetry Metrics, traces and logs from any OpenTelemetry SDK or collector.](https://sillydev.co.uk/docs/monitoring/opentelemetry)
- [Limits and retention What every project can send, and how long it is kept.](https://sillydev.co.uk/docs/monitoring/limits)
- [Keys Server keys, browser keys and the websites allowed to use them.](https://sillydev.co.uk/docs/monitoring/keys)
