HTTP API
The Node.js SDK does all of this for you: encryption, queuing, retries and authentication. This page is for sending logs from anywhere else, in any language with AES-256-GCM.
Sending logs is the only public part of the API. Reading them back happens in the Lumber apps, which decrypt them on your own devices; the rest of the API serves those apps and can change without notice.
The base URL is https://api.lumber.infinitetoken.com.
Authentication
Send the token from an app's API key, the part before the dot, in the Authorization header:
Authorization: Bearer <token>
Never send the part after the dot. It's the app's encryption key, which you use to encrypt each log yourself. API keys are created in the Lumber app on your app's Secrets screen. See API Keys.
POST /logs
Sends one log. There's no batch format: send one request per log, as JSON with a Content-Type: application/json header.
{
"level": 3,
"encrypted": {
"nonce": "base64encodedNonce",
"payload": "base64encodedAuthTagAndCiphertext",
"keyVersion": 1
}
}
| Field | Type | Description |
|---|---|---|
level | number | The log's level (see Log levels). The one field Lumber can read, for filtering and push alerts |
encrypted.nonce | string | Base64 of the 12-byte nonce |
encrypted.payload | string | Base64 of the 16-byte authentication tag followed by the ciphertext |
encrypted.keyVersion | number | Always 1 |
The payload
What's encrypted is a JSON object with the log's content:
| Field | Type | Description |
|---|---|---|
level | number | The log's level, the same as the body's |
message | string | The log line |
requestId | string | Optional. Ties the log to one request; the Lumber app shows it under IDs |
traceId | string | Optional. A distributed trace's identifier, also under IDs |
context | object | Optional. Where the log came from (the SDK sends pid, hostname, env, and the caller's file and line when enabled) |
metadata | object | Optional. Anything else about the log |
Encrypting it
- Split the API key at the dot: the token, then the encryption key, base64 for 32 bytes
- Make a random 12-byte nonce for each log, and never reuse one with the same key
- Encrypt the payload's JSON with AES-256-GCM, the key and the nonce, which gives the ciphertext and a 16-byte authentication tag
- Send the nonce, and the tag followed by the ciphertext, each base64-encoded
In Node.js without the SDK, for example:
import { createCipheriv, randomBytes } from 'node:crypto'
const [token, encryptionKey] = process.env.LUMBER_API_KEY.split('.')
const key = Buffer.from(encryptionKey, 'base64')
async function sendLog(level, message, fields = {}) {
const nonce = randomBytes(12)
const cipher = createCipheriv('aes-256-gcm', key, nonce)
const ciphertext = Buffer.concat([cipher.update(JSON.stringify({ level, message, ...fields })), cipher.final()])
const payload = Buffer.concat([cipher.getAuthTag(), ciphertext])
const res = await fetch('https://api.lumber.infinitetoken.com/logs', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ level, encrypted: { nonce: nonce.toString('base64'), payload: payload.toString('base64'), keyVersion: 1 } }),
})
if (!res.ok) throw new Error(`Lumber answered ${res.status}`)
}
await sendLog(3, 'Payment failed', { requestId: 'req-42', metadata: { amount: 1200 } })
Log levels
| Value | Name |
|---|---|
0 | debug |
1 | info (default for lumber.log()) |
2 | warn |
3 | error |
4 | fatal |
Response
201 Created with the stored record:
{
"data": {
"id": "6f1c2a9e-...",
"appId": "0b7d4e12-...",
"level": 2,
"encrypted": {
"nonce": "base64encodedIV",
"payload": "base64encodedAuthTagAndCiphertext",
"keyVersion": 1
},
"createdAt": "2026-10-15T14:32:01.452Z"
},
"metadata": {
"requestId": "a3e1c7d0-...",
"durationMs": 12
}
}
Errors
| Status | Description |
|---|---|
400 | Malformed JSON, or a missing or invalid field (level, encrypted, encrypted.nonce, encrypted.payload, encrypted.keyVersion) |
401 | Missing Authorization header, or an invalid or deleted API key |
413 | Request body larger than 100 KB |
500 | Server error |
Response format
Successful responses use a consistent envelope:
{
"data": { ... },
"metadata": {
"requestId": "string",
"durationMs": 0
}
}
requestId echoes the X-Request-Id request header when you send one, and is a generated UUID otherwise.
Error responses use standard HTTP status codes with a JSON body containing a message:
{
"message": "Invalid token"
}
Server errors (5xx) always return { "message": "Internal Server Error" }.