REST API & webhooks

One API behind everything: the inbox, Telegram, Discord, the MCP server and the CLI are all clients of it.

Basics

curl "https://app.inngame.app/v1/conversations?status=open&unread=1" \
  -H "Authorization: Bearer $INNGAME_API_KEY"

Conversations and messages

MethodPathScopeNotes
GET/conversationsreadFilters: status (open, closed, snoozed, all), app, unread=1, assignee (me, none, user id), tag, lang (player language: pt also matches pt-BR; und = unknown), q (name, email, Steam id, message text, translations), before, limit. Most urgent first.
GET/conversations/:idreadConversation, last 50 messages (with translations, context, drafts, notes) and the player profile (customer).
GET/conversations/:id/messagesreadOlder messages: ?before=<seq>.
POST/conversations/:id/messagesreply{body, note?, attachments?, card?, lang?}. Translated for the player. note: true for an internal note. Reopens a closed conversation.
POST/conversations/:id/drafts/:msgId/approvereplySend an AI draft, optionally with an edited body.
DELETE/conversations/:id/drafts/:msgIdreplyDiscard an AI draft.
POST/conversations/:id/messages/:msgId/retractreplyUnsend a message sent to the player (team, AI or automation). It leaves the player's chat at once and stays struck through for the team. An item already claimed can't be taken back (409 card_already_claimed). An API key can unsend messages from the AI, automations and agents, not from a person.
POST/conversations/:id/ai-suggestreplyHave the built-in AI answer or draft now.
PATCH/conversations/:idreply{status, snoozeUntil, assigneeId, aiMode, priority}. aiMode: inherit, auto, draft, off. priority: 0–2.
POST/conversations/:id/readreadMark as read by the team.
POST/conversations/:id/tagsreply{name}
DELETE/conversations/:id/tags/:namereply
GET/tagsreadTags with usage counts.
GET/saved-repliesreadSaved replies; POST, PATCH, DELETE need reply.
POST/uploadsreplyMultipart file; returns an attachment to put in attachments.
GET/statsreaddays (≤ 365), app: volumes, first response time, AI share, satisfaction, per-day series, conversations by player language (byLanguage).

Cards

Add a card to a message to show a button in the chat:

{
  "body": "Sorry about that! Here's a little something.",
  "card": {
    "kind": "item",                 // review | offer | link | screen | item | refund | restore
    "label": "Claim",
    "item": { "id": "gems_500", "name": "500 gems", "quantity": 1 }
  }
}

Other fields: url (link, review fallback), screen and value (screen), offerId (offer). An item card can be claimed once; you get an item.claimed webhook to grant it on your server.

Leave out label on review and refund cards to show the SDK's own text in the player's language (“Leave a review”, “Request a refund”). Cards Inngame sends itself carry the English default plus "defaultLabel": true; SDK 0.2.2 and later replace it with the translated text.

Players

MethodPathScopeNotes
GET/usersreadapp, q, before. Most recently seen first.
GET/users/:idreadProfile, devices, metadata (including subscription status from RevenueCat) and conversations.
PATCH/users/:idreply{teamName, blocked}. teamName is a name only your team sees.
POST/users/:id/messagesreplyWrite first to a player who has no conversation yet.
GET/users/:id/exportreadEverything held on the player as a JSON file, for data access requests: profile, devices, conversations with messages, translations, context and tags, bug reports. Photos as links valid 7 days. No tokens. Logged as user.export.
DELETE/users/:idhuman adminDelete the player, their conversations and files. Agent keys get 403 human_approval_required.

Apps, organization and keys

MethodPathScopeNotes
GET/orgreadOrganization, plan, AI provider, this month's usage, and who you are (key name and scope).
PATCH/orgall{name, aiProvider, aiKey, aiModel}: see BYOK.
DELETE/orgowner, signed in{confirm} with the exact organization name. Deletes everything in the organization (apps, players, conversations, files, keys, integrations) and cancels its subscription. API keys can't.
GET/appsreadApps with public keys and settings.
GET/apps/:idreadOne app. The identity secret is included only for admins.
POST/appsallCreate an app.
PATCH/apps/:idallRename or change settings (deep-merged): AI mode and pause, translation, moderation, appearance (texts; light and dark colors as #RRGGBB: accent, background, text, header, incoming, outgoing), review ids, verified-only, retention.closedDays (0 = keep forever, else 7 to 3650), csat ({enabled, askReview}: ask players to rate closed conversations, then offer the store review to those who rate 4 or 5)… Moderation, identity and retention need a human admin.
DELETE/apps/:idall
POST/apps/:id/rotate-identity-secretall{unverify?}
POST/apps/:id/logoallUpload the chat logo: multipart field file, PNG or JPEG, up to 512 KB. Returns {logoUrl}, a public URL that changes on every upload (cache it forever). DELETE removes it. Players get it as appearance.logoUrl in the SDK config.
GET/membersreadTeam members. Invitations and roles need all.
GET/api-keyshuman adminPOST {name, scopes} creates a key (shown once); DELETE revokes. Agent keys can't manage keys.
GET/auditallAudit log: app, actor, target, before. Each entry says whether it is undoable or already undone.
POST/audit/:id/undoadmin, signed inUndo an entry: unsend the messages it sent (replies, AI answers, cards, automations), remove the tag, put the conversation back as it was, or unblock the player. Once per entry. API keys can't undo.
GET/billingreadPlan, trial end, usage and quota. Checkout is for signed-in human admins only.

Bugs

The same bugs as the Bugs page of the inbox. Every bug has a url to that page, which your teammates can open from Telegram, Discord, the weekly digest or an agent's summary.

MethodPathScopeNotes
GET/bugsreadapp, status (open, fixed, released, wontfix; comma-separated for several), q (search), sort (players, newest, recent), limit (max 200). Each bug comes with reports (players), reports24h, notified, firstSeen, lastSeen, versions, platforms and url; plus counts per status.
GET/bugs/countsread{open, fixed, closed, all}, optional app.
GET/bugs/:idreadBug, players over 30 days (timeline), breakdown by version, platform, device and OS, players' current versions, log excerpts with the last error (samples), linked conversations (reports), whether GitHub is connected, and a preview of the issue body.
POST/bugsreply{conversationId, bugId} to link a conversation, or {conversationId, title, summary} to create a bug.
PATCH/bugs/:idreply{title, summary, status, fixedInVersion}. Setting fixedInVersion notifies reporters who already run it, and the others when they update. Returns notified.
POST/bugs/:id/github-issuereplyOpens the issue in the connected repository. Returns url.
POST/bugs/:id/mergereply{into}: moves the reports to another bug of the same app and deletes this one. A player in both is counted once.
POST/bugs/:id/request-fixallComments on the issue to ask the repository's coding agent (default @claude) for a PR. Never merges.

Refunds

MethodPathScopeNotes
POST/conversations/:id/refund-proposalreply{provider, reference, amountCents?, reason}, provider: stripe, googleplay, apple. Leaves a note for an admin; returns 202 awaiting_human_approval.
POST/conversations/:id/refundhuman adminAPI keys get 403 human_approval_required. Stripe {paymentId, amountCents?}, Google Play {orderId, revoke}, Apple {transactionId} (sends Apple's refund sheet to the player).

FAQ and automations

MethodPathScopeNotes
GET/apps/:id/faqreadArticles.
POST/apps/:id/faqreply{title, body, published}. PATCH and DELETE on /apps/:id/faq/:articleId.
POST/apps/:id/faq/importall{url, replace}: an https page or sitemap (up to 30 pages).
POST/apps/:id/faq/generateallDrafts up to 15 unpublished articles from past conversations.
GET/apps/:id/automationsreadAutomations with 30-day run counts, subscription events and suggested game events.
POST/apps/:id/automationsallCreate. Returns the Track snippet for Unity and Swift. PUT and DELETE on /apps/:id/automations/:automationId.

An automation has a triggerEvent (a game event or a subscription event such as trial_started, subscription_cancelled, billing_issue), a frequency, an audience (rules on player metadata), a delay (now, minutes or local_time) and actions: message, card (offer, link, screen, item), review, open_chat, wait.

SDK endpoints

The SDKs call /v1/sdk/* with the public app key in X-App-Key and a device token as bearer. Use them directly only if you're writing your own client (a web game, another engine).

MethodPathScopeNotes
POST/sdk/sessionapp key{device, identity?}. Returns a device token (keep it), the player, conversation id, unread count and remote config. The config has availability: {online, replyTime, nextOpenAt}: replyTime is "minutes", "hours", "day" or null (too little history, or hidden); nextOpenAt (Unix ms) is set when the team is outside its office hours.
POST/sdk/identifydevice{userId, name, email, token, steamTicket, metadata}
GET/sdk/messagesdevice?before=<seq>
POST/sdk/messagesdevice{body, clientId, attachments, context}. clientId makes retries safe; context ≤ 64 KB.
POST/sdk/uploadsdeviceMultipart file. Images are moderated; rejected ones return 422 photo_rejected and are never stored.
POST/sdk/readdeviceMark team messages as read.
GET/sdk/unreaddeviceUnread count (for polling).
POST/sdk/pushdevice{token, kind}, kind: apns, apns_sandbox, fcm. null unregisters.
POST/sdk/eventsdevice{name, props, opensChat}. May return openChat: true.
POST/sdk/cards/:messageId/actiondevice{action, nonce?, rating?}: tapped, failed, claimed, dismissed, rated. For rated (satisfaction cards only), send rating from 1 to 5; only the first rating counts and the answer returns it. For claimed, send a random nonce and keep it for retries: the answer has granted: true only for the claim that won (or its retry with the same nonce). Grant nothing otherwise. 404 if the team unsent the card.
GET/sdk/faqapp key?q= searches published articles.
POST/sdk/logoutdeviceDevice continues as a new anonymous player.
POST/sdk/delete-datadeviceErase the player.
GET/sdk/wsdeviceWebSocket for real-time messages (app_key and token as query parameters).

Webhooks

Webhooks send events to your server: grant claimed items, log refunds, page someone on a bug spike. Manage them in the inbox (Settings → Webhooks) or with the API, signed in as an admin. Agent keys can't create or read webhooks, even with scope all: a webhook can send every message to any URL, so a person sets it up.

MethodPathScopeNotes
GET/webhooksadminYour webhooks, last delivery status, and the list of events.
POST/webhooksadmin{url, appId?, events?}. https only. events defaults to ["*"]. Returns the secret (whsec_…) once.
POST/webhooks/:id/testadminSends a ping event and returns the result.
DELETE/webhooks/:idadmin

Each delivery is a POST with a JSON body, a user-agent of Inngame-Webhooks/1 and an inngame-signature header. Respond with a 2xx within 8 seconds. Failed deliveries (network errors, timeouts, 408, 429 and 5xx) are retried after about 1 and 5 minutes, then 30 minutes, 2, 6 and 12 hours: seven attempts over about a day. Another 4xx is taken as a refusal and not retried. Retries keep the same event id, so make your handler idempotent. The last result is on the webhook (last_status, last_error, pending retries). POST /webhooks/:id/test pings that webhook only.

{
  "id": "evt_8sd7f6g5h4j3k2l1m0n9",
  "type": "item.claimed",
  "created": 1793452800000,
  "appId": "app_...",
  "data": {
    "messageId": "msg_...",
    "conversationId": "cnv_...",
    "player": { "id": "eu_...", "externalId": "player-1842", "steamId": null },
    "card": { "kind": "item", "label": "Claim", "item": { "id": "gems_500", "name": "500 gems", "quantity": 1 }, "claimedAt": 1793452800000 },
    "action": "claimed"
  }
}

Event list

EventWhendata
message.createdA message is sent in a conversation (player, team or AI; not drafts or notes).conversationId, playerId, message
item.claimedA player claims an item card (once per card).messageId, conversationId, player, card, action
card.actionA player taps, dismisses or fails to open any other card.Same as above.
refund.issuedAn admin issued a refund or sent the Apple refund sheet.conversationId, playerId, provider, id, status
bug.spikeReports for a bug spike within 24 hours.bugId, title, reports24h, url
conversation.closedA conversation is closed (inbox, API, Telegram or Discord).conversationId, playerId, by
player.blockedA player is blocked.playerId, by
message.retractedThe team unsent a message.conversationId, messageId, playerId, by
conversation.ratedA player rated a closed conversation (satisfaction on).conversationId, messageId, rating (1 to 5), player
digest.weeklyThe Monday digest of the past week.apps[], suggestions[]
pingSent by the test endpoint.hello

Verifying signatures

The header looks like this:

inngame-signature: t=1793452800,v1=pZ3f…Kq8
// Node.js (Express)
import crypto from "node:crypto";

app.post("/inngame-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("inngame-signature") ?? "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  const body = req.body.toString("utf8");
  const expected = crypto
    .createHmac("sha256", process.env.INNGAME_WEBHOOK_SECRET)
    .update(`${t}.${body}`)
    .digest("base64url");
  const ok =
    parts.v1 &&
    parts.v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) &&
    Math.abs(Date.now() / 1000 - t) < 300;
  if (!ok) return res.status(400).send("bad signature");

  const event = JSON.parse(body);
  if (event.type === "item.claimed") grantItem(event.data.player, event.data.card.item);
  res.sendStatus(200);
});
# Python
import base64, hashlib, hmac, time

def verify(header: str, body: bytes, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = parts.get("t", "")
    mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).digest()
    expected = base64.urlsafe_b64encode(mac).rstrip(b"=").decode()
    return hmac.compare_digest(parts.get("v1", ""), expected) and abs(time.time() - int(t)) < 300