Skip to main content

Endpoint

POST /v1/logs Base URL: https://timberlogs-ingest.enaboapps.workers.dev

Supported Formats

The ingestion endpoint accepts logs in multiple formats. Format is resolved by priority:
  1. ?format= query parameter (highest priority)
  2. Content-Type header
  3. Auto-detection via content sniffing (fallback)
FormatContent-Type?format=Description
JSONapplication/jsonjsonDefault. { logs: [...] }, bare array, or single object
JSONLapplication/x-ndjsonjsonlOne JSON object per line
Syslogapplication/x-syslogsyslogRFC 5424 and RFC 3164
Texttext/plaintextOne log per line, auto-extracts timestamp and level
CSVtext/csvcsvHeader row with field names, comma or tab delimited
OBLapplication/x-obloblOpen Board Logging format for AAC device logs

Request

Headers

HeaderRequiredDescription
AuthorizationYesBearer <api-key>
Content-TypeNoFormat hint (see table above). Defaults to application/json

Query Parameters

ParameterDescription
formatExplicit format override: json, jsonl, syslog, text, csv, obl
sourceDefault source for logs that don’t include one
environmentDefault environment for logs that don’t include one
levelDefault level for logs that don’t include one
datasetDefault dataset for logs that don’t include one
Default query parameters are useful for formats like plain text and syslog where source and environment may not be present in the log data.

Body (JSON)

{
  "logs": [
    {
      "level": "info",
      "message": "User signed in",
      "source": "api-server",
      "environment": "production",
      "data": {
        "userId": "user_123"
      }
    }
  ]
}
The JSON parser also accepts a bare array [...] or a single object {...}.

Body (JSONL)

{"level":"error","message":"Connection refused","source":"api","environment":"production"}
{"level":"info","message":"Request completed","source":"api","environment":"production"}
One JSON object per line. Empty lines are skipped.

Body (Syslog)

RFC 5424:
<165>1 2024-01-15T10:30:00.000Z myhost api 1234 - - Connection refused
RFC 3164:
<34>Jan 15 10:30:00 myhost api[1234]: Connection refused
Syslog severity is mapped to log levels: emergency/alert/critical/error → error, warning → warn, notice/info → info, debug → debug. The APP-NAME or TAG becomes source, and HOSTNAME/PROCID are stored in data.

Body (Plain Text)

2024-01-15 10:30:00 ERROR Connection refused to database
2024-01-15 10:30:01 INFO Retrying connection...
One log per line. The parser extracts timestamps (ISO 8601, YYYY-MM-DD HH:MM:SS, MM/DD/YYYY HH:MM:SS) and level keywords (ERROR, WARN, WARNING, INFO, DEBUG, case-insensitive). Use ?source= and ?environment= query params to set required fields.

Body (CSV)

level,message,source,timestamp
error,Connection refused,api,1705312200000
info,Request completed,api,1705312201000
First row is the header. Column names must match log schema field names. Tab-delimited files are also accepted (auto-detected). Quoted fields with commas are supported.

Body (OBL)

{
  "format": "open-board-log-0.1",
  "user_id": "user_abc",
  "source": "my-aac-app",
  "sessions": [
    {
      "id": "sess_1",
      "type": "log",
      "started": "2024-01-15T10:00:00Z",
      "ended": "2024-01-15T11:00:00Z",
      "events": [
        {
          "id": "e1",
          "timestamp": "2024-01-15T10:05:00Z",
          "type": "button",
          "label": "hello",
          "spoken": true
        },
        {
          "id": "e2",
          "timestamp": "2024-01-15T10:06:00Z",
          "type": "utterance",
          "text": "hello world"
        }
      ]
    }
  ]
}
Open Board Logging (.obl) is a standard format for AAC (Augmentative and Alternative Communication) device logs. Each event in each session becomes a separate log entry. Event types are button, utterance, action, and note. Event mapping:
  • button → message: Button: {label}, data includes spoken, button_id, board_id
  • utterance → message: Utterance: {text}, data includes constituent buttons
  • action → message: Action: {action}, data includes destination_board_id, text
  • note → message: Note: {text}, data includes author_name
The OBL user_id maps to userId, device_id maps to sessionId, and session/event metadata is stored in data. The root source field is used as the log source. Use ?source= and ?environment= query params to set defaults. Files may start with a /* ... */ comment block which is stripped before parsing. Auto-detection works by checking for "open-board-log-" in the JSON.

Log Entry Schema

FieldTypeRequiredDescription
levelstringYesLog level: debug, info, warn, error
messagestringYesLog message (1-10,000 characters)
sourcestringYesApplication/service name (1-100 characters)
environmentstringYesdevelopment, staging, or production
datasetstringNoDataset name for grouping (default: default)
versionstringNoApplication version
userIdstringNoUser identifier
sessionIdstringNoSession identifier
requestIdstringNoRequest identifier for correlation
dataobjectNoArbitrary JSON data
errorNamestringNoError class/name
errorStackstringNoError stack trace
tagsstring[]NoArray of tags (max 20 tags, 50 chars each)
timestampnumberNoUnix timestamp in milliseconds
flowIdstringNoFlow identifier for grouping related logs
stepIndexnumberNoStep index within a flow (0-1000)
Required fields can be provided via query parameter defaults when using non-JSON formats.

Batch Limits

  • Minimum: 1 log per request
  • Maximum: 100 logs per request

Response

Success (200)

{
  "success": true,
  "count": 5,
  "timestamp": 1704067200000
}
FieldTypeDescription
successbooleanAlways true on success
countnumberNumber of logs ingested
timestampnumberServer timestamp of ingestion

Error Responses

400 Bad Request - Invalid request body or parse error
{
  "error": "Failed to parse line 3: invalid JSON",
  "code": "PARSE_ERROR"
}
How to resolve: Check the error message for details. For JSONL, the line number is included. For JSON, check the issues array. Common causes: invalid level enum value, missing required fields, malformed input, or exceeding the 100-log batch limit. 401 Unauthorized - Invalid or missing API key
{
  "error": "Invalid API key"
}
How to resolve: See Authentication for details on API key format and usage. 429 Too Many Requests - Rate limit exceeded
{
  "error": "Rate limit exceeded",
  "retryAfter": 60
}
How to resolve: Wait retryAfter seconds before retrying. Consider batching logs into fewer requests or upgrading your plan.

Examples

Basic Log (JSON)

curl -X POST https://timberlogs-ingest.enaboapps.workers.dev/v1/logs \
  -H "Authorization: Bearer tb_live_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "logs": [{
      "level": "info",
      "message": "Application started",
      "source": "my-app",
      "environment": "production"
    }]
  }'

JSONL

curl -X POST https://timberlogs-ingest.enaboapps.workers.dev/v1/logs \
  -H "Authorization: Bearer tb_live_xxxxxxxxxxxxx" \
  -H "Content-Type: application/x-ndjson" \
  -d '{"level":"error","message":"Connection refused","source":"api","environment":"production"}
{"level":"info","message":"Request completed","source":"api","environment":"production"}'

Syslog

curl -X POST 'https://timberlogs-ingest.enaboapps.workers.dev/v1/logs?format=syslog&environment=production' \
  -H "Authorization: Bearer tb_live_xxxxxxxxxxxxx" \
  -d '<165>1 2024-01-15T10:30:00.000Z myhost api 1234 - - Connection refused
<14>1 2024-01-15T10:30:01.000Z myhost api 1234 - - Retrying connection'

Plain Text

curl -X POST 'https://timberlogs-ingest.enaboapps.workers.dev/v1/logs?source=nginx&environment=production' \
  -H "Authorization: Bearer tb_live_xxxxxxxxxxxxx" \
  -H "Content-Type: text/plain" \
  -d '2024-01-15 10:30:00 ERROR Connection refused to database
2024-01-15 10:30:01 INFO Retrying connection...'

CSV

curl -X POST 'https://timberlogs-ingest.enaboapps.workers.dev/v1/logs?environment=production' \
  -H "Authorization: Bearer tb_live_xxxxxxxxxxxxx" \
  -H "Content-Type: text/csv" \
  -d 'level,message,source,timestamp
error,Connection refused,api,1705312200000
info,Request completed,api,1705312201000'

OBL (AAC Device Logs)

curl -X POST 'https://timberlogs-ingest.enaboapps.workers.dev/v1/logs?source=my-aac-app&environment=production' \
  -H "Authorization: Bearer tb_live_xxxxxxxxxxxxx" \
  -H "Content-Type: application/x-obl" \
  -d '{
    "format": "open-board-log-0.1",
    "user_id": "user_abc",
    "sessions": [{
      "id": "sess_1",
      "type": "log",
      "started": "2024-01-15T10:00:00Z",
      "ended": "2024-01-15T11:00:00Z",
      "events": [
        {"id": "e1", "timestamp": "2024-01-15T10:05:00Z", "type": "button", "label": "I", "spoken": true},
        {"id": "e2", "timestamp": "2024-01-15T10:05:01Z", "type": "button", "label": "want", "spoken": true},
        {"id": "e3", "timestamp": "2024-01-15T10:05:02Z", "type": "utterance", "text": "I want juice"}
      ]
    }]
  }'

Log with Data

curl -X POST https://timberlogs-ingest.enaboapps.workers.dev/v1/logs \
  -H "Authorization: Bearer tb_live_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "logs": [{
      "level": "info",
      "message": "User signed in",
      "source": "auth-service",
      "environment": "production",
      "userId": "user_123",
      "sessionId": "sess_abc",
      "data": {
        "method": "oauth",
        "provider": "google"
      },
      "tags": ["auth", "login"]
    }]
  }'

Error Log

curl -X POST https://timberlogs-ingest.enaboapps.workers.dev/v1/logs \
  -H "Authorization: Bearer tb_live_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "logs": [{
      "level": "error",
      "message": "Database connection failed",
      "source": "api-server",
      "environment": "production",
      "errorName": "ConnectionError",
      "errorStack": "ConnectionError: ECONNREFUSED\n    at connect (/app/db.js:42:11)\n    at ...",
      "tags": ["database", "critical"]
    }]
  }'

Batch Logs

curl -X POST https://timberlogs-ingest.enaboapps.workers.dev/v1/logs \
  -H "Authorization: Bearer tb_live_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "logs": [
      {
        "level": "info",
        "message": "Request received",
        "source": "api",
        "environment": "production",
        "requestId": "req_xyz"
      },
      {
        "level": "debug",
        "message": "Processing request",
        "source": "api",
        "environment": "production",
        "requestId": "req_xyz"
      },
      {
        "level": "info",
        "message": "Request completed",
        "source": "api",
        "environment": "production",
        "requestId": "req_xyz",
        "data": {"duration": 145}
      }
    ]
  }'

Flow Tracking

curl -X POST https://timberlogs-ingest.enaboapps.workers.dev/v1/logs \
  -H "Authorization: Bearer tb_live_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "logs": [
      {
        "level": "info",
        "message": "Checkout started",
        "source": "checkout-service",
        "environment": "production",
        "flowId": "checkout-a1b2c3d4",
        "stepIndex": 0
      },
      {
        "level": "info",
        "message": "Payment processed",
        "source": "checkout-service",
        "environment": "production",
        "flowId": "checkout-a1b2c3d4",
        "stepIndex": 1
      }
    ]
  }'

Raw Ingestion via SDK

The TypeScript and Python SDKs provide ingestRaw() / ingest_raw() methods that send pre-formatted data directly to this endpoint. The SDK handles Content-Type headers, query parameter encoding, and retry logic automatically.
await timber.ingestRaw(body, 'csv', { source: 'nginx', environment: 'production' })
This is equivalent to:
curl -X POST 'https://timberlogs-ingest.enaboapps.workers.dev/v1/logs?format=csv&source=nginx&environment=production' \
  -H "Authorization: Bearer tb_live_xxxxxxxxxxxxx" \
  -H "Content-Type: text/csv" \
  -d "$body"
See the TypeScript SDK and Python SDK docs for full details.

Rate Limits

Rate limits depend on your plan:
PlanRequests/minuteLogs/month
Free6010,000
Pro6001,000,000
EnterpriseUnlimitedCustom
When rate limited, you’ll receive a 429 response with a retryAfter value indicating when to retry.

Best Practices

  1. Batch logs - Send multiple logs per request to reduce overhead
  2. Use the SDK - The TypeScript and Python SDKs handle batching and retries automatically
  3. Include context - Add userId, sessionId, and requestId for correlation
  4. Use structured data - Put details in data rather than interpolating into message
  5. Tag appropriately - Use consistent tags for filtering
  6. Handle errors - Implement retry logic with exponential backoff
  7. Use explicit formats - Prefer ?format= or Content-Type over auto-detection for reliability
  8. Set defaults for non-JSON formats - Use ?source= and ?environment= query params when sending plain text or syslog