How to Send Logs to Carpathian Logger
Send your application logs to Carpathian Logger with one HTTP request easily.
To send logs to Carpathian Logger, you make an HTTP POST to your log service's ingest URL with the service's key in an Authorization: Bearer header and your events as JSON in the body. Each request is one batch. Carpathian writes the batch as a single compressed file in your log service's Object Storage bucket and answers with how many events it accepted. You don't name the service or the bucket anywhere in the request, because the key already says which one it belongs to.
This guide covers getting a key, the body formats you can send, working examples, the limits, and what each response tells you.
Before you start
You need a log service. Open Logging in the dashboard and create one. Creating a service does three things at once: it makes a bucket for your logs in Object Storage, it sets the service's limits, and it gives you an ingest key.
The key starts with cpk_ and is shown once, when you create the service. Copy it somewhere safe, like your application's secret store or environment variables. If you lose it, open the service, go to Settings, and use Rotate key under Ingest key. Rotating gives you a new key and stops the old one working straight away, so update your application before you rotate if it's running.
The ingest URL is on the service's Overview tab, with a copy button. It ends in /api/v1/logs and points at the region your service lives in, because your logs are stored there. Use the URL from that tab rather than one from another service, since a key sent to a different region's address is refused. The examples below read it from a CARPATHIAN_LOG_URL environment variable.
Sending your first event
Here's the smallest useful request:
curl -X POST "$CARPATHIAN_LOG_URL" \
-H "Authorization: Bearer $CARPATHIAN_LOG_KEY" \
-H "Content-Type: application/json" \
-d '{"events": [{"level": "info", "message": "Service started"}]}'
If it worked, you get a 202 back:
{"success": true, "accepted": 1, "object": "3f9c2a7b1d4e8f60"}
accepted is the number of events stored from your batch, and object identifies the file they were written to. Open the service's Events tab and the event is there.
What an event looks like
An event is a JSON object. Four keys have a meaning of their own:
messageis the text of the log line.levelis whatever severity you use, such asdebug,info,warn, orerror. It's stored as text, so any label works.atis when the event happened. Send it as an ISO 8601 timestamp (2026-09-28T14:03:11Z) or as seconds since the Unix epoch (1790604191). If you leave it out, the event is stamped with the time it arrived.fieldsis an object for anything structured you want to keep with the event.
Every other key you send is kept too. It's moved into fields rather than dropped, so you can log objects in whatever shape your application already has:
{
"at": "2026-09-28T14:03:11Z",
"level": "error",
"message": "Payment failed",
"order_id": 48213,
"customer_region": "us-east",
"fields": {"gateway": "card", "attempt": 2}
}
That event is stored with order_id, customer_region, gateway, and attempt all under fields.
If an at value can't be read as a timestamp, Carpathian keeps your original value under fields.at and stamps the event with its arrival time, so nothing you sent is lost.
You don't have to send objects at all. A plain string is stored as the event's message with no level.
Body formats you can send
Send whichever of these is easiest from your application. They all end up stored the same way.
- An object with an
eventsarray, like the examples above. This is the clearest choice if you're writing the code yourself. - A bare JSON array of events:
[{"message": "one"}, {"message": "two"}]. - A single JSON object, which is stored as one event.
- Newline-delimited JSON (NDJSON), one event per line. Set
Content-Type: application/x-ndjson. Most logging libraries can write this format already. A line that isn't valid JSON is kept as a plain message instead of failing the batch.
The body has to be UTF-8.
To cut bandwidth, gzip the body and add Content-Encoding: gzip. Log data compresses well, so for high-volume services this is worth doing:
gzip -c events.ndjson | curl -X POST "$CARPATHIAN_LOG_URL" \
-H "Authorization: Bearer $CARPATHIAN_LOG_KEY" \
-H "Content-Type: application/x-ndjson" \
-H "Content-Encoding: gzip" \
--data-binary @-
Examples in Python and Node
In Python, with the requests library:
import os
import requests
def ship(events):
response = requests.post(
os.environ["CARPATHIAN_LOG_URL"],
headers={"Authorization": f"Bearer {os.environ['CARPATHIAN_LOG_KEY']}"},
json={"events": events},
timeout=10,
)
response.raise_for_status()
return response.json()["accepted"]
ship([
{"level": "info", "message": "Job finished", "job": "nightly-export", "rows": 1204},
])
In Node 18 or later, with the built-in fetch:
async function ship(events) {
const response = await fetch(process.env.CARPATHIAN_LOG_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CARPATHIAN_LOG_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ events }),
});
if (!response.ok) {
throw new Error(`Log shipping failed: ${response.status} ${await response.text()}`);
}
return (await response.json()).accepted;
}
await ship([{ level: "warn", message: "Cache miss rate above 20%", service: "api" }]);
Batch your events
Every request becomes one stored file, and each service has a limit on requests per minute, not on events. So send events in batches rather than one request per line. A common pattern is to buffer events in memory and flush every few seconds, or as soon as the buffer reaches a few hundred events, whichever comes first. Flush once more when your application shuts down so the last batch isn't lost.
Limits
Each service has two limits you can change on its Settings tab, under Ingest limits:
- Requests per minute starts at 600. You can set it anywhere from 1 to 60,000.
- Events per request starts at 1,000. You can set it anywhere from 1 to 10,000.
Two limits apply to every request regardless of settings. The body can be at most 5 MB as sent, and a gzipped body can expand to at most 64 MB. Split anything larger into several batches.
The requests-per-minute limit belongs to each service on its own, so a noisy application can't use up another service's allowance. If you run several applications, give each its own service.
What each response means
A 202 means the batch was stored. A 4xx response means nothing from that batch was stored, so retrying it after fixing the cause won't create duplicates. Every error comes back as JSON with an error field explaining what went wrong.
- 400 means the body couldn't be read: it's empty, it isn't UTF-8, or it says it's gzipped when it isn't. Fix the body before retrying.
- 401 means the key is missing, mistyped, or was rotated. Check the header is exactly
Authorization: Bearerfollowed by the key. - 403 means the key is valid but isn't allowed here. The request went to a different region's address than the one on the service's Overview tab, or the key doesn't belong to a log service, or the request came from an address your key or your organization's firewall doesn't allow.
- 409 means the service can't take events right now. Either it's paused (use Resume ingest on the Settings tab), or its bucket was deleted, in which case there's nowhere left to write and you'll need a new service.
- 413 means the batch is too big: over 5 MB, over 64 MB once decompressed, over the service's events-per-request setting, or the bucket is at its storage quota. Send smaller batches, raise the setting, or make room in the bucket.
- 429 means you're over the service's requests-per-minute limit. The response carries a
Retry-Afterheader with the number of seconds until the next minute starts. Wait that long, then send again, or batch more events per request. - 503 means logging isn't available in the region your account is using.
For a shipper that runs unattended, retry 429 and 5xx responses after a short wait and set aside batches that get a 400, since sending those again won't change the answer. A retried 5xx can occasionally store a batch twice, because the error may arrive after the file was written.
Where your logs are stored
Each accepted batch is stored as one gzipped file of newline-delimited JSON in the service's bucket, in a folder per day (2026/09/28/), named by the time it arrived. Each line is one event with its at, level, message, fields, and the time Carpathian received it.
You can read recent events on the service's Events tab, or open the bucket in Object Storage and download the files to search or process them with your own tools. Because the files are ordinary gzipped JSON lines, standard command-line tools and most log processors read them directly.
You pay the standard Object Storage rate for what the bucket holds. There's no charge per event or per request. Logs aren't deleted automatically, so if you only need to keep a few weeks, delete older folders from the bucket when you no longer need them.
Deleting a log service revokes its key but leaves the bucket and every log in it, so you don't lose stored data by removing a service. Delete the bucket from Object Storage if you want the logs gone too.
Keeping the key safe
Treat the ingest key like a password. Keep it in environment variables or a secret manager, never in source control or in code that runs in a browser. The key can only write logs to its own service, so a leaked key can't read your data or reach anything else in your account, but someone holding it could fill your bucket. If you think a key has leaked, rotate it from the service's Settings tab.