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
  }
}
FieldTypeDescription
levelnumberThe log's level (see Log levels). The one field Lumber can read, for filtering and push alerts
encrypted.noncestringBase64 of the 12-byte nonce
encrypted.payloadstringBase64 of the 16-byte authentication tag followed by the ciphertext
encrypted.keyVersionnumberAlways 1

The payload

What's encrypted is a JSON object with the log's content:

FieldTypeDescription
levelnumberThe log's level, the same as the body's
messagestringThe log line
requestIdstringOptional. Ties the log to one request; the Lumber app shows it under IDs
traceIdstringOptional. A distributed trace's identifier, also under IDs
contextobjectOptional. Where the log came from (the SDK sends pid, hostname, env, and the caller's file and line when enabled)
metadataobjectOptional. Anything else about the log

Encrypting it

  1. Split the API key at the dot: the token, then the encryption key, base64 for 32 bytes
  2. Make a random 12-byte nonce for each log, and never reuse one with the same key
  3. Encrypt the payload's JSON with AES-256-GCM, the key and the nonce, which gives the ciphertext and a 16-byte authentication tag
  4. 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

ValueName
0debug
1info (default for lumber.log())
2warn
3error
4fatal

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

StatusDescription
400Malformed JSON, or a missing or invalid field (level, encrypted, encrypted.nonce, encrypted.payload, encrypted.keyVersion)
401Missing Authorization header, or an invalid or deleted API key
413Request body larger than 100 KB
500Server 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" }.