> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aiola.app/llms.txt
> Use this file to discover all available pages before exploring further.

# App Logs over HTTP

> Send App Logs from runtimes without an Aiola SDK using one authenticated HTTP request.

Post JSON from any HTTP-capable runtime and receive the issue fingerprint that Aiola used for grouping. Generate the secret key in **Settings → API Key** and keep it out of client bundles when the runtime can call your server instead.

## Request contract

```text theme={null}
POST https://aiola.app/api/app-logs-ingest
x-aiola-key: <project-key>
Content-Type: application/json
```

<ParamField path="message" type="string">
  Required unless `exception` is present; maximum stored length is 8,192 characters.
</ParamField>

<ParamField path="level" type="string">
  `fatal`, `error`, `warning`, `info`, or `debug`. Defaults to `error`.
</ParamField>

<ParamField path="exception" type="object">
  `type`, `value`, and optional `stacktrace.frames`; at most 200 frames.
</ParamField>

<ParamField path="tags" type="object">
  At most 50 string values, each capped at 500 characters. A numeric `status_code` tag is clamped to `0`–`65535`.
</ParamField>

<ParamField path="breadcrumbs" type="array">
  The newest 100 entries are retained. Each entry can contain `timestamp`, `category`, `message`, `level`, and `data`.
</ParamField>

<ParamField path="environment" type="string" />

<ParamField path="release" type="string" />

<ParamField path="platform" type="string" />

<ParamField path="timestamp" type="string" />

<ParamField path="request" type="object" />

<ParamField path="user" type="object" />

<ParamField path="contexts" type="object" />

All examples send the same payload shape. Replace `aio_REPLACE_WITH_PROJECT_KEY` or set `AIOLA_KEY` as shown.

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl -X POST 'https://aiola.app/api/app-logs-ingest' \
      -H "x-aiola-key: ${AIOLA_KEY}" \
      -H 'Content-Type: application/json' \
      -d '{"message":"Checkout failed","level":"error","platform":"http","environment":"production","release":"1.4.2","tags":{"app":"store","screen":"checkout"},"breadcrumbs":[{"timestamp":"2026-09-19T12:00:00Z","category":"navigation","message":"Opened checkout","level":"info"}]}'
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={null}
    import Foundation

    var request = URLRequest(url: URL(string: "https://aiola.app/api/app-logs-ingest")!)
    request.httpMethod = "POST"
    request.setValue("aio_REPLACE_WITH_PROJECT_KEY", forHTTPHeaderField: "x-aiola-key")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONSerialization.data(withJSONObject: [
      "message": "Checkout failed", "level": "error", "platform": "http",
      "environment": "production", "release": "1.4.2",
      "tags": ["app": "store", "screen": "checkout"],
      "breadcrumbs": [["timestamp": "2026-09-19T12:00:00Z", "category": "navigation", "message": "Opened checkout", "level": "info"]]
    ])
    let (_, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw URLError(.badServerResponse) }
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    import java.net.URI
    import java.net.http.HttpClient
    import java.net.http.HttpRequest
    import java.net.http.HttpResponse

    fun main() {
      val body = """{"message":"Checkout failed","level":"error","platform":"http","environment":"production","release":"1.4.2","tags":{"app":"store","screen":"checkout"},"breadcrumbs":[{"timestamp":"2026-09-19T12:00:00Z","category":"navigation","message":"Opened checkout","level":"info"}]}"""
      val request = HttpRequest.newBuilder(URI("https://aiola.app/api/app-logs-ingest"))
        .header("x-aiola-key", "aio_REPLACE_WITH_PROJECT_KEY")
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body)).build()
      val response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())
      require(response.statusCode() == 200) { response.body() }
    }
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    package main

    import (
      "bytes"
      "log"
      "net/http"
    )

    func main() {
      payload := []byte(`{"message":"Checkout failed","level":"error","platform":"http","environment":"production","release":"1.4.2","tags":{"app":"store","screen":"checkout"},"breadcrumbs":[{"timestamp":"2026-09-19T12:00:00Z","category":"navigation","message":"Opened checkout","level":"info"}]}`)
      req, err := http.NewRequest("POST", "https://aiola.app/api/app-logs-ingest", bytes.NewReader(payload))
      if err != nil { log.Fatal(err) }
      req.Header.Set("x-aiola-key", "aio_REPLACE_WITH_PROJECT_KEY")
      req.Header.Set("Content-Type", "application/json")
      res, err := http.DefaultClient.Do(req)
      if err != nil { log.Fatal(err) }
      defer res.Body.Close()
      if res.StatusCode != http.StatusOK { log.Fatalf("Aiola returned %s", res.Status) }
    }
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    #[tokio::main]
    async fn main() -> Result<(), reqwest::Error> {
        reqwest::Client::new()
            .post("https://aiola.app/api/app-logs-ingest")
            .header("x-aiola-key", "aio_REPLACE_WITH_PROJECT_KEY")
            .json(&serde_json::json!({
                "message": "Checkout failed", "level": "error", "platform": "http",
                "environment": "production", "release": "1.4.2",
                "tags": {"app": "store", "screen": "checkout"},
                "breadcrumbs": [{"timestamp": "2026-09-19T12:00:00Z", "category": "navigation", "message": "Opened checkout", "level": "info"}]
            }))
            .send().await?.error_for_status()?;
        Ok(())
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import os
    import requests

    response = requests.post(
        "https://aiola.app/api/app-logs-ingest",
        headers={"x-aiola-key": os.environ["AIOLA_KEY"]},
        json={
            "message": "Checkout failed", "level": "error", "platform": "http",
            "environment": "production", "release": "1.4.2",
            "tags": {"app": "store", "screen": "checkout"},
            "breadcrumbs": [{"timestamp": "2026-09-19T12:00:00Z", "category": "navigation", "message": "Opened checkout", "level": "info"}],
        },
        timeout=10,
    )
    response.raise_for_status()
    ```
  </Tab>

  <Tab title="Node">
    ```js theme={null}
    const response = await fetch("https://aiola.app/api/app-logs-ingest", {
      method: "POST",
      headers: { "x-aiola-key": process.env.AIOLA_KEY, "content-type": "application/json" },
      body: JSON.stringify({
        message: "Checkout failed", level: "error", platform: "http",
        environment: "production", release: "1.4.2",
        tags: { app: "store", screen: "checkout" },
        breadcrumbs: [{ timestamp: "2026-09-19T12:00:00Z", category: "navigation", message: "Opened checkout", level: "info" }],
      }),
    });
    if (!response.ok) throw new Error(`Aiola returned ${response.status}: ${await response.text()}`);
    ```
  </Tab>

  <Tab title="React Native">
    ```tsx theme={null}
    async function sendLog() {
      const response = await fetch("https://aiola.app/api/app-logs-ingest", {
        method: "POST",
        headers: { "x-aiola-key": "aio_REPLACE_WITH_PROJECT_KEY", "content-type": "application/json" },
        body: JSON.stringify({
          message: "Checkout failed", level: "error", platform: "http",
          environment: "production", release: "1.4.2",
          tags: { app: "store", screen: "checkout" },
          breadcrumbs: [{ timestamp: "2026-09-19T12:00:00Z", category: "navigation", message: "Opened checkout", level: "info" }],
        }),
      });
      if (!response.ok) throw new Error(`Aiola returned ${response.status}`);
    }

    void sendLog();
    ```
  </Tab>

  <Tab title="Shell trap">
    ```bash theme={null}
    set -Eeuo pipefail

    report_failure() {
      local exit_code="$?"
      local command="${BASH_COMMAND}"
      trap - ERR
      curl --silent --show-error --max-time 10 \
        -X POST 'https://aiola.app/api/app-logs-ingest' \
        -H "x-aiola-key: ${AIOLA_KEY}" \
        -H 'Content-Type: application/json' \
        --data "$(jq -n --arg message "Command failed: ${command}" '{message:$message,level:"error",platform:"http",environment:"production",release:"1.4.2",tags:{app:"store",screen:"checkout"},breadcrumbs:[{timestamp:"2026-09-19T12:00:00Z",category:"navigation",message:"Opened checkout",level:"info"}]}')" \
        >/dev/null || true
      exit "$exit_code"
    }
    trap report_failure ERR
    ```
  </Tab>
</Tabs>

## Send an exception

Frames are stored in the order sent. Include `in_app: true` on application frames; Aiola selects the last in-app frame as the issue culprit.

```json theme={null}
{
  "message": "Checkout failed",
  "level": "error",
  "exception": {
    "type": "PaymentError",
    "value": "Card declined",
    "stacktrace": {
      "frames": [
        {
          "filename": "CheckoutView.swift",
          "function": "submitOrder",
          "lineno": 128,
          "colno": 17,
          "context_line": "try await submit(order)",
          "in_app": true
        }
      ]
    }
  },
  "platform": "ios",
  "environment": "production",
  "release": "1.4.2"
}
```

## Tags and breadcrumbs

| HTTP field        | App Logs behavior                                                                                                                                                                                                                               |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tags`            | Stored as searchable event metadata. Top-level `environment` and `release` win; otherwise `tags.environment` and `tags.release` supply them. `tags.sdk_name` and `tags.sdk_version` populate SDK metadata; `platform` is the fallback SDK name. |
| `breadcrumbs`     | Stored as the ordered trail before the failure. If more than 100 are sent, Aiola keeps the newest 100.                                                                                                                                          |
| `request.headers` | Stored after secret-bearing headers are removed; retained values are capped at 500 characters.                                                                                                                                                  |
| `request.body`    | Stored up to 4,096 characters, then marked as truncated.                                                                                                                                                                                        |

## Response and errors

```json theme={null}
{ "ok": true, "group_hash": "<32-character-hash>", "is_new": true }
```

| Status | Meaning                                                                                                                |
| ------ | ---------------------------------------------------------------------------------------------------------------------- |
| `200`  | Stored; `group_hash` identifies the issue group and `is_new` tells whether the group was created.                      |
| `400`  | Invalid JSON, or neither `message` nor `exception` was sent.                                                           |
| `401`  | Missing or invalid project key.                                                                                        |
| `402`  | The key owner has neither a Pro subscription nor an active trial.                                                      |
| `403`  | App Logs is disabled for this project.                                                                                 |
| `405`  | The method is not `POST` or `OPTIONS`.                                                                                 |
| `413`  | Request text exceeds 512,000 characters.                                                                               |
| `429`  | The project limit was exceeded; the fallback is 60 events/minute per key. The source-IP backstop is 120 events/minute. |
| `500`  | Aiola could not fingerprint, store, or group the event.                                                                |
| `504`  | The public gateway timed out.                                                                                          |

For every optional field and the redaction list, see [API Reference](/reference/api#ingest-app-logs).
