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
| |
|---|
| Endpoint | POST /v1/ingest |
| Content-Type | multipart/mixed; boundary=<boundary> |
| Authentication | API Key: Authorization: Bearer {region}-lf_<key> |
| Entries per request | 1 - 1000 |
| Max request size | 10 MiB |
| Max entry size | 1 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:
POST /v1/handshake/init with your API key – server returns your RSA public key- Generate a random AES-256 key locally
- Encrypt the AES key with your RSA public key (RSA-OAEP-SHA256)
POST /v1/handshake/complete with the encrypted AES key – server returns a key_id UUID- Use the
key_id and AES key for all subsequent log entries
The LogFlux Agent and SDKs handle this automatically.
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--
|
Each MIME part supports the following headers:
| Header | Required | Default | Description |
|---|
X-LF-Key-ID | Yes (types 1-6) | – | UUID of the AES key from handshake completion |
X-LF-Nonce | Yes (types 1-6) | – | Base64-encoded 12-byte nonce for AES-GCM (must be unique per entry) |
X-LF-Entry-Type | No | 1 | Entry type: 1-7 (see table below) |
X-LF-Payload-Type | No | 1 | Payload encoding (see table below) |
X-LF-Timestamp | No | Current time | RFC3339 timestamp |
X-LF-Search-Tokens | No | – | Comma-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
| Type | Name | Category | Encryption | Description |
|---|
| 1 | Log | Events | E2E (AES-256-GCM) | Application and system logs |
| 2 | Metric | Events | E2E (AES-256-GCM) | Numeric measurements |
| 3 | Trace | Traces | E2E (AES-256-GCM) | Distributed trace spans |
| 4 | Event | Events | E2E (AES-256-GCM) | Application events |
| 5 | Audit | Audit | E2E (AES-256-GCM) + Object Lock | Compliance logs (365-day undeletable retention) |
| 6 | Telemetry | Traces | E2E (AES-256-GCM) | E2E encrypted telemetry |
| 7 | TelemetryManaged | Traces | Server-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
| Value | Name | Used By | Description |
|---|
| 1 | aes256-gcm-gzip-json | Types 1-6 (default) | AES-GCM encrypted, gzip-compressed JSON |
| 2 | aes256-gcm-zstd-json | Types 1-6 | AES-GCM encrypted, zstd-compressed JSON |
| 3 | gzip-json | Type 7 only | Gzip-compressed JSON (no client encryption) |
| 4 | zstd-json | Type 7 only | Zstd-compressed JSON (no client encryption) |
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):
- Generate a cryptographically random 12-byte nonce (must be unique per entry)
- Compress the log payload (gzip or zstd)
- Encrypt with AES-256-GCM using your session key and the nonce
- Set
X-LF-Key-ID to the key UUID from handshake - Set
X-LF-Nonce to the base64-encoded nonce - Put the raw ciphertext (including GCM auth tag) in the MIME part body
Rate Limits
| Limit | Value |
|---|
| Requests per minute | 600 (default, configurable per customer) |
| Burst | 100 requests |
| Max entries per request | 1000 |
| Max request size | 10 MiB |
| Max entry payload | 1 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 Status | Code | Description |
|---|
| 400 | VALIDATION_ERROR | Invalid request format, missing headers, or bad nonce |
| 401 | AUTHENTICATION_REQUIRED | Missing or invalid API key |
| 402 | QUOTA_EXCEEDED | Event quota exceeded (upgrade plan or enable overage) |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests (retry after Retry-After seconds) |
| 507 | QUOTA_EXCEEDED | Storage quota exceeded |
Regional Endpoints
| Region | Ingestor 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
|