Documentation / Connect
HTTP API
Send errors, logs, metrics and page views with plain HTTP requests, without an SDK.
The SDKs are a convenience: underneath, every one of them makes the HTTP requests on this page. Use the API directly from a language without an SDK, from a script or from CI.
Before you start
You need three things, all from the project settings in CloseYourIt:
- the address of your install, for example
https://bugs.example.com; - the project id;
- an ingest token (
cyi_…).
The token is secret and stays on the server. ingest allows sending; private reads require the separate read scope. Browser and mobile clients use a public key for supported signals, without reading or administration access. Available signals depend on the SDK and version; see JavaScript SDK.
Every request carries the token and sends JSON, with field names in snake_case:
Authorization: Bearer cyi_...
Content-Type: application/json
Where to send
| What | Address | Body | Answer |
|---|---|---|---|
| Error | POST /api/v1/projects/<id>/events | one event | 202 {"data":{"id":"…"}} |
| Performance | POST /api/v1/projects/<id>/metrics | one sample or a list | 202 {"data":{"accepted":N}} |
| Logs | POST /api/v1/projects/<id>/logs | one entry or a list | 202 {"data":{"accepted":N}} |
| Page views | POST /api/v1/projects/<id>/pageviews | one visit or a list | 202 {"data":{"accepted":N}} |
| Session replay | POST /api/v1/projects/<id>/replays | one chunk or a list | 202 {"data":{"accepted":N}} |
The <id> in the address must be the project the token belongs to.
202 means accepted, not saved: the data is written a moment later by a background job. accepted counts the items that passed the first checks.
Send an error
The body is a Sentry-style event.
curl -X POST https://bugs.example.com/api/v1/projects/$PROJECT_ID/events \
-H "Authorization: Bearer $CYI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_id": "fc6d8c0c43fc4630ad850ee518f1b9d0",
"timestamp": "2026-10-03T10:15:30Z",
"level": "error",
"platform": "ruby",
"environment": "production",
"release": "v1.4.2",
"exception": { "values": [ {
"type": "RuntimeError",
"value": "boom",
"stacktrace": { "frames": [
{ "filename": "app/models/order.rb", "function": "charge", "lineno": 42, "in_app": true }
] }
} ] }
}'
| Field | Notes |
|---|---|
event_id | your own unique id; sending the same one twice does not create a second error |
timestamp | ISO 8601 or seconds since 1970; defaults to now |
level | debug, info, warning, error or fatal; defaults to error |
exception.values[] | each with type, value and stacktrace.frames[]; the last one is read |
message | used as the title when there is no exception |
environment, release, server_name, platform | optional |
user | id, email, ip_address: only a hash of them is kept |
request, breadcrumbs, tags, extra, contexts | optional; values under sensitive keys are replaced with [FILTERED] |
trace_id | links the error to logs of the same request |
fingerprint | a list that overrides how errors are grouped |
Errors are grouped by exception type and place in the code, not by the message text.
Send logs
curl -X POST https://bugs.example.com/api/v1/projects/$PROJECT_ID/logs \
-H "Authorization: Bearer $CYI_TOKEN" \
-H "Content-Type: application/json" \
-d '[
{ "timestamp": "2026-10-03T10:15:30Z", "level": "info", "message": "order charged",
"logger": "billing", "attributes": { "order_id": 7 } }
]'
| Field | Notes |
|---|---|
message | required, not empty. It is stored as you send it: do not put secrets in it |
level | debug, info, warning, error or fatal; anything else becomes info |
timestamp | ISO 8601 or seconds since 1970 |
logger | the name of the part of your app that wrote it |
attributes | any object; values under sensitive keys are filtered |
event_id, trace_id, environment, release | optional |
Send performance samples
curl -X POST https://bugs.example.com/api/v1/projects/$PROJECT_ID/metrics \
-H "Authorization: Bearer $CYI_TOKEN" \
-H "Content-Type: application/json" \
-d '[
{ "kind": "slow_query", "duration_ms": 812.4,
"sql": "SELECT * FROM orders WHERE id = 7", "environment": "production" },
{ "kind": "slow_method", "duration_ms": 240.0, "label": "Invoice#render" }
]'
| Field | Notes |
|---|---|
kind | required: slow_query, slow_method or performance_issue |
duration_ms | how long it took |
sql | for slow_query; numbers and ids are replaced so equal queries group together |
label | for slow_method; required, it is what samples are grouped by |
subtype | for performance_issue; required: n_plus_one, slow_request, slow_external_http, high_query_count, jank, repeated_http or rebuild_storm |
sample_id | your own unique id (a UUID), for safe retries |
occurred_at, environment, trace_id | optional |
Send page views
curl -X POST https://bugs.example.com/api/v1/projects/$PROJECT_ID/pageviews \
-H "Authorization: Bearer $CYI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "hostname": "www.example.com", "path": "/pricing",
"referrer": "https://www.google.com/", "utm_source": "newsletter" }'
| Field | Notes |
|---|---|
hostname, path | required. path never includes the query string |
name | leave it out for a page view; any other name is a custom event |
referrer | only its host name is kept |
utm_source, utm_medium, utm_campaign, utm_term, utm_content | optional |
screen_width | in pixels; stored only as a class (mobile, tablet, laptop, desktop) |
event_id, occurred_at, environment | optional |
No cookie is used. The visitor is counted from a hash that changes every day and cannot be turned back into a person. A request that looks like a bot gets 202 with accepted: 0.
Page views need the secret token, so this address is called from your server, not from the browser. For a website, use the JavaScript SDK.
Limits
| Limit | Value |
|---|---|
| Logs in one request | 1000 |
| Performance samples in one request | 1000 |
| Page views in one request | 100 |
| Replay chunks in one request | 50 |
| Requests per minute, errors, logs, metrics (per project, each) | 1200 |
| Requests per minute, page views, replays (per project, each) | 600 |
| Requests per minute from one IP address, whole API | 300 |
Over a size limit the answer is 413 and nothing is saved. Over a rate limit the answer is 429 with a Retry-After header, in seconds.
With the ingest gateway on, one request can be at most 5 MB.
When something goes wrong
An error answer looks like this:
{ "error": { "code": "R422-LOG-004", "message": "…" } }
| Answer | Meaning | Send again? |
|---|---|---|
202 | accepted | no |
401 | token missing, wrong or revoked | no: fix the token |
403 | the token is not allowed to do this | no |
404 | the project in the address is not the token's project | no: fix the address |
413 | too many items in one request | no: send smaller batches |
422 | the body is not valid JSON, or no item in it is valid | no: fix the body |
429 | too many requests | yes, after Retry-After seconds |
500, 502, 503, 504, or no answer | the app is busy or restarting | yes, with growing pauses |
To retry safely, keep the same event_id or sample_id: CloseYourIt then ignores the copy if the first attempt had arrived. Replay chunks are the exception: a retried chunk can be stored twice.
Read data back
The same token reads the project's data. Each address returns the latest 25 items.
| Address | Filters |
|---|---|
GET /api/v1/error_groups | status: unresolved, resolved, ignored |
GET /api/v1/error_groups/<id> | |
GET /api/v1/metric_groups | kind: slow_query, slow_method, performance_issue |
GET /api/v1/log_entries | level, trace_id, environment |
GET /api/v1/projects/<id>/analytics | range: 24h, 7d, 30d, 1y; environment |
curl -H "Authorization: Bearer $CYI_TOKEN" \
"https://bugs.example.com/api/v1/error_groups?status=unresolved"
To mark an error as resolved: PUT /api/v1/error_groups/<id>/resolution. To reopen it: DELETE on the same address.
Coming from Sentry
Applications that already use a Sentry SDK do not need this page: they keep their SDK and change one setting. See Sentry SDK.