# Share API bot integration Use this endpoint when a trusted Discord bot needs to publish a member's text attachment and return a public Erika Share link. ```http POST https://erika.sudloh.com/api/share Authorization: Bearer Content-Type: multipart/form-data ``` The endpoint does not use a NextAuth session or a public API key. It is private and requires the shared bearer secret configured on the website as `SHARE_BOT_SECRET`. Store the same secret in the bot's private environment; never place it in source code, Discord messages, browser code, or logs. ## Required website configuration The website needs these values before the endpoint can work: - `SHARE_BOT_SECRET`: long random secret shared only with the bot. - `BASE_URL`, or `NEXTAUTH_URL` as its fallback: canonical website origin used in returned links. - Discord bot token and guild ID already used by Erika's Discord integration. - Redis configuration for rate limiting. The bot can use its own variable names: ```dotenv ERIKA_BASE_URL=https://erika.sudloh.com ERIKA_SHARE_BOT_SECRET=replace-with-the-same-secret ``` Do not include a trailing slash in `ERIKA_BASE_URL`. ## Multipart fields | Field | Required | Contract | | --- | --- | --- | | `file` | Yes | Non-empty UTF-8 `.txt` file, maximum 5 MiB. Blank text and NUL characters are rejected. | | `senderDiscordId` | Yes | `message.author.id`, as a 15–22 digit Discord snowflake. Do not send the bot's ID. | | `description` | No | Plain text, maximum 5,000 characters. | | `image` | No | PNG, JPEG, WebP, or GIF, maximum 50 MiB. The declared MIME type must match the file contents. | Do not send `displayName`, `avatarUrl`, `authorName`, or `authorAvatarUrl`. The API rejects those fields. It verifies that `senderDiscordId` belongs to the configured Discord guild and fetches the member's global display name and global avatar directly from Discord. ## Discord.js example This example uses the built-in `fetch`, `FormData`, and `Blob` APIs available in modern Node.js runtimes. It publishes the first `.txt` attachment on a message and uses the message text as the optional description. ```ts import type { Attachment, Message } from "discord.js"; type ShareSuccess = { success: true; share: { id: string; url: string; author: { displayName: string; avatarUrl: string | null; }; imageUrl: string | null; createdAt: string; }; }; type ShareFailure = { success: false; error: string; }; async function attachmentBlob(attachment: Attachment) { const response = await fetch(attachment.url); if (!response.ok) { throw new Error(`Unable to download ${attachment.name}: ${response.status}`); } return new Blob([await response.arrayBuffer()], { type: attachment.contentType ?? "application/octet-stream", }); } export async function publishTextAttachment(message: Message) { if (message.author.bot) return; const textAttachment = message.attachments.find((attachment) => attachment.name?.toLowerCase().endsWith(".txt"), ); if (!textAttachment?.name) return; const baseUrl = process.env.ERIKA_BASE_URL; const secret = process.env.ERIKA_SHARE_BOT_SECRET; if (!baseUrl || !secret) { throw new Error("Erika Share API is not configured"); } const form = new FormData(); form.set("file", await attachmentBlob(textAttachment), textAttachment.name); form.set("senderDiscordId", message.author.id); const description = message.content.trim(); if (description) form.set("description", description.slice(0, 5_000)); const response = await fetch(`${baseUrl}/api/share`, { method: "POST", headers: { Authorization: `Bearer ${secret}`, }, body: form, }); const result = (await response.json()) as ShareSuccess | ShareFailure; if (!response.ok || !result.success) { const retryAfter = response.headers.get("retry-after"); const retryNote = retryAfter ? `; retry after ${retryAfter}s` : ""; throw new Error(`Erika Share failed (${response.status}): ${result.error}${retryNote}`); } await message.reply(result.share.url); return result.share; } ``` Do not manually set the `Content-Type` request header. `FormData` supplies the required multipart boundary automatically. To include the first supported image attachment, add it before the request: ```ts const imageAttachment = message.attachments.find((attachment) => attachment.contentType?.startsWith("image/"), ); if (imageAttachment?.name) { form.set("image", await attachmentBlob(imageAttachment), imageAttachment.name); } ``` ## cURL example ```bash curl --fail-with-body \ -X POST "https://erika.sudloh.com/api/share" \ -H "Authorization: Bearer $ERIKA_SHARE_BOT_SECRET" \ -F "file=@./message.txt;type=text/plain" \ -F "senderDiscordId=123456789012345678" \ -F "description=Optional context for this share" ``` ## Successful response The API returns HTTP `201`: ```json { "success": true, "share": { "id": "a1b2c3d4", "url": "https://erika.sudloh.com/share/a1b2c3d4", "author": { "displayName": "Example User", "avatarUrl": "https://cdn.discordapp.com/avatars/..." }, "imageUrl": null, "createdAt": "2026-08-16T00:00:00.000Z" } } ``` The bot should post `share.url`. The shared page is public and does not require the recipient to log in. ## Error response Failures use this shape: ```json { "success": false, "error": "Human-readable error" } ``` | Status | Meaning | | --- | --- | | `400` | Missing/malformed multipart data, invalid sender ID, blank text, invalid UTF-8, NUL content, or a supplied profile field. | | `401` | Missing or incorrect bearer secret. | | `413` | Text, description, or image exceeds its limit. | | `415` | The main file is not `.txt`, or the image type/content is unsupported. | | `422` | The sender is not a member of the configured Discord guild. | | `429` | A rate limit was exceeded. Read the `Retry-After` response header. | | `503` | Bot API configuration, Discord profile lookup, or Redis rate limiting is unavailable. | | `500` | Unexpected internal failure. Log the status and error without logging the bearer secret. | ## Rate limits - 60 authenticated bot API requests per hour globally. - 10 shares per hour per Discord sender, combined across bot and website uploads. On HTTP `429`, wait for the number of seconds in `Retry-After` before retrying. Do not retry `400`, `401`, `413`, `415`, or `422` automatically. A short bounded retry with backoff is appropriate for `503` and transient `500` responses. ## Identity and duplicate safety The website creates or updates the member's linked NextAuth user and Discord account inside a transaction. Repeated or concurrent uploads for one Discord ID resolve to the same user/account pair. If that member later signs in through Discord OAuth, the login reuses the bot-created user instead of creating a duplicate.