TranscriptsMit Discord anmelden

Ticket Transcript API

Deine Bots laden Ticket-Transcripts hoch, gebunden an die jeweilige Discord-Guild. Nutzer und Teammitglieder melden sich per Discord an und sehen genau die Transcripts, für die sie berechtigt sind. Die maschinenlesbare Spezifikation gibt es unter /api/openapi.json (OpenAPI 3.1 – importierbar in Postman, Insomnia, Bruno).

Schnellstart

  1. API-Key erstellen (Admin):
    curl -X POST "$API/api/admin/keys" \
      -H "Authorization: Bearer $ADMIN_API_KEY" -H "Content-Type: application/json" \
      -d '{ "name": "Support-Bot", "guildIds": ["1100000000000000001"] }'
    Die Antwort enthält secret (tt_…) – nur einmal sichtbar. Alternativ: pnpm key:create --name "Support-Bot" --guild 1100000000000000001
  2. Guild registrieren und Staff-Rollen setzen:
    curl -X PUT "$API/api/v1/guilds/1100000000000000001" \
      -H "Authorization: Bearer $TT_KEY" -H "Content-Type: application/json" \
      -d '{ "name": "Mein Server", "staffRoleIds": ["1500000000000000001"] }'
  3. Transcript hochladen:
    curl -X POST "$API/api/v1/transcripts" \
      -H "Authorization: Bearer $TT_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "guildId": "1100000000000000001",
        "guild": { "name": "Mein Server" },
        "externalId": "ticket-42",
        "ticketNumber": 42,
        "channel": { "id": "1200000000000000001", "name": "ticket-0042" },
        "category": "Support",
        "status": "CLOSED",
        "opener": { "id": "1300000000000000001", "username": "max" },
        "closedBy": { "id": "1300000000000000002", "username": "supporter" },
        "closeReason": "Erledigt",
        "openedAt": "2026-10-08T12:00:00Z",
        "closedAt": "2026-10-08T12:30:00Z",
        "messages": [{
          "id": "1400000000000000001",
          "author": { "id": "1300000000000000001", "username": "max" },
          "content": "Hallo, ich brauche Hilfe!",
          "timestamp": "2026-10-08T12:00:05Z"
        }]
      }'
    Die Antwort enthält transcript.url – den Link zum Web-Viewer, den dein Bot z.B. in einen Log-Kanal postet.

Authentifizierung

Bot API /api/v1/*
Authorization: Bearer tt_… (alternativ X-API-Key). Jeder Key ist auf bestimmte Guilds beschränkt (oder allGuilds) und hat Scopes: transcripts:read, transcripts:write, transcripts:delete, guilds:read, guilds:write. Keys werden nur als SHA-256-Hash gespeichert.
Web API /api/*
Session-Cookie nach Discord-Login (/api/auth/login). Scopes: identify guilds guilds.members.read. Schreibende Requests müssen von der eigenen Origin kommen (CSRF-Schutz).
Admin /api/admin/*
Authorization: Bearer $ADMIN_API_KEY

Zugriffsmodell

Manager
Owner, Administrator, „Server verwalten“ oder eine managerRoleIds-Rolle. Sieht alles, darf löschen, Einstellungen ändern und das Audit-Log sehen.
Team
Mitglied mit einer staffRoleIds-Rolle. Sieht alle Transcripts der Guild.
Teilnehmer
Ersteller des Tickets, alle Autoren von Nachrichten und per participants hinzugefügte Nutzer (canView: true). Sehen nur „ihre“ Transcripts – auch nach Verlassen des Servers. Abschaltbar per allowParticipantAccess: false.

Rollen werden über Discord OAuth2 (/users/@me/guilds/{id}/member) ermittelt und MEMBER_CACHE_TTL_SECONDS lang zwischengespeichert. Für Fremde existieren Transcripts nicht (404 statt 403).

Konventionen

  • Alle Discord-IDs sind Strings (Snowflakes), alle Zeitstempel ISO-8601 (UTC).
  • Idempotenz: externalId ist pro Guild eindeutig – erneutes POST /transcripts aktualisiert statt zu duplizieren. Nachrichten werden über ihre Discord-ID dedupliziert; Requests dürfen also wiederholt werden.
  • Limits: 1000 Nachrichten pro Request, Body max. MAX_BODY_BYTES (Standard 15 MB), Content-Encoding: gzip wird unterstützt.
  • Pagination: Listen liefern nextCursor; diesen als ?cursor= übergeben, bis er null ist.
  • Rate-Limits: pro Key RATE_LIMIT_BOT_PER_MINUTE (Standard 600/min), pro Nutzer 120/min. Header X-RateLimit-Limit/Remaining/Reset, bei 429 zusätzlich Retry-After.
  • Attachments: Discord-CDN-Links laufen ab. Die API lädt Dateien daher im Hintergrund herunter (ARCHIVE_ATTACHMENTS) und liefert sie über /api/attachments/{id} aus – nur an Berechtigte.
  • Fehlerformat:
    { "error": { "code": "VALIDATION_ERROR", "message": "Die Anfrage ist ungültig.",
                 "details": [{ "path": "messages.0.id", "code": "invalid_format", "message": "…" }],
                 "requestId": "Xk2…" } }
    Codes: BAD_REQUEST, VALIDATION_ERROR, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, PAYLOAD_TOO_LARGE, UNSUPPORTED_MEDIA_TYPE, RATE_LIMITED, DISCORD_ERROR, INTERNAL_ERROR.

Beispiel: discord.js-Integration

import { Collection, type Message, type TextChannel } from "discord.js";

const API = process.env.TRANSCRIPT_API_URL!;     // z.B. https://transcripts.example.com
const KEY = process.env.TRANSCRIPT_API_KEY!;     // tt_...

async function api(path: string, body: unknown) {
  const res = await fetch(API + path, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`Transcript-API ${res.status}: ${await res.text()}`);
  return res.json();
}

/** Alle Nachrichten eines Kanals laden (älteste zuerst). */
async function fetchAll(channel: TextChannel): Promise<Message[]> {
  const all: Message[] = [];
  let before: string | undefined;
  for (;;) {
    const batch: Collection<string, Message> = await channel.messages.fetch({ limit: 100, before });
    if (batch.size === 0) break;
    all.push(...batch.values());
    before = batch.last()!.id;
  }
  return all.reverse();
}

const toUser = (u: { id: string; username: string; bot: boolean; displayAvatarURL(): string }, displayName?: string) =>
  ({ id: u.id, username: u.username, displayName, bot: u.bot, avatarUrl: u.displayAvatarURL() });

const toMessage = (m: Message) => ({
  id: m.id,
  author: { ...toUser(m.author, m.member?.displayName), color: m.member?.displayColor || null },
  content: m.content,
  timestamp: m.createdAt.toISOString(),
  editedTimestamp: m.editedAt?.toISOString() ?? null,
  type: m.type,
  pinned: m.pinned,
  replyTo: m.reference?.messageId ?? null,
  embeds: m.embeds.map((e) => e.toJSON()),
  attachments: m.attachments.map((a) => ({
    id: a.id, filename: a.name, url: a.url, contentType: a.contentType,
    size: a.size, width: a.width, height: a.height, spoiler: a.spoiler,
  })),
  stickers: m.stickers.map((s) => ({ id: s.id, name: s.name })),
  reactions: m.reactions.cache.map((r) => ({
    emoji: { id: r.emoji.id, name: r.emoji.name ?? "?", animated: r.emoji.animated ?? false },
    count: r.count,
  })),
});

export async function saveTranscript(channel: TextChannel, ticket: { id: string; openerId: string; openedAt: Date }, closer: Message["author"], reason?: string) {
  const messages = (await fetchAll(channel)).map(toMessage);
  const opener = await channel.client.users.fetch(ticket.openerId);

  // 1) Transcript mit den ersten 1000 Nachrichten anlegen
  const { transcript } = await api("/api/v1/transcripts", {
    guildId: channel.guild.id,
    guild: { name: channel.guild.name, iconHash: channel.guild.icon },
    externalId: ticket.id,
    channel: { id: channel.id, name: channel.name },
    status: "OPEN",
    opener: toUser(opener),
    openedAt: ticket.openedAt.toISOString(),
    messages: messages.slice(0, 1000),
  });

  // 2) Restliche Nachrichten in Batches nachschieben
  for (let i = 1000; i < messages.length; i += 1000) {
    await api(`/api/v1/transcripts/${transcript.id}/messages`, { messages: messages.slice(i, i + 1000) });
  }

  // 3) Schließen
  await api(`/api/v1/transcripts/${transcript.id}/close`, { closedBy: toUser(closer), closeReason: reason });

  return transcript.url as string; // z.B. in den Log-Kanal oder per DM an den Nutzer senden
}

Bot API

GET/api/v1/me

API-Key prüfen

🔐 Bot-Key

Gibt Informationen über den verwendeten Key zurück (Scopes, Guilds). Gut zum Testen der Verbindung.

Antworten

  • 200 Key-Informationen → ApiKeyResponse
  • 401 Nicht authentifiziert → Error
PUT/api/v1/guilds/{guildId}

Guild registrieren / konfigurieren

🔐 Bot-KeyScope: guilds:write

Legt die Guild an oder aktualisiert sie. Hier werden u.a. die Staff-Rollen gesetzt, deren Mitglieder alle Transcripts sehen dürfen.

Request-Body (JSON Schema)
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "iconHash": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 64
        },
        {
          "type": "null"
        }
      ]
    },
    "staffRoleIds": {
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^\\d{15,21}$",
        "description": "Discord-ID (Snowflake) als String"
      }
    },
    "managerRoleIds": {
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^\\d{15,21}$",
        "description": "Discord-ID (Snowflake) als String"
      }
    },
    "allowParticipantAccess": {
      "type": "boolean"
    },
    "retentionDays": {
      "anyOf": [
        {
          "type": "integer",
          "minimum": 1,
          "maximum": 3650
        },
        {
          "type": "null"
        }
      ],
      "description": "null = nie automatisch löschen"
    }
  },
  "description": "GuildUpsert"
}

Antworten

  • 200 Aktualisiert → GuildResponse
  • 201 Angelegt → GuildResponse
  • 400 Validierungsfehler → Error
  • 403 Keine Berechtigung → Error
GET/api/v1/guilds/{guildId}

Guild-Konfiguration abrufen

🔐 Bot-KeyScope: guilds:read

Antworten

  • 200 Guild → GuildResponse
  • 404 Nicht gefunden → Error
GET/api/v1/guilds/{guildId}/stats

Statistiken einer Guild

🔐 Bot-KeyScope: guilds:read

Antworten

  • 200 Statistiken → GuildStatsResponse
  • 404 Nicht gefunden → Error
POST/api/v1/transcripts

Transcript erstellen (Upsert über externalId)

🔐 Bot-KeyScope: transcripts:write

Erstellt ein Transcript inkl. bis zu 1000 Nachrichten. Ist `externalId` gesetzt und existiert bereits, wird das Transcript aktualisiert und neue Nachrichten werden angehängt (idempotent – Duplikate per Message-ID werden übersprungen). Für größere Tickets weitere Nachrichten über `POST /api/v1/transcripts/{id}/messages` nachschieben. Unterstützt `Content-Encoding: gzip`.

Request-Body (JSON Schema)
{
  "type": "object",
  "properties": {
    "guildId": {
      "type": "string",
      "pattern": "^\\d{15,21}$",
      "description": "Discord-ID (Snowflake) als String"
    },
    "guild": {
      "description": "Legt die Guild an bzw. aktualisiert Name/Icon",
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        },
        "iconHash": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 64
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "required": [
        "name"
      ]
    },
    "externalId": {
      "description": "Ticket-ID in deinem Bot. Gleiche externalId = Upsert (idempotent)",
      "type": "string",
      "minLength": 1,
      "maxLength": 128
    },
    "ticketNumber": {
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 9007199254740991
    },
    "channel": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^\\d{15,21}$",
          "description": "Discord-ID (Snowflake) als String"
        },
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        }
      },
      "required": [
        "id",
        "name"
      ]
    },
    "category": {
      "type": "string",
      "maxLength": 100
    },
    "subject": {
      "type": "string",
      "maxLength": 500
    },
    "tags": {
      "default": [],
      "maxItems": 20,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 50
      }
    },
    "status": {
      "default": "CLOSED",
      "type": "string",
      "enum": [
        "OPEN",
        "CLOSED"
      ]
    },
    "opener": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^\\d{15,21}$",
          "description": "Discord-ID (Snowflake) als String"
        },
        "username": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        },
        "displayName": {
          "description": "Server-Nickname oder Global Name",
          "anyOf": [
            {
              "type": "string",
              "maxLength": 100
            },
            {
              "type": "null"
            }
          ]
        },
        "avatarUrl": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 4096,
              "format": "uri"
            },
            {
              "type": "null"
            }
          ]
        },
        "bot": {
          "default": false,
          "type": "boolean"
        },
        "color": {
          "description": "Farbe der höchsten Rolle als Integer",
          "anyOf": [
            {
              "type": "integer",
              "minimum": 0,
              "maximum": 16777215
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "required": [
        "id",
        "username"
      ],
      "description": "DiscordUser"
    },
    "claimedBy": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^\\d{15,21}$",
          "description": "Discord-ID (Snowflake) als String"
        },
        "username": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        },
        "displayName": {
          "description": "Server-Nickname oder Global Name",
          "anyOf": [
            {
              "type": "string",
              "maxLength": 100
            },
            {
              "type": "null"
            }
          ]
        },
        "avatarUrl": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 4096,
              "format": "uri"
            },
            {
              "type": "null"
            }
          ]
        },
        "bot": {
          "default": false,
          "type": "boolean"
        },
        "color": {
          "description": "Farbe der höchsten Rolle als Integer",
          "anyOf": [
            {
              "type": "integer",
              "minimum": 0,
              "maximum": 16777215
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "required": [
        "id",
        "username"
      ],
      "description": "DiscordUser"
    },
    "closedBy": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^\\d{15,21}$",
          "description": "Discord-ID (Snowflake) als String"
        },
        "username": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        },
        "displayName": {
          "description": "Server-Nickname oder Global Name",
          "anyOf": [
            {
              "type": "string",
              "maxLength": 100
            },
            {
              "type": "null"
            }
          ]
        },
        "avatarUrl": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 4096,
              "format": "uri"
            },
            {
              "type": "null"
            }
          ]
        },
        "bot": {
          "default": false,
          "type": "boolean"
        },
        "color": {
          "description": "Farbe der höchsten Rolle als Integer",
          "anyOf": [
            {
              "type": "integer",
              "minimum": 0,
              "maximum": 16777215
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "required": [
        "id",
        "username"
      ],
      "description": "DiscordUser"
    },
    "closeReason": {
      "type": "string",
      "maxLength": 2000
    },
    "openedAt": {
      "type": "string",
      "format": "date-time"
    },
    "closedAt": {
      "type": "string",
      "format": "date-time"
    },
    "participants": {
      "default": [],
      "maxItems": 500,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^\\d{15,21}$",
            "description": "Discord-ID (Snowflake) als String"
          },
          "username": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "displayName": {
            "description": "Server-Nickname oder Global Name",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 100
              },
              {
                "type": "null"
              }
            ]
          },
          "avatarUrl": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 4096,
                "format": "uri"
              },
              {
                "type": "null"
              }
            ]
          },
          "bot": {
            "default": false,
            "type": "boolean"
          },
          "color": {
            "description": "Farbe der höchsten Rolle als Integer",
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 16777215
              },
              {
                "type": "null"
              }
            ]
          },
          "role": {
            "description": "Rolle im Ticket (Standard: aus Kontext abgeleitet)",
            "type": "string",
            "enum": [
              "OPENER",
              "STAFF",
              "MEMBER",
              "BOT"
            ]
          },
          "canView": {
            "description": "Darf der Nutzer das Transcript im Web-Viewer sehen? (Standard: true)",
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "username"
        ]
      }
    },
    "resolved": {
      "type": "object",
      "properties": {
        "users": {
          "type": "object",
          "propertyNames": {
            "type": "string",
            "pattern": "^\\d{15,21}$",
            "description": "Discord-ID (Snowflake) als String"
          },
          "additionalProperties": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "maxLength": 100
              }
            },
            "required": [
              "name"
            ]
          }
        },
        "roles": {
          "type": "object",
          "propertyNames": {
            "type": "string",
            "pattern": "^\\d{15,21}$",
            "description": "Discord-ID (Snowflake) als String"
          },
          "additionalProperties": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "maxLength": 100
              },
              "color": {
                "anyOf": [
                  {
                    "type": "integer",
                    "minimum": -9007199254740991,
                    "maximum": 9007199254740991
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "name"
            ]
          }
        },
        "channels": {
          "type": "object",
          "propertyNames": {
            "type": "string",
            "pattern": "^\\d{15,21}$",
            "description": "Discord-ID (Snowflake) als String"
          },
          "additionalProperties": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "maxLength": 100
              }
            },
            "required": [
              "name"
            ]
          }
        }
      },
      "description": "Namen für Mentions (<@id>, <@&id>, <#id>) im Nachrichtentext"
    },
    "metadata": {
      "type": "object",
      "propertyNames": {
        "type": "string",
        "maxLength": 64
      },
      "additionalProperties": {},
      "description": "Beliebige zusätzliche Daten deines Bots (max. 16 KB)"
    },
    "messages": {
      "default": [],
      "maxItems": 1000,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^\\d{15,21}$",
            "description": "Discord-Message-ID (dient der Deduplizierung)"
          },
          "author": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "pattern": "^\\d{15,21}$",
                "description": "Discord-ID (Snowflake) als String"
              },
              "username": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
              },
              "displayName": {
                "description": "Server-Nickname oder Global Name",
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 100
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "avatarUrl": {
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 4096,
                    "format": "uri"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "bot": {
                "default": false,
                "type": "boolean"
              },
              "color": {
                "description": "Farbe der höchsten Rolle als Integer",
                "anyOf": [
                  {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 16777215
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "id",
              "username"
            ],
            "description": "DiscordUser"
          },
          "content": {
            "default": "",
            "type": "string",
            "maxLength": 10000
          },
          "timestamp": {
            "description": "Erstellungszeitpunkt (ISO-8601 oder Unix-ms)",
            "type": "string",
            "format": "date-time"
          },
          "editedTimestamp": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "type": {
            "default": 0,
            "description": "Discord-Message-Type",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "pinned": {
            "default": false,
            "type": "boolean"
          },
          "replyTo": {
            "description": "ID der Nachricht, auf die geantwortet wird",
            "anyOf": [
              {
                "type": "string",
                "pattern": "^\\d{15,21}$",
                "description": "Discord-ID (Snowflake) als String"
              },
              {
                "type": "null"
              }
            ]
          },
          "embeds": {
            "default": [],
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string",
                  "maxLength": 256
                },
                "description": {
                  "type": "string",
                  "maxLength": 4096
                },
                "url": {
                  "type": "string",
                  "maxLength": 2048
                },
                "color": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 16777215
                },
                "timestamp": {
                  "type": "string",
                  "maxLength": 64
                },
                "author": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "maxLength": 256
                    },
                    "url": {
                      "type": "string"
                    },
                    "icon_url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name"
                  ],
                  "additionalProperties": {}
                },
                "footer": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string",
                      "maxLength": 2048
                    },
                    "icon_url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "text"
                  ],
                  "additionalProperties": {}
                },
                "image": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "maxLength": 4096
                    }
                  },
                  "required": [
                    "url"
                  ],
                  "additionalProperties": {}
                },
                "thumbnail": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "maxLength": 4096
                    }
                  },
                  "required": [
                    "url"
                  ],
                  "additionalProperties": {}
                },
                "fields": {
                  "maxItems": 25,
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "maxLength": 256
                      },
                      "value": {
                        "type": "string",
                        "maxLength": 1024
                      },
                      "inline": {
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "name",
                      "value"
                    ]
                  }
                }
              },
              "additionalProperties": {},
              "description": "Discord-Embed im Format der Discord-API (snake_case)"
            }
          },
          "attachments": {
            "default": [],
            "maxItems": 25,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "description": "Discord-Attachment-ID",
                  "type": "string",
                  "pattern": "^\\d{15,21}$"
                },
                "filename": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 255
                },
                "url": {
                  "type": "string",
                  "maxLength": 4096,
                  "format": "uri",
                  "description": "Discord-CDN-URL – wird (falls aktiviert) automatisch archiviert"
                },
                "contentType": {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 255
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "size": {
                  "default": 0,
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "width": {
                  "anyOf": [
                    {
                      "type": "integer",
                      "exclusiveMinimum": 0,
                      "maximum": 9007199254740991
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "height": {
                  "anyOf": [
                    {
                      "type": "integer",
                      "exclusiveMinimum": 0,
                      "maximum": 9007199254740991
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "spoiler": {
                  "default": false,
                  "type": "boolean"
                }
              },
              "required": [
                "filename",
                "url"
              ]
            }
          },
          "stickers": {
            "default": [],
            "maxItems": 3,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "pattern": "^\\d{15,21}$",
                  "description": "Discord-ID (Snowflake) als String"
                },
                "name": {
                  "type": "string",
                  "maxLength": 100
                },
                "formatType": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "id",
                "name"
              ]
            }
          },
          "reactions": {
            "default": [],
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "emoji": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "anyOf": [
                        {
                          "type": "string",
                          "pattern": "^\\d{15,21}$",
                          "description": "Discord-ID (Snowflake) als String"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "name": {
                      "type": "string",
                      "maxLength": 100
                    },
                    "animated": {
                      "default": false,
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "name"
                  ]
                },
                "count": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "emoji",
                "count"
              ]
            }
          },
          "components": {
            "default": [],
            "description": "Rohe Discord-Components (Buttons etc.)",
            "maxItems": 10,
            "type": "array",
            "items": {}
          }
        },
        "required": [
          "id",
          "author",
          "timestamp"
        ]
      }
    }
  },
  "required": [
    "guildId",
    "channel",
    "opener",
    "openedAt"
  ],
  "description": "TranscriptCreate"
}

Antworten

  • 200 Bestehendes Transcript aktualisiert → TranscriptWriteResponse
  • 201 Erstellt → TranscriptWriteResponse
  • 400 Validierungsfehler → Error
  • 403 Keine Berechtigung → Error
  • 413 Body zu groß → Error
GET/api/v1/transcripts

Transcripts auflisten / suchen

🔐 Bot-KeyScope: transcripts:read

Cursor-basierte Pagination: `nextCursor` der Antwort als `cursor` übergeben.

Query-Parameter

guildIdstringDiscord-ID (Snowflake) als String
statusstring
userIdstringNur Transcripts, an denen dieser Nutzer beteiligt ist
openerIdstringDiscord-ID (Snowflake) als String
externalIdstring
categorystring
tagstring
qstringSuche in Kanalname, Betreff, externalId
searchMessagesstringSuche zusätzlich im Nachrichtentext
fromstringopenedAt >= from
tostringopenedAt <= to
limitinteger
cursorstringnextCursor aus der vorherigen Antwort

Antworten

  • 200 Liste → TranscriptList
  • 400 Validierungsfehler → Error
GET/api/v1/transcripts/{transcriptId}

Transcript abrufen (inkl. Teilnehmer, Metadaten)

🔐 Bot-KeyScope: transcripts:read

Antworten

  • 200 Transcript → TranscriptResponse
  • 404 Nicht gefunden → Error
PATCH/api/v1/transcripts/{transcriptId}

Transcript aktualisieren

🔐 Bot-KeyScope: transcripts:write
Request-Body (JSON Schema)
{
  "type": "object",
  "properties": {
    "ticketNumber": {
      "anyOf": [
        {
          "type": "integer",
          "exclusiveMinimum": 0,
          "maximum": 9007199254740991
        },
        {
          "type": "null"
        }
      ]
    },
    "channelName": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "category": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 100
        },
        {
          "type": "null"
        }
      ]
    },
    "subject": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 500
        },
        {
          "type": "null"
        }
      ]
    },
    "tags": {
      "maxItems": 20,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 50
      }
    },
    "status": {
      "type": "string",
      "enum": [
        "OPEN",
        "CLOSED"
      ]
    },
    "claimedBy": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "pattern": "^\\d{15,21}$",
              "description": "Discord-ID (Snowflake) als String"
            },
            "username": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "displayName": {
              "description": "Server-Nickname oder Global Name",
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 100
                },
                {
                  "type": "null"
                }
              ]
            },
            "avatarUrl": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 4096,
                  "format": "uri"
                },
                {
                  "type": "null"
                }
              ]
            },
            "bot": {
              "default": false,
              "type": "boolean"
            },
            "color": {
              "description": "Farbe der höchsten Rolle als Integer",
              "anyOf": [
                {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 16777215
                },
                {
                  "type": "null"
                }
              ]
            }
          },
          "required": [
            "id",
            "username"
          ],
          "description": "DiscordUser"
        },
        {
          "type": "null"
        }
      ]
    },
    "closedBy": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "pattern": "^\\d{15,21}$",
              "description": "Discord-ID (Snowflake) als String"
            },
            "username": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "displayName": {
              "description": "Server-Nickname oder Global Name",
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 100
                },
                {
                  "type": "null"
                }
              ]
            },
            "avatarUrl": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 4096,
                  "format": "uri"
                },
                {
                  "type": "null"
                }
              ]
            },
            "bot": {
              "default": false,
              "type": "boolean"
            },
            "color": {
              "description": "Farbe der höchsten Rolle als Integer",
              "anyOf": [
                {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 16777215
                },
                {
                  "type": "null"
                }
              ]
            }
          },
          "required": [
            "id",
            "username"
          ],
          "description": "DiscordUser"
        },
        {
          "type": "null"
        }
      ]
    },
    "closeReason": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 2000
        },
        {
          "type": "null"
        }
      ]
    },
    "closedAt": {
      "anyOf": [
        {
          "type": "string",
          "format": "date-time"
        },
        {
          "type": "null"
        }
      ]
    },
    "resolved": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "users": {
              "type": "object",
              "propertyNames": {
                "type": "string",
                "pattern": "^\\d{15,21}$",
                "description": "Discord-ID (Snowflake) als String"
              },
              "additionalProperties": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  }
                },
                "required": [
                  "name"
                ]
              }
            },
            "roles": {
              "type": "object",
              "propertyNames": {
                "type": "string",
                "pattern": "^\\d{15,21}$",
                "description": "Discord-ID (Snowflake) als String"
              },
              "additionalProperties": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "color": {
                    "anyOf": [
                      {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                },
                "required": [
                  "name"
                ]
              }
            },
            "channels": {
              "type": "object",
              "propertyNames": {
                "type": "string",
                "pattern": "^\\d{15,21}$",
                "description": "Discord-ID (Snowflake) als String"
              },
              "additionalProperties": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          },
          "description": "Namen für Mentions (<@id>, <@&id>, <#id>) im Nachrichtentext"
        },
        {
          "type": "null"
        }
      ]
    },
    "metadata": {
      "anyOf": [
        {
          "type": "object",
          "propertyNames": {
            "type": "string",
            "maxLength": 64
          },
          "additionalProperties": {},
          "description": "Beliebige zusätzliche Daten deines Bots (max. 16 KB)"
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "description": "TranscriptUpdate"
}

Antworten

  • 200 Aktualisiert → TranscriptResponse
  • 404 Nicht gefunden → Error
DELETE/api/v1/transcripts/{transcriptId}

Transcript löschen (inkl. Dateien)

🔐 Bot-KeyScope: transcripts:delete

Antworten

  • 204 Gelöscht
  • 404 Nicht gefunden → Error
POST/api/v1/transcripts/{transcriptId}/messages

Nachrichten anhängen

🔐 Bot-KeyScope: transcripts:write

Bis zu 1000 Nachrichten pro Request. Bereits vorhandene Message-IDs werden übersprungen – Requests können also gefahrlos wiederholt werden.

Request-Body (JSON Schema)
{
  "type": "object",
  "properties": {
    "messages": {
      "minItems": 1,
      "maxItems": 1000,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^\\d{15,21}$",
            "description": "Discord-Message-ID (dient der Deduplizierung)"
          },
          "author": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "pattern": "^\\d{15,21}$",
                "description": "Discord-ID (Snowflake) als String"
              },
              "username": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
              },
              "displayName": {
                "description": "Server-Nickname oder Global Name",
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 100
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "avatarUrl": {
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 4096,
                    "format": "uri"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "bot": {
                "default": false,
                "type": "boolean"
              },
              "color": {
                "description": "Farbe der höchsten Rolle als Integer",
                "anyOf": [
                  {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 16777215
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "id",
              "username"
            ],
            "description": "DiscordUser"
          },
          "content": {
            "default": "",
            "type": "string",
            "maxLength": 10000
          },
          "timestamp": {
            "description": "Erstellungszeitpunkt (ISO-8601 oder Unix-ms)",
            "type": "string",
            "format": "date-time"
          },
          "editedTimestamp": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "type": {
            "default": 0,
            "description": "Discord-Message-Type",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "pinned": {
            "default": false,
            "type": "boolean"
          },
          "replyTo": {
            "description": "ID der Nachricht, auf die geantwortet wird",
            "anyOf": [
              {
                "type": "string",
                "pattern": "^\\d{15,21}$",
                "description": "Discord-ID (Snowflake) als String"
              },
              {
                "type": "null"
              }
            ]
          },
          "embeds": {
            "default": [],
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string",
                  "maxLength": 256
                },
                "description": {
                  "type": "string",
                  "maxLength": 4096
                },
                "url": {
                  "type": "string",
                  "maxLength": 2048
                },
                "color": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 16777215
                },
                "timestamp": {
                  "type": "string",
                  "maxLength": 64
                },
                "author": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "maxLength": 256
                    },
                    "url": {
                      "type": "string"
                    },
                    "icon_url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name"
                  ],
                  "additionalProperties": {}
                },
                "footer": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string",
                      "maxLength": 2048
                    },
                    "icon_url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "text"
                  ],
                  "additionalProperties": {}
                },
                "image": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "maxLength": 4096
                    }
                  },
                  "required": [
                    "url"
                  ],
                  "additionalProperties": {}
                },
                "thumbnail": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "maxLength": 4096
                    }
                  },
                  "required": [
                    "url"
                  ],
                  "additionalProperties": {}
                },
                "fields": {
                  "maxItems": 25,
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "maxLength": 256
                      },
                      "value": {
                        "type": "string",
                        "maxLength": 1024
                      },
                      "inline": {
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "name",
                      "value"
                    ]
                  }
                }
              },
              "additionalProperties": {},
              "description": "Discord-Embed im Format der Discord-API (snake_case)"
            }
          },
          "attachments": {
            "default": [],
            "maxItems": 25,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "description": "Discord-Attachment-ID",
                  "type": "string",
                  "pattern": "^\\d{15,21}$"
                },
                "filename": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 255
                },
                "url": {
                  "type": "string",
                  "maxLength": 4096,
                  "format": "uri",
                  "description": "Discord-CDN-URL – wird (falls aktiviert) automatisch archiviert"
                },
                "contentType": {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 255
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "size": {
                  "default": 0,
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "width": {
                  "anyOf": [
                    {
                      "type": "integer",
                      "exclusiveMinimum": 0,
                      "maximum": 9007199254740991
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "height": {
                  "anyOf": [
                    {
                      "type": "integer",
                      "exclusiveMinimum": 0,
                      "maximum": 9007199254740991
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "spoiler": {
                  "default": false,
                  "type": "boolean"
                }
              },
              "required": [
                "filename",
                "url"
              ]
            }
          },
          "stickers": {
            "default": [],
            "maxItems": 3,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "pattern": "^\\d{15,21}$",
                  "description": "Discord-ID (Snowflake) als String"
                },
                "name": {
                  "type": "string",
                  "maxLength": 100
                },
                "formatType": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "id",
                "name"
              ]
            }
          },
          "reactions": {
            "default": [],
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "emoji": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "anyOf": [
                        {
                          "type": "string",
                          "pattern": "^\\d{15,21}$",
                          "description": "Discord-ID (Snowflake) als String"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "name": {
                      "type": "string",
                      "maxLength": 100
                    },
                    "animated": {
                      "default": false,
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "name"
                  ]
                },
                "count": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "emoji",
                "count"
              ]
            }
          },
          "components": {
            "default": [],
            "description": "Rohe Discord-Components (Buttons etc.)",
            "maxItems": 10,
            "type": "array",
            "items": {}
          }
        },
        "required": [
          "id",
          "author",
          "timestamp"
        ]
      }
    },
    "participants": {
      "default": [],
      "maxItems": 500,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^\\d{15,21}$",
            "description": "Discord-ID (Snowflake) als String"
          },
          "username": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "displayName": {
            "description": "Server-Nickname oder Global Name",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 100
              },
              {
                "type": "null"
              }
            ]
          },
          "avatarUrl": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 4096,
                "format": "uri"
              },
              {
                "type": "null"
              }
            ]
          },
          "bot": {
            "default": false,
            "type": "boolean"
          },
          "color": {
            "description": "Farbe der höchsten Rolle als Integer",
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 16777215
              },
              {
                "type": "null"
              }
            ]
          },
          "role": {
            "description": "Rolle im Ticket (Standard: aus Kontext abgeleitet)",
            "type": "string",
            "enum": [
              "OPENER",
              "STAFF",
              "MEMBER",
              "BOT"
            ]
          },
          "canView": {
            "description": "Darf der Nutzer das Transcript im Web-Viewer sehen? (Standard: true)",
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "username"
        ]
      }
    }
  },
  "required": [
    "messages"
  ],
  "description": "AppendMessages"
}

Antworten

  • 200 Alles Duplikate → AppendResult
  • 201 Eingefügt → AppendResult
  • 404 Nicht gefunden → Error
GET/api/v1/transcripts/{transcriptId}/messages

Nachrichten abrufen (chronologisch, paginiert)

🔐 Bot-KeyScope: transcripts:read

Query-Parameter

limitinteger
cursorstring
authorIdstringDiscord-ID (Snowflake) als String

Antworten

  • 200 Nachrichten → MessageList
POST/api/v1/transcripts/{transcriptId}/close

Ticket schließen

🔐 Bot-KeyScope: transcripts:write

Setzt Status CLOSED, closedAt, closedBy und closeReason. Optional mit letzter Nachrichten-Batch.

Request-Body (JSON Schema)
{
  "type": "object",
  "properties": {
    "closedBy": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^\\d{15,21}$",
          "description": "Discord-ID (Snowflake) als String"
        },
        "username": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        },
        "displayName": {
          "description": "Server-Nickname oder Global Name",
          "anyOf": [
            {
              "type": "string",
              "maxLength": 100
            },
            {
              "type": "null"
            }
          ]
        },
        "avatarUrl": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 4096,
              "format": "uri"
            },
            {
              "type": "null"
            }
          ]
        },
        "bot": {
          "default": false,
          "type": "boolean"
        },
        "color": {
          "description": "Farbe der höchsten Rolle als Integer",
          "anyOf": [
            {
              "type": "integer",
              "minimum": 0,
              "maximum": 16777215
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "required": [
        "id",
        "username"
      ],
      "description": "DiscordUser"
    },
    "closeReason": {
      "type": "string",
      "maxLength": 2000
    },
    "closedAt": {
      "description": "Standard: jetzt",
      "type": "string",
      "format": "date-time"
    },
    "messages": {
      "default": [],
      "description": "Letzte Nachrichten-Batch",
      "maxItems": 1000,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^\\d{15,21}$",
            "description": "Discord-Message-ID (dient der Deduplizierung)"
          },
          "author": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "pattern": "^\\d{15,21}$",
                "description": "Discord-ID (Snowflake) als String"
              },
              "username": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
              },
              "displayName": {
                "description": "Server-Nickname oder Global Name",
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 100
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "avatarUrl": {
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 4096,
                    "format": "uri"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "bot": {
                "default": false,
                "type": "boolean"
              },
              "color": {
                "description": "Farbe der höchsten Rolle als Integer",
                "anyOf": [
                  {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 16777215
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "id",
              "username"
            ],
            "description": "DiscordUser"
          },
          "content": {
            "default": "",
            "type": "string",
            "maxLength": 10000
          },
          "timestamp": {
            "description": "Erstellungszeitpunkt (ISO-8601 oder Unix-ms)",
            "type": "string",
            "format": "date-time"
          },
          "editedTimestamp": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "type": {
            "default": 0,
            "description": "Discord-Message-Type",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "pinned": {
            "default": false,
            "type": "boolean"
          },
          "replyTo": {
            "description": "ID der Nachricht, auf die geantwortet wird",
            "anyOf": [
              {
                "type": "string",
                "pattern": "^\\d{15,21}$",
                "description": "Discord-ID (Snowflake) als String"
              },
              {
                "type": "null"
              }
            ]
          },
          "embeds": {
            "default": [],
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string",
                  "maxLength": 256
                },
                "description": {
                  "type": "string",
                  "maxLength": 4096
                },
                "url": {
                  "type": "string",
                  "maxLength": 2048
                },
                "color": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 16777215
                },
                "timestamp": {
                  "type": "string",
                  "maxLength": 64
                },
                "author": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "maxLength": 256
                    },
                    "url": {
                      "type": "string"
                    },
                    "icon_url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name"
                  ],
                  "additionalProperties": {}
                },
                "footer": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string",
                      "maxLength": 2048
                    },
                    "icon_url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "text"
                  ],
                  "additionalProperties": {}
                },
                "image": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "maxLength": 4096
                    }
                  },
                  "required": [
                    "url"
                  ],
                  "additionalProperties": {}
                },
                "thumbnail": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "maxLength": 4096
                    }
                  },
                  "required": [
                    "url"
                  ],
                  "additionalProperties": {}
                },
                "fields": {
                  "maxItems": 25,
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "maxLength": 256
                      },
                      "value": {
                        "type": "string",
                        "maxLength": 1024
                      },
                      "inline": {
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "name",
                      "value"
                    ]
                  }
                }
              },
              "additionalProperties": {},
              "description": "Discord-Embed im Format der Discord-API (snake_case)"
            }
          },
          "attachments": {
            "default": [],
            "maxItems": 25,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "description": "Discord-Attachment-ID",
                  "type": "string",
                  "pattern": "^\\d{15,21}$"
                },
                "filename": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 255
                },
                "url": {
                  "type": "string",
                  "maxLength": 4096,
                  "format": "uri",
                  "description": "Discord-CDN-URL – wird (falls aktiviert) automatisch archiviert"
                },
                "contentType": {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 255
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "size": {
                  "default": 0,
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "width": {
                  "anyOf": [
                    {
                      "type": "integer",
                      "exclusiveMinimum": 0,
                      "maximum": 9007199254740991
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "height": {
                  "anyOf": [
                    {
                      "type": "integer",
                      "exclusiveMinimum": 0,
                      "maximum": 9007199254740991
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "spoiler": {
                  "default": false,
                  "type": "boolean"
                }
              },
              "required": [
                "filename",
                "url"
              ]
            }
          },
          "stickers": {
            "default": [],
            "maxItems": 3,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "pattern": "^\\d{15,21}$",
                  "description": "Discord-ID (Snowflake) als String"
                },
                "name": {
                  "type": "string",
                  "maxLength": 100
                },
                "formatType": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "id",
                "name"
              ]
            }
          },
          "reactions": {
            "default": [],
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "emoji": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "anyOf": [
                        {
                          "type": "string",
                          "pattern": "^\\d{15,21}$",
                          "description": "Discord-ID (Snowflake) als String"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "name": {
                      "type": "string",
                      "maxLength": 100
                    },
                    "animated": {
                      "default": false,
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "name"
                  ]
                },
                "count": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "emoji",
                "count"
              ]
            }
          },
          "components": {
            "default": [],
            "description": "Rohe Discord-Components (Buttons etc.)",
            "maxItems": 10,
            "type": "array",
            "items": {}
          }
        },
        "required": [
          "id",
          "author",
          "timestamp"
        ]
      }
    }
  },
  "description": "TranscriptClose"
}

Antworten

  • 200 Geschlossen → TranscriptWriteResponse
  • 404 Nicht gefunden → Error
GET/api/v1/transcripts/{transcriptId}/participants

Teilnehmer auflisten

🔐 Bot-KeyScope: transcripts:read

Antworten

  • 200 Teilnehmer → ParticipantList
POST/api/v1/transcripts/{transcriptId}/participants

Teilnehmer hinzufügen / Lesezugriff vergeben

🔐 Bot-KeyScope: transcripts:write

Nutzer mit `canView: true` können das Transcript nach Discord-Login sehen, auch ohne Staff-Rolle.

Request-Body (JSON Schema)
{
  "type": "object",
  "properties": {
    "participants": {
      "minItems": 1,
      "maxItems": 500,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^\\d{15,21}$",
            "description": "Discord-ID (Snowflake) als String"
          },
          "username": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "displayName": {
            "description": "Server-Nickname oder Global Name",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 100
              },
              {
                "type": "null"
              }
            ]
          },
          "avatarUrl": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 4096,
                "format": "uri"
              },
              {
                "type": "null"
              }
            ]
          },
          "bot": {
            "default": false,
            "type": "boolean"
          },
          "color": {
            "description": "Farbe der höchsten Rolle als Integer",
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 16777215
              },
              {
                "type": "null"
              }
            ]
          },
          "role": {
            "description": "Rolle im Ticket (Standard: aus Kontext abgeleitet)",
            "type": "string",
            "enum": [
              "OPENER",
              "STAFF",
              "MEMBER",
              "BOT"
            ]
          },
          "canView": {
            "description": "Darf der Nutzer das Transcript im Web-Viewer sehen? (Standard: true)",
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "username"
        ]
      }
    }
  },
  "required": [
    "participants"
  ],
  "description": "ParticipantsUpsert"
}

Antworten

  • 200 Teilnehmer → ParticipantList
DELETE/api/v1/transcripts/{transcriptId}/participants/{userId}

Lesezugriff eines Teilnehmers entziehen

🔐 Bot-KeyScope: transcripts:write

Antworten

  • 204 Entzogen
  • 404 Nicht gefunden → Error
POST/api/v1/transcripts/{transcriptId}/attachments

Datei hochladen

🔐 Bot-KeyScope: transcripts:write

multipart/form-data mit Feld `file`, optional `messageId` (Discord-Message-ID) und `attachmentId` (ersetzt ein per Nachricht gemeldetes Attachment).

Body: multipart/form-data mit file (Pflicht), messageId, attachmentId

Antworten

  • 201 Hochgeladen → AttachmentResponse
  • 413 Datei zu groß → Error

Web API

GET/api/users/me

Eingeloggter Nutzer + zugängliche Guilds

🔐 Discord-Login

Antworten

  • 200 Nutzer → Me
  • 401 Nicht authentifiziert → Error
GET/api/guilds

Zugängliche Guilds

🔐 Discord-Login

`?refresh=true` gleicht Guild-Mitgliedschaften sofort mit Discord ab.

Antworten

  • 200 Guilds → AccessibleGuildList
GET/api/guilds/{guildId}

Guild-Details (Stats für Staff, Einstellungen für Manager)

🔐 Discord-Login

Antworten

  • 200 Guild
  • 404 Nicht gefunden → Error
PATCH/api/guilds/{guildId}

Guild-Einstellungen ändern (MANAGER)

🔐 Discord-Login
Request-Body (JSON Schema)
{
  "type": "object",
  "properties": {
    "staffRoleIds": {
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^\\d{15,21}$",
        "description": "Discord-ID (Snowflake) als String"
      }
    },
    "managerRoleIds": {
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^\\d{15,21}$",
        "description": "Discord-ID (Snowflake) als String"
      }
    },
    "allowParticipantAccess": {
      "type": "boolean"
    },
    "retentionDays": {
      "anyOf": [
        {
          "type": "integer",
          "minimum": 1,
          "maximum": 3650
        },
        {
          "type": "null"
        }
      ],
      "description": "null = nie automatisch löschen"
    }
  },
  "description": "GuildSettings"
}

Antworten

  • 200 Aktualisiert → GuildResponse
  • 403 Keine Berechtigung → Error
GET/api/guilds/{guildId}/transcripts

Transcripts einer Guild

🔐 Discord-Login

STAFF/MANAGER sehen alle Transcripts, Mitglieder nur solche, an denen sie beteiligt sind.

Query-Parameter

statusstring
userIdstringNur Transcripts, an denen dieser Nutzer beteiligt ist
openerIdstringDiscord-ID (Snowflake) als String
externalIdstring
categorystring
tagstring
qstringSuche in Kanalname, Betreff, externalId
searchMessagesstringSuche zusätzlich im Nachrichtentext
fromstringopenedAt >= from
tostringopenedAt <= to
limitinteger
cursorstringnextCursor aus der vorherigen Antwort

Antworten

  • 200 Liste → TranscriptList
GET/api/guilds/{guildId}/audit-log

Audit-Log (MANAGER)

🔐 Discord-Login

Query-Parameter

transcriptIdstring
actionstring
limitinteger
cursorstring

Antworten

  • 200 Einträge
  • 403 Keine Berechtigung → Error
GET/api/transcripts/{transcriptId}

Transcript abrufen

🔐 Discord-Login

Antworten

  • 200 Transcript
  • 404 Nicht gefunden → Error
DELETE/api/transcripts/{transcriptId}

Transcript löschen (MANAGER)

🔐 Discord-Login

Antworten

  • 204 Gelöscht
  • 403 Keine Berechtigung → Error
GET/api/transcripts/{transcriptId}/messages

Nachrichten abrufen

🔐 Discord-Login

Query-Parameter

limitinteger
cursorstring
authorIdstringDiscord-ID (Snowflake) als String

Antworten

  • 200 Nachrichten → MessageList
GET/api/transcripts/{transcriptId}/export

Export als HTML oder JSON

🔐 Discord-Login

Query-Parameter

formatstring
downloadstring

Antworten

  • 200 Datei
GET/api/attachments/{attachmentId}

Attachment herunterladen

🔐 Discord-Login

Funktioniert mit Session-Cookie oder Bot-Key (`transcripts:read`). `?download` erzwingt einen Download.

Antworten

  • 200 Datei
  • 302 Weiterleitung zum Discord-CDN (nicht archiviert)

Auth

GET/api/auth/login

Discord-Login starten

🔐 Öffentlich

Leitet zu Discord weiter. `?next=/pfad` bestimmt das Ziel nach dem Login.

Antworten

  • 307 Weiterleitung zu Discord
GET/api/auth/callback

OAuth2-Callback (von Discord aufgerufen)

🔐 Öffentlich

Antworten

  • 307 Weiterleitung ins Dashboard
POST/api/auth/logout

Abmelden

🔐 Discord-Login

Antworten

  • 204 Abgemeldet

Admin

GET/api/admin/keys

API-Keys auflisten

🔐 Admin-Key

Antworten

  • 200 Keys
POST/api/admin/keys

API-Key erstellen

🔐 Admin-Key

Der Klartext-Key (`secret`) wird **nur einmal** zurückgegeben.

Request-Body (JSON Schema)
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "scopes": {
      "default": [
        "transcripts:read",
        "transcripts:write",
        "transcripts:delete",
        "guilds:read",
        "guilds:write"
      ],
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "transcripts:read",
          "transcripts:write",
          "transcripts:delete",
          "guilds:read",
          "guilds:write"
        ]
      }
    },
    "allGuilds": {
      "default": false,
      "type": "boolean"
    },
    "guildIds": {
      "default": [],
      "maxItems": 500,
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^\\d{15,21}$",
        "description": "Discord-ID (Snowflake) als String"
      }
    },
    "expiresAt": {
      "type": "string",
      "format": "date-time"
    }
  },
  "required": [
    "name"
  ],
  "description": "ApiKeyCreate"
}

Antworten

  • 201 Erstellt → ApiKeyCreated
PATCH/api/admin/keys/{keyId}

API-Key ändern

🔐 Admin-Key
Request-Body (JSON Schema)
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "scopes": {
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "transcripts:read",
          "transcripts:write",
          "transcripts:delete",
          "guilds:read",
          "guilds:write"
        ]
      }
    },
    "allGuilds": {
      "type": "boolean"
    },
    "guildIds": {
      "maxItems": 500,
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^\\d{15,21}$",
        "description": "Discord-ID (Snowflake) als String"
      }
    },
    "expiresAt": {
      "anyOf": [
        {
          "type": "string",
          "format": "date-time"
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "description": "ApiKeyUpdate"
}

Antworten

  • 200 Aktualisiert → ApiKeyResponse
DELETE/api/admin/keys/{keyId}

API-Key widerrufen

🔐 Admin-Key

Antworten

  • 200 Widerrufen → ApiKeyResponse
POST/api/cron/retention

Aufräum-Job ausführen

🔐 Cron-Secret

Löscht Transcripts gemäß `retentionDays`, abgelaufene Sessions, alte Caches und Audit-Einträge (> 365 Tage).

Antworten

  • 200 Ergebnis
GET/api/health

Health-Check

🔐 Öffentlich

Antworten

  • 200 OK
  • 503 Datenbank nicht erreichbar