Integrations

The Integrations feature lets your server connect an external service (your stats backend, a killfeed, a price ticker, a CRM lookup) to Discord without writing code. You set up the connection in the dashboard, MARVIN fetches the data on demand, and it becomes available to Custom Commands, Tickets, and Welcome messages.

Every step below is also available from the Start from template dropdown on the Integrations page in your dashboard — pick a preset and the form pre-fills. You still enter your own values (URL, key) before saving.

What this is

  • Connection — the base URL + auth credentials for one API.
  • Endpoint — a specific route on that connection, with its method (GET/POST), path template, optional body template, and a response map that tells the bot which JSON fields to surface.
  • Field mapping — a JSONPath-lite expression (e.g. data[0].kills) that pulls a value out of the response and exposes it under a friendly name like kills.
  • Binding — a custom command or ticket template that interpolates the mapped value via {api.<connection>.<field>}.

Credentials never round-trip after save — the dashboard only stores them encrypted; the bot reads + decrypts them locally. Edits require re-entering the credential.

1. Create a connection

  1. Open /dashboard/<guild>/api-connections.
  2. In the left rail, click New connection.
  3. Pick a template from the Start from template dropdown. Three presets exist today:
    • Blank — empty form. Use when no preset matches.
    • Custom API (generic JSON) — sane defaults for the typical case: JSON-over-HTTPS with an X-API-Key header.
    • Killfeed (worked example) — pre-fills the structure used in this doc's example below.
  4. Replace the placeholder values:
    • name — any short alphanumeric label. This is how you'll reference the connection in command bindings ({api.killfeed.kills} ← that killfeed token).
    • base_url — full https:// URL of your API host. The bot enforces HTTPS unless allow_http is explicitly enabled.
    • auth_type — one of none, api_key_header, bearer, basic, or query_key. Pick the one your API uses.
    • credentials — header name + key, bearer token, etc. depending on auth_type.
  5. Click Create connection.

2. Add an endpoint

A connection by itself does nothing — it's just credentials + a base URL. You need at least one endpoint.

  1. Select the connection you just created from the left rail.
  2. Click Add endpoint.
  3. Fill in:
    • name — short identifier like player_stats. Used in bindings as the second token: {api.<connection>.<endpoint>.<field>}.
    • methodGET for reads, POST for sending JSON.
    • path_template — path appended to base_url. Use {var} tokens for runtime interpolation. The built-in guild_id, user_id, and channel_id are always available; custom command args fill the rest.
    • body_template (POST only) — JSON body with the same {var} interpolation. Values are JSON-escaped automatically.
    • response_map — one row per output field. key is the friendly name; JSONPath is where to find it in the response.

3. Test it

Use the Test button on the endpoint. The dashboard sends the request via the bot (so it uses the real credentials + the SSRF allowlist), then shows you the raw response + the field map output side-by-side. If a JSONPath doesn't resolve, you'll see (unmapped) next to that field — fix the path and re-test.

4. Use the data

Open the Custom commands page. Create or edit a command. In the response body, use the field picker (the inline blue token icon) or type the placeholder manually:

/!stats — "{api.killfeed.player_stats.kills} kills, {api.killfeed.player_stats.deaths} deaths"

When the command fires, the bot calls the endpoint, applies the response map, and substitutes the values. Failed fetches surface as a friendly error message in the channel rather than silently dropping the command.

Worked example: Killfeed

Suppose you run a Rust/Squad/Tarkov server and your stats backend exposes a killfeed at https://killfeed.example.com/feed?guild=<id> with an API key in an X-API-Key header.

Sample response:

{
  "guild_id": "1156016923855827074",
  "recent": [
    { "killer": "Player-A", "victim": "Player-B", "weapon": "AK-47", "distance": 142 },
    { "killer": "Player-C", "victim": "Player-D", "weapon": "M4",    "distance": 87 }
  ],
  "totals": { "kills_24h": 412, "active_players": 38 }
}

Connection setup (pick the Killfeed (worked example) preset, then swap in real values):

  • name: killfeed
  • base_url: https://killfeed.example.com
  • auth_type: api_key_header
  • header_name: X-API-Key
  • key: your API key

Endpoint setup:

  • name: recent
  • method: GET
  • path_template: /feed?guild={guild_id}
  • response_map:
    • last_killerrecent[0].killer
    • last_victimrecent[0].victim
    • last_weaponrecent[0].weapon
    • kills_24htotals.kills_24h
    • activetotals.active_players
  • cache_seconds: 30 (don't hammer the API on every command)

Custom command binding:

/!lastkill →
"{api.killfeed.recent.last_killer} killed {api.killfeed.recent.last_victim} with {api.killfeed.recent.last_weapon} ({api.killfeed.recent.active} players active right now)"

Now /!lastkill in any channel returns the live killfeed snapshot. Same plumbing works for leaderboards, player lookups, ticket enrichment, etc. — just add more endpoints to the same connection or create a new connection per backend.

Tips + gotchas

  • HTTPS-only by default. If your API is plain HTTP (rare; don't use plain HTTP in prod), toggle allow_http on the connection.
  • SSRF guard. The bot blocks fetches to private IPs (10.x, 192.168.x, 169.254.x, etc.) and metadata-service hostnames. If your API is on a private network, expose it through a proper tunnel.
  • Cache aggressively. cache_seconds defaults to 0 — every command fires a fresh request. Bump it to 30-300s on hot commands so you don't blow your API's rate limit.
  • Credential rotation. Re-open the connection, re-enter the credential, save. The encrypted column is overwritten in place.
  • Field picker beats typing. Use the blue token icon in the custom-command editor to click-pick a mapped field instead of typing {api.foo.bar.baz} from memory.