Skip to content
Codeskop

API Reference: Ingest API

The HTTPS API every Codeskop SDK uses. Call it directly to send errors and API timings from a language or runtime we don't have an SDK for yet, such as a backend service, a worker or a script.

Prefer an SDK when there is one

The SDKs queue events, retry, sample and redact for you. Use the API directly when you need full control or there's no SDK for your platform. The contract below is stable: we add optional fields, we don't remove or rename them.

Quick example

Send one exception
curl -X POST https://api.codeskop.com/v1/events \  -H "Authorization: Bearer cs_live_pk_…" \  -H "Content-Type: application/json" \  -d '{    "sent_at": "2026-10-05T12:00:00Z",    "context": {      "device": { "platform": "python", "hostname": "api-1" },      "app": { "release": "1.4.2", "environment": "production" }    },    "batch": [{      "event_id": "0f8fad5b-d9cb-469f-a165-70867728950e",      "type": "exception",      "severity": "high",      "occurred_at": "2026-10-05T11:59:58Z",      "user": { "id": "user_8412" },      "payload": {        "exception_class": "ValueError",        "message": "Order total can't be negative",        "stacktrace": [          { "class": "app.orders", "method": "create_order", "file": "app/orders.py", "line": 42 },          { "class": "app.views", "method": "checkout", "file": "app/views.py", "line": 118 }        ]      }    }]  }'

Endpoint and authentication

OptionTypeDescription
POST /v1/eventsJSON bodySends a batch of events. Base URL https://api.codeskop.com.
AuthorizationheaderBearer cs_live_pk_… or cs_test_pk_…: the project's public key from API tokens. Secret keys (_sk_) are always refused with 403.
Content-Typeheaderapplication/json
Content-EncodingheaderOptional gzip. Recommended: limits apply to the compressed size.

The key decides the project and environment, so you never send those in the body. Use a test key while developing; test environments have shorter retention and don't count toward billing.

The envelope

{  "sent_at": "2026-10-05T12:00:00Z",   // when this request left your server (UTC)  "context": {    "device": { ... },                  // where it ran: platform, hostname, os, runtime    "app":    { ... }                   // what ran: release, environment, package  },  "batch": [ <event>, <event>, ... ]    // 1 to 100 events}

context describes where the events came from and applies to every event in the batch. Both device and app are required objects but may be empty; unknown fields are kept. Useful fields for servers: device.platform (for example python, node, go), device.hostname, device.os, app.release and app.environment.

Each event

{  "event_id":    "uuid",                // unique per event; used to drop duplicates  "type":        "exception",           // see Event types  "severity":    "low" | "medium" | "high" | "critical",  "occurred_at": "2026-10-05T11:59:58Z",  "user":        { "id": "user_8412" }, // optional: your own stable user ID  "payload":     { ... }                // depends on the type}

Send a new event_id (a UUID) for every event and reuse it when you retry: we drop duplicates for 48 hours, so a retried batch is never counted twice. If your server clock is off, we correct occurred_at using the difference between sent_at and when we received the request.

Event types

OptionTypeDescription
exceptionerrorA handled or unhandled error. Payload: exception_class, message, stacktrace (list of frames with class, method, file, line; innermost first). Every plan.
crasherrorA process crash. Same payload as exception. Every plan.
api_errornetworkA failed outgoing call: status 400 or more, or a network error. Payload below. Starter and up.
api_timingnetworkEvery outgoing call (sampled). Same payload without error_kind. Starter and up.
track / screen / identifyanalyticsProduct analytics. See Tracking. Growth and Scale.
heartbeatpresenceKeeps sessions and live users accurate. Free; doesn't count toward your quota.

Events your plan doesn't include are accepted and quietly dropped, so upgrading never requires a code change.

How errors are grouped

We group exceptions by exception_class plus the class and method of the top five frames. Line numbers don't affect grouping, so a deploy that shifts lines keeps the same issue. Without a stacktrace, all errors of one class end up in one issue.

Network payload

{  "event_id": "…", "type": "api_error", "severity": "high", "occurred_at": "…",  "payload": {    "method": "POST",    "host": "payments.example.com",    "path": "/v1/charges",    "status": 502,    "duration_ms": 1840,    "error_kind": "http_5xx"            // api_error only  }}

Send path without the query string. We turn IDs into placeholders (/users/42 becomes /users/{id}) so each endpoint is one row on the Network page. Optional: request_bytes, response_bytes and timing (dns, connect, tls, ttfb, total in ms). error_kind is one of http_4xx, http_5xx, network_error, timeout, aborted.

Limits

OptionTypeDescription
Request size1 MBMeasured before decompression. 16 MB after decompression.
Events per request100Larger batches are refused with 400.
Event size64 KBA larger event is skipped and its ID returned in rejected; the rest of the batch is kept.
Requests per minuteper keyFree 60, Starter 300, Growth 1,200, Scale 6,000.
Monthly eventsper planOver your quota, events are dropped but the request still succeeds. See Plans.

Responses and retries

OptionTypeDescription
200acceptedBody {}, or {"rejected": ["event_id", …]} listing events that were skipped. Don't resend those.
400invalidBad JSON, a bad envelope or too many events. The body explains why in errors. Fix it; don't retry.
401 / 403keyUnknown, revoked or secret key, a key without ingest access, or a suspended account. Don't retry.
413too largeSplit the batch and send the parts.
429 / 503try laterRate limited or briefly unavailable. Wait for Retry-After seconds (default 5), then send the same batch again.
Python, with gzip and retry handling
import gzip, json, uuid, datetime, requests def now():    return datetime.datetime.now(datetime.timezone.utc).isoformat() body = {    "sent_at": now(),    "context": {"device": {"platform": "python"}, "app": {"release": "1.4.2"}},    "batch": [{        "event_id": str(uuid.uuid4()),        "type": "exception",        "severity": "high",        "occurred_at": now(),        "payload": {"exception_class": "ValueError", "message": "Order total can't be negative"},    }],} r = requests.post(    "https://api.codeskop.com/v1/events",    data=gzip.compress(json.dumps(body).encode()),    headers={        "Authorization": "Bearer cs_live_pk_…",        "Content-Type": "application/json",        "Content-Encoding": "gzip",    },    timeout=10,)if r.status_code in (429, 503):    retry_after = int(r.headers.get("Retry-After", "5"))  # wait, then send the same batch again

Remote config

SDKs read GET /v1/config with the same key to learn which features your plan and project settings allow and how much to sample. Cache it: the response has an ETag and Cache-Control: max-age=300.

GET https://api.codeskop.com/v1/configAuthorization: Bearer cs_live_pk_…{  "enabled": true,  "sample_rates": { "api_timing": 0.2 },  "features": { "anr": true, "network": true, "analytics": false },  "max_queue_mb": 10}

Privacy

Never send request or response bodies, passwords or tokens. As a safety net we replace the value of any payload or user field whose name looks like a secret (password, token, authorization, api_key, card number, cvv, ssn and similar) before it's stored. Use your own user IDs, not emails. More in Privacy & Security.