# Logs API

Send logs to FiveDash over HTTP in the same format as a Discord webhook: endpoint, auth, body, limits, responses, errors and examples.

Source: https://fivedash.fivex.gg/docs/en/developers/logs-api

Every FiveDash log has its own link that works like a Discord webhook. Any script, bot or system that already sends to `discord.com/api/webhooks/...` can send to FiveDash by swapping only the link. This page is the full reference for that endpoint, and it also shows the fastest way to send logs from inside the city: the `fivedash` resource's export.

## Before you start

* **Plan:** the **Logs** module must be in the organization's plan.
* **A log created:** in **Logs → New log**. See [Create logs](https://fivedash.fivex.gg/docs/en/logs/create-logs).
* **Access level:** the link only shows to whoever can configure the log: **Configure** level on **All logs** ("Create logs, configure all of them and see the links"), or whoever created the log through the **Create logs** area. Others see "Whoever configures this log sees the webhook link."
* **Which one to use:**

| Way                           | When to use                                                                                                                           |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Export `exports.fivedash:log` | City scripts, server side. Faster, it already reads the player's character ID, name and identifiers, and exposes no link. Recommended |
| Link (webhook)                | Ready-made scripts that already send to a Discord webhook, bots and systems outside the city                                          |

## Get the log's link

### Open the log

In the **Logs** module, click the log in the sidebar.

### Open How to send

Click **How to send**. If the log is in export mode, click **Use webhook**.

### Copy the link

Copy the link and paste it in the script where the Discord link was. Don't share it: with the link, anyone can send logs to that log.

The link looks like this:

```http
https://<logs-address>/api/logs/<channelId>/<token>
```

Always use the link exactly as the panel shows it. The logs address may be different from the panel's address. Old links, on the panel's address, keep working.

> The token at the end of the link is the log's password. Don't publish the link on GitHub, in screenshots or in chats. If it leaks, open the log, click **Edit log**, then **Generate new link**, and update your scripts: the current link stops working right away.

## Endpoint

| Method | URL                             | What it does                                     |
| ------ | ------------------------------- | ------------------------------------------------ |
| `POST` | `/api/logs/<channelId>/<token>` | Stores one or more logs                          |
| `GET`  | `/api/logs/<channelId>/<token>` | Checks the link (answers like a Discord webhook) |

**Authentication:** the token in the path itself. No `Authorization` header needed.

### URL parameters

| Parameter   | Where | What it is                                                                                                                            |
| ----------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `channelId` | path  | The log's ID (a UUID)                                                                                                                 |
| `token`     | path  | The log's secret                                                                                                                      |
| `wait`      | query | `?wait=true` makes the response carry the stored message (status `200`), like Discord. Without it, the response is `204` with no body |

### Headers

| Header                | Required               | What it does                                                                                                                                                |
| --------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Content-Type`        | Yes                    | `application/json`, `multipart/form-data` or `application/x-www-form-urlencoded`                                                                            |
| `Idempotency-Key`     | No                     | Your own ID for the message (1 to 256 characters, no control characters). Repeating the same ID doesn't store the log twice. Requires `X-FiveDash-Event-At` |
| `X-FiveDash-Event-At` | With `Idempotency-Key` | When it happened, in Unix milliseconds (e.g. `1762711200000`). Must be between 7 days ago and 5 minutes ahead                                               |

Without these two headers, each `POST` stores new logs, with the arrival time.

## Body

### JSON

The same body as a Discord webhook:

```json
{
  "username": "my-script",
  "content": "Loose text, optional",
  "embeds": [
    {
      "title": "Store purchase",
      "description": "John bought 3 waters",
      "color": 3066993,
      "fields": [
        { "name": "Item", "value": "water", "inline": true },
        { "name": "Qty", "value": "3", "inline": true }
      ],
      "author": { "name": "24/7 Store" },
      "footer": { "text": "Example City" },
      "image": { "url": "https://cdn.fivex.sh/example-city/screenshots/print-0001.webp" },
      "timestamp": "2026-11-09T18:00:00.000Z"
    }
  ]
}
```

| Field                    | Type   | What it becomes in FiveDash                                         | Limit                                |
| ------------------------ | ------ | ------------------------------------------------------------------- | ------------------------------------ |
| `content`                | string | The log's text. Without a `title`, its first line becomes the title | 2,000 characters                     |
| `username`               | string | Who sent it. Shows as the log's **Script** and works as a filter    | 80 characters                        |
| `embeds`                 | array  | Each embed becomes a separate log                                   | 10 embeds                            |
| `embeds[].title`         | string | The log's title                                                     | 256                                  |
| `embeds[].description`   | string | The log's text                                                      | 4,096                                |
| `embeds[].url`           | string | Title link (`http`/`https`)                                         |                                      |
| `embeds[].color`         | number | Stripe color                                                        |                                      |
| `embeds[].fields`        | array  | Fields `{ "name", "value", "inline" }`                              | 25 fields; `name` 256, `value` 1,024 |
| `embeds[].author.name`   | string | Author                                                              | 256                                  |
| `embeds[].footer.text`   | string | Footer                                                              | 2,048                                |
| `embeds[].image.url`     | string | Large image (`http`/`https`)                                        |                                      |
| `embeds[].thumbnail.url` | string | Thumbnail (`http`/`https`)                                          |                                      |
| `embeds[].timestamp`     | string | The embed's date (ISO 8601)                                         | 64                                   |

Other Discord fields (`avatar_url`, `tts`, `components`, `allowed_mentions`…) are accepted and ignored. Text over the limit is cut.

FiveDash also reads the text for search and filters, with no change to your script:

| In the text                                                                                                                         | Becomes               |
| ----------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| Lines like `**Item:** water`, `Item: water`, `Item = water` or `[Item]: water`, and the `fields`                                    | Field and value pairs |
| `Passaporte: 123`, `passport 123`, `user_id: 123`, `citizenid: 123`, or a field like **ID**, **Victim** or **Killer** with a number | Player ID             |
| `<@123456789012345678>` or `discord:123456789012345678`                                                                             | Discord account       |
| `license:…`, `license2:…`, `steam:…`, `fivem:…`, `xbl:…`, `live:…`                                                                  | Identifiers           |
| `vector3(1.0, 2.0, 3.0)`, `x: 1.0, y: 2.0, z: 3.0` or `1.0, 2.0, 3.0`                                                               | Coordinates           |

This covers up to 10 players, 10 Discords, 10 identifiers and 10 coordinates per log.

### Multipart and form

For scripts that send the way Discord accepts in `multipart/form-data` or `application/x-www-form-urlencoded`:

| Form field                  | What it is                                                       |
| --------------------------- | ---------------------------------------------------------------- |
| `payload_json`              | The same JSON as above, as text                                  |
| `content`                   | Used if `payload_json` has no `content`                          |
| `username`                  | Used if `payload_json` has no `username`                         |
| Files (`files[0]`, `file`…) | Accepted, but **not stored**: the log only records how many came |

A message with only a file (no text and no embed) also becomes a log.

### Limits

| Limit                  | Value                   |
| ---------------------- | ----------------------- |
| JSON body              | 256 KB                  |
| Multipart or form body | 4 MB                    |
| Embeds per message     | 10 (each becomes a log) |
| Fields per embed       | 25                      |

## Responses

| Status | When                          | Body                                         |
| ------ | ----------------------------- | -------------------------------------------- |
| `204`  | Stored (without `?wait=true`) | empty                                        |
| `200`  | Stored, with `?wait=true`     | The message, in Discord's format (see below) |
| `200`  | `GET` on the link             | The webhook's data                           |

Response with `?wait=true`:

```json
{
  "id": "0193a5f0-0000-7000-8000-000000000000",
  "type": 0,
  "channel_id": "<channelId>",
  "webhook_id": "<channelId>",
  "author": { "id": "<channelId>", "username": "my-script", "bot": true, "discriminator": "0000", "avatar": null },
  "attachments": [],
  "mentions": [],
  "mention_roles": [],
  "pinned": false,
  "mention_everyone": false,
  "tts": false,
  "timestamp": "2026-11-09T18:00:00.123Z",
  "edited_timestamp": null,
  "flags": 0,
  "components": [],
  "content": "Loose text, optional",
  "embeds": [{ "title": "Store purchase" }]
}
```

`GET` response:

```json
{ "type": 1, "id": "<channelId>", "name": "Store", "avatar": null, "channel_id": "<channelId>", "guild_id": null, "application_id": null, "token": "<token>" }
```

## Errors

Errors follow Discord's codes, for scripts that already handle those cases. Some messages come in Portuguese.

| Status | Body                                                                               | When                                                                                                                                      |
| ------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `{"code":50109,"message":"The request body contains invalid JSON."}`               | Invalid JSON or malformed form                                                                                                            |
| `400`  | `{"code":50006,"message":"Cannot send an empty message"}`                          | No `content`, no embed with content and no file                                                                                           |
| `400`  | `{"code":50035,"message":"…"}`                                                     | Invalid content, invalid `Idempotency-Key`, or `X-FiveDash-Event-At` missing or out of range ("data do evento inválida ou fora do prazo") |
| `401`  | `{"code":50027,"message":"Invalid Webhook Token"}`                                 | The token doesn't match (for example, after **Generate new link**)                                                                        |
| `404`  | `{"code":10015,"message":"Unknown Webhook"}`                                       | The log doesn't exist or was deleted                                                                                                      |
| `409`  | `{"code":50035,"message":"destino do evento removido ou inválido"}`                | The log is no longer active                                                                                                               |
| `413`  | `{"code":40005,"message":"Request entity too large"}`                              | Body over the limit                                                                                                                       |
| `429`  | `{"error":"too_many","message":"limite de eventos ou bytes por segundo atingido"}` | Too fast for the server. Honor `Retry-After` (1 s)                                                                                        |
| `503`  | `{"error":"unavailable","message":"serviço temporariamente indisponível"}`         | Service busy or down. Retry after `Retry-After`                                                                                           |

> To retry without duplicates, send `Idempotency-Key` and `X-FiveDash-Event-At` with the same values on every attempt.

## Examples

### cURL

```bash
curl -X POST "https://<logs-address>/api/logs/<channelId>/<token>" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "my-script",
    "embeds": [{
      "title": "Store purchase",
      "fields": [{ "name": "Item", "value": "water" }, { "name": "Qty", "value": "3" }]
    }]
  }'
```

With confirmation and no duplicates:

```bash
curl -X POST "https://<logs-address>/api/logs/<channelId>/<token>?wait=true" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: purchase-48213" \
  -H "X-FiveDash-Event-At: 1762711200000" \
  -d '{ "content": "Order 48213 delivered" }'
```

Multipart, like Discord:

```bash
curl -X POST "https://<logs-address>/api/logs/<channelId>/<token>" \
  -F 'payload_json={"username":"my-script","content":"Screenshot attached"}' \
  -F "files[0]=@print.png"
```

### Lua (PerformHttpRequest)

On the server side of a script that sends to Discord today, swap only the link:

```lua
local LOG_URL = "https://<logs-address>/api/logs/<channelId>/<token>"

local function sendLog(title, fields)
    local embed = { title = title, color = 3066993, fields = {} }
    for _, field in ipairs(fields) do
        embed.fields[#embed.fields + 1] = { name = field[1], value = tostring(field[2]), inline = true }
    end
    PerformHttpRequest(LOG_URL, function(status)
        if status ~= 204 then print(("[logs] FiveDash answered %s"):format(status)) end
    end, "POST", json.encode({ username = GetCurrentResourceName(), embeds = { embed } }), {
        ["Content-Type"] = "application/json",
    })
end

sendLog("Store purchase", { { "Item", "water" }, { "Qty", 3 } })
```

> Never put the link in client scripts (`client_scripts`): they're sent to players' PCs, and anyone could send fake logs. Send from the server.

### JavaScript (Node, bot or server)

```ts
const LOG_URL = process.env.FIVEDASH_LOG_URL!; // the log's link, outside the code

await fetch(`${LOG_URL}?wait=true`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": "ban-9921",
    "X-FiveDash-Event-At": String(Date.now()),
  },
  body: JSON.stringify({
    username: "staff-bot",
    embeds: [{ title: "Ban", fields: [{ name: "Reason", value: "RDM" }] }],
  }),
});
```

## Send logs from inside the city (Lua)

In city scripts, prefer the `fivedash` resource's export. It uses no link, batches logs, keeps them in a queue while the panel is away and fills in the player's character ID, name and identifiers on its own:

```lua
exports.fivedash:log("deaths", {
    title = "Death recorded",
    players = { { source = killer, label = "Killer" }, { source = victim, label = "Victim" } },
    fields = { { "Weapon", weapon } },
})
```

The **Key** (`deaths`) is the log's key in the panel. Every option is in [Exports, events and commands](https://fivedash.fivex.gg/docs/en/developers/exports-and-events).

Under the hood, the resource uses two endpoints of its own, authenticated by the server key (`Authorization: Bearer fdk_…`): `GET /api/fivem/logs/channels` (which keys exist) and `POST /api/fivem/logs` (batches of up to 500 logs, JSON with optional gzip, up to 2 MB). They belong to the resource: don't call them directly from your scripts.

## FAQ

**Does my script send to Discord and FiveDash at the same time?**
No: you swap the Discord link for the FiveDash one. If you want both, the script must send twice.

**Does FiveDash keep attached files?**
No. It accepts the upload (so the script doesn't break) and records how many files came. To keep images, use the [CDN](https://fivedash.fivex.gg/docs/en/developers/cdn-api) and send the link in `image.url`.

**I got 401 after a while.**
Someone clicked **Generate new link**. Copy the new link from the panel and update the script.

**How many logs does a message with 3 embeds become?**
Three: one per embed. The `content` goes with the first.

## Next steps

[Migrate from webhooks](https://fivedash.fivex.gg/docs/en/logs/migrate-from-webhooks)Swap Discord webhooks for FiveDash links.
[Exports, events and commands](https://fivedash.fivex.gg/docs/en/developers/exports-and-events)The logs export in detail.
[Create logs](https://fivedash.fivex.gg/docs/en/logs/create-logs)Create a log and choose how the script sends.
[Search and filter](https://fivedash.fivex.gg/docs/en/logs/search-and-filter)Find any log by player, item or period.
