Ingestion API

Multipart/mixed ingestion format for sending encrypted logs to LogFlux

The LogFlux Ingestion API accepts encrypted log entries via POST /v1/ingest using the multipart/mixed format. This is the recommended way to send logs to LogFlux from agents, SDKs, and custom integrations.

Overview

EndpointPOST /v1/ingest
Content-Typemultipart/mixed; boundary=<boundary>
AuthenticationAPI Key: Authorization: Bearer {region}-lf_<key>
Entries per request1 - 1000
Max request size10 MiB
Max entry size1 MiB

Each MIME part carries one log entry as raw binary ciphertext in the body, with metadata in MIME headers. This eliminates the 33% overhead of base64 encoding used by the deprecated JSON format.

Note: JSON ingestion (application/json) and the standalone batch endpoint (POST /v1/batch) are deprecated. Use multipart/mixed for all new integrations.

Before You Start: Encryption Handshake

Before sending log entries, your client must complete a one-time encryption handshake to establish an AES-256 session key. See Security & Encryption for the full protocol.

Quick summary:

  1. POST /v1/handshake/init with your API key – server returns your RSA public key
  2. Generate a random AES-256 key locally
  3. Encrypt the AES key with your RSA public key (RSA-OAEP-SHA256)
  4. POST /v1/handshake/complete with the encrypted AES key – server returns a key_id UUID
  5. Use the key_id and AES key for all subsequent log entries

The LogFlux Agent and SDKs handle this automatically.

Request Format

Single Entry

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
POST /v1/ingest HTTP/1.1
Host: api.ingest.eu.logflux.io
Authorization: Bearer eu-lf_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Content-Type: multipart/mixed; boundary=logflux

--logflux
X-LF-Entry-Type: 1
X-LF-Payload-Type: 1
X-LF-Key-ID: 7f8a9b0c-1d2e-3f4a-5b6c-7d8e9f0a1b2c
X-LF-Nonce: YWJjZGVmZ2hpamts
X-LF-Timestamp: 2026-03-15T14:30:45.123Z

<raw binary AES-GCM ciphertext>
--logflux--

Batch (Multiple Entries)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /v1/ingest HTTP/1.1
Host: api.ingest.eu.logflux.io
Authorization: Bearer eu-lf_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Content-Type: multipart/mixed; boundary=logflux

--logflux
X-LF-Entry-Type: 1
X-LF-Payload-Type: 1
X-LF-Key-ID: 7f8a9b0c-1d2e-3f4a-5b6c-7d8e9f0a1b2c
X-LF-Nonce: YWJjZGVmZ2hpamts
X-LF-Timestamp: 2026-03-15T14:30:45.123Z

<raw binary ciphertext for entry 1>
--logflux
X-LF-Entry-Type: 3
X-LF-Payload-Type: 1
X-LF-Key-ID: 7f8a9b0c-1d2e-3f4a-5b6c-7d8e9f0a1b2c
X-LF-Nonce: bm9uY2UxMjM0NTY=
X-LF-Timestamp: 2026-03-15T14:30:45.456Z

<raw binary ciphertext for entry 2>
--logflux--

MIME Headers

Each MIME part supports the following headers:

HeaderRequiredDefaultDescription
X-LF-Key-IDYes (types 1-6)UUID of the AES key from handshake completion
X-LF-NonceYes (types 1-6)Base64-encoded 12-byte nonce for AES-GCM (must be unique per entry)
X-LF-Entry-TypeNo1Entry type: 1-7 (see table below)
X-LF-Payload-TypeNo1Payload encoding (see table below)
X-LF-TimestampNoCurrent timeRFC3339 timestamp
X-LF-Search-TokensNoComma-separated HMAC tokens for searchable fields

Part Body

The body of each MIME part is raw binary – the AES-256-GCM ciphertext output directly, with no base64 encoding.

Entry Types

TypeNameCategoryEncryptionDescription
1LogEventsE2E (AES-256-GCM)Application and system logs
2MetricEventsE2E (AES-256-GCM)Numeric measurements
3TraceTracesE2E (AES-256-GCM)Distributed trace spans
4EventEventsE2E (AES-256-GCM)Application events
5AuditAuditE2E (AES-256-GCM) + Object LockCompliance logs (365-day undeletable retention)
6TelemetryTracesE2E (AES-256-GCM)E2E encrypted telemetry
7TelemetryManagedTracesServer-side (S3 SSE-KMS)Server-encrypted telemetry

Entry types affect pricing category and storage behavior. Types 1-6 use client-side encryption (require X-LF-Key-ID and X-LF-Nonce). Type 7 uses server-side encryption only.

Payload Types

ValueNameUsed ByDescription
1aes256-gcm-gzip-jsonTypes 1-6 (default)AES-GCM encrypted, gzip-compressed JSON
2aes256-gcm-zstd-jsonTypes 1-6AES-GCM encrypted, zstd-compressed JSON
3gzip-jsonType 7 onlyGzip-compressed JSON (no client encryption)
4zstd-jsonType 7 onlyZstd-compressed JSON (no client encryption)

Response Format

Single Entry Response

A request with exactly one MIME part returns:

1
2
3
4
5
6
7
8
9
{
  "status": "success",
  "message": "Log entry ingested successfully",
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-03-15T14:30:45.200Z",
  "data": {
    "entry_id": "1234567890123456789"
  }
}

Batch Response

A request with multiple MIME parts returns:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "status": "success",
  "message": "Batch ingestion complete",
  "request_id": "550e8400-e29b-41d4-a716-446655440001",
  "timestamp": "2026-03-15T14:30:45.300Z",
  "data": {
    "total": 50,
    "successful": 49,
    "failed": 1,
    "entries": [
      {"index": 0, "entry_id": "1234567890123456789", "status": "success"},
      {"index": 1, "entry_id": "1234567890123456790", "status": "success"}
    ],
    "errors": [
      {"index": 12, "error": "invalid nonce length", "code": "VALIDATION_ERROR"}
    ]
  }
}

Partial failures are reported per-entry. Successfully ingested entries are not rolled back.

Encryption Handshake

Step 1: Initialize

1
2
3
4
5
6
7
8
POST /v1/handshake/init HTTP/1.1
Host: api.ingest.eu.logflux.io
Authorization: Bearer eu-lf_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Content-Type: application/json

{
  "api_key": "eu-lf_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}

Response includes your RSA public key and ingestion limits:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "status": "success",
  "data": {
    "public_key": "-----BEGIN PUBLIC KEY-----\nMIICIjANBg...\n-----END PUBLIC KEY-----",
    "encryption_mode": 1,
    "max_payload_size": 1048576,
    "max_batch_size": 1000,
    "supports_multipart": true
  }
}

Step 2: Complete

Generate an AES-256 key, encrypt it with the RSA public key (OAEP-SHA256), then send:

1
2
3
4
5
6
7
8
9
POST /v1/handshake/complete HTTP/1.1
Host: api.ingest.eu.logflux.io
Authorization: Bearer eu-lf_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Content-Type: application/json

{
  "api_key": "eu-lf_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "encrypted_secret": "<base64-encoded RSA-encrypted AES key>"
}

Response returns the key ID to use in X-LF-Key-ID:

1
2
3
4
5
6
{
  "status": "success",
  "data": {
    "key_id": "7f8a9b0c-1d2e-3f4a-5b6c-7d8e9f0a1b2c"
  }
}

Encryption Flow Per Entry

For each log entry (types 1-6):

  1. Generate a cryptographically random 12-byte nonce (must be unique per entry)
  2. Compress the log payload (gzip or zstd)
  3. Encrypt with AES-256-GCM using your session key and the nonce
  4. Set X-LF-Key-ID to the key UUID from handshake
  5. Set X-LF-Nonce to the base64-encoded nonce
  6. Put the raw ciphertext (including GCM auth tag) in the MIME part body

Rate Limits

LimitValue
Requests per minute600 (default, configurable per customer)
Burst100 requests
Max entries per request1000
Max request size10 MiB
Max entry payload1 MiB

Rate limit headers are included in responses:

1
2
3
4
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 599
X-RateLimit-Reset: 1710518445
Retry-After: 30

Error Codes

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid request format, missing headers, or bad nonce
401AUTHENTICATION_REQUIREDMissing or invalid API key
402QUOTA_EXCEEDEDEvent quota exceeded (upgrade plan or enable overage)
429RATE_LIMIT_EXCEEDEDToo many requests (retry after Retry-After seconds)
507QUOTA_EXCEEDEDStorage quota exceeded

Regional Endpoints

RegionIngestor URL
EU (Frankfurt)https://api.ingest.eu.logflux.io
US (Virginia)https://api.ingest.us.logflux.io
CA (Montreal)https://api.ingest.ca.logflux.io
AU (Sydney)https://api.ingest.au.logflux.io
AP (Singapore)https://api.ingest.ap.logflux.io

The region is determined by your API key prefix. With region-prefixed keys, SDKs auto-discover the correct endpoint via Service Discovery.

Example: Go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "net/textproto"
    "time"
)

func ingestLog(apiKey, keyID string, aesKey []byte, payload []byte) error {
    // Generate unique nonce
    nonce := make([]byte, 12)
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return err
    }

    // Encrypt with AES-256-GCM
    block, _ := aes.NewCipher(aesKey)
    gcm, _ := cipher.NewGCM(block)
    ciphertext := gcm.Seal(nil, nonce, payload, nil)

    // Build multipart request
    var body bytes.Buffer
    writer := multipart.NewWriter(&body)
    writer.SetBoundary("logflux")

    header := make(textproto.MIMEHeader)
    header.Set("X-LF-Entry-Type", "1")
    header.Set("X-LF-Payload-Type", "1")
    header.Set("X-LF-Key-ID", keyID)
    header.Set("X-LF-Nonce", base64.StdEncoding.EncodeToString(nonce))
    header.Set("X-LF-Timestamp", time.Now().UTC().Format(time.RFC3339Nano))

    part, _ := writer.CreatePart(header)
    part.Write(ciphertext)
    writer.Close()

    // Send request
    req, _ := http.NewRequest("POST",
        "https://api.ingest.eu.logflux.io/v1/ingest", &body)
    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "multipart/mixed; boundary=logflux")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        return fmt.Errorf("ingestion failed: %d", resp.StatusCode)
    }
    return nil
}

Example: Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import os
import base64
import requests
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from datetime import datetime, timezone

def ingest_log(api_key: str, key_id: str, aes_key: bytes, payload: bytes):
    # Generate unique nonce
    nonce = os.urandom(12)

    # Encrypt with AES-256-GCM
    gcm = AESGCM(aes_key)
    ciphertext = gcm.encrypt(nonce, payload, None)

    # Build multipart/mixed body
    boundary = "logflux"
    timestamp = datetime.now(timezone.utc).isoformat()
    body = (
        f"--{boundary}\r\n"
        f"X-LF-Entry-Type: 1\r\n"
        f"X-LF-Payload-Type: 1\r\n"
        f"X-LF-Key-ID: {key_id}\r\n"
        f"X-LF-Nonce: {base64.b64encode(nonce).decode()}\r\n"
        f"X-LF-Timestamp: {timestamp}\r\n"
        f"\r\n"
    ).encode() + ciphertext + f"\r\n--{boundary}--\r\n".encode()

    # Send request
    resp = requests.post(
        "https://api.ingest.eu.logflux.io/v1/ingest",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": f"multipart/mixed; boundary={boundary}",
        },
        data=body,
    )
    resp.raise_for_status()
    return resp.json()

Example: cURL

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Note: This example shows the structure. In practice, you need to
# encrypt the payload with AES-GCM first using your session key.

curl -X POST "https://api.ingest.eu.logflux.io/v1/ingest" \
  -H "Authorization: Bearer eu-lf_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
  -H "Content-Type: multipart/mixed; boundary=logflux" \
  --data-binary @- << 'EOF'
--logflux
X-LF-Entry-Type: 1
X-LF-Payload-Type: 1
X-LF-Key-ID: 7f8a9b0c-1d2e-3f4a-5b6c-7d8e9f0a1b2c
X-LF-Nonce: YWJjZGVmZ2hpamts
X-LF-Timestamp: 2026-03-15T14:30:45.123Z

<binary ciphertext here>
--logflux--
EOF