Contract changelog
A reference for HTTP, realtime, and game client integrations.
Unreleased (breaking): Game client API v2 lists online characters
window.lootlogGameClientApi now reports apiVersion: 2. Only the
online-players contract changed:
getOnlinePlayers()results andonline-players:changedevents carryplayersas aPublicOnlinePlayer[]sorted by name, instead of a map from Discord ID to sessions.- Entries no longer include
discordId,sessionId,platform,status,guildId,characterId, oraccountId. Character fields moved fromplayerto the entry itself. Web app sessions, which have no character, are left out.
To migrate, accept apiVersion === 2
and read result.players as an array. Timers and Organizations are unchanged.
Public battle links no longer return the uploader's userId from
GET /battles/public/{battleId}.
Unreleased (breaking): Party gatherings belong to one Lootlog
A party gathering is created in exactly one Lootlog (Discord guild) and is visible only to its members.
POST /messaging/party-gatheringacceptsguildId. The deprecatedguildIdsis still accepted with exactly one element; send one of the two fields, not both. Two or more Lootlogs, or neither field, is a400.POST /messagingwithisGatheringParty: truerequires exactly oneguildIdselement; more is a400. Ordinary NPC notifications still accept up to ten Lootlogs.- A Lootlog the caller cannot create gatherings in is a
403, instead of being dropped from a list of targets. - Responses and realtime events keep
guildIds; a new gathering carries one element. Gatherings created before this release can carry several until they expire, at most 30 minutes later.
Deploy the API before the game client. Older game clients that send one Lootlog keep working; when they target several, creation fails instead of broadcasting the gathering. No persistence migration is required.
Unreleased (breaking): One error body, filter and page shape across the main API
Every main API operation now fails, filters and pages the same way, as described in errors and limits. Release the SDK as a breaking version.
- Error bodies are
{ code, message?, details? }. Bodies that were empty,{ statusCode },{ message }with a machine reason, or carried extra top-level fields now put the reason incodeand the extra values indetails. Validation failures moveissuestodetails.issues. - A
429sendsRetry-Afterinstead ofretryAfterMsin the body. npcIdinPUT /guilds/{guildId}/lootlog-config/{npcId}is an integer; a non-integer is a400instead of a404.- Lists that paged by offset or returned a bare array return
{ items, nextCursor }and take an opaquecursor. GET /guilds/{guildId}/events/{eventId}/kill-historyreturns its page initemsinstead ofdata, next to the unchangedkind,memberandnextCursor. A malformed or differently scopedcursorfails with codeINVALID_CURSOR, and alimitoutside 1 to 100 or amemberIdthat is not a positive integer is a400. Cursors issued before this release are rejected; restart pagination without a cursor.GET /guilds/{guildId}/lootsreturns{ items, nextCursor }instead of a bare array, newest first.cursoris the opaquenextCursorof the previous page instead of a loot id, and is valid only with the filters andlimitit was issued for; any other cursor fails with codeINVALID_CURSOR.nextCursorisnullon the last page.- Loot filters: an unknown
npcTypesorraritiesvalue is a400instead of a500, a level range whose minimum exceeds its maximum is a400,searchis trimmed and limited to 100 characters, and%and_in it match literally. npcTypesonGET /guilds/{guildId}/loots/statsis a repeated parameter instead of a comma-separated string; an unknown type is a400instead of being ignored.worldon loot endpoints, includingGET /users/@me/stats/loots, is trimmed and lower-cased.- A loot comment's
contentis trimmed and must be 1 to 256 characters. Itsmemberaddsid, always carriesavatarandroles, and a role'scoloris always present (nullwhen unset). - The
loot.snapshotrealtime event carries each submission'smemberwithidand a non-optionalavatar, asGET /guilds/{guildId}/lootsdoes. activeOnlyonGET /guilds/{guildId}/eventsaccepts onlytrueorfalse; any other value is a400. Omitting it still lists active events.worldon event and timer endpoints, including theworldof a new event, is trimmed and lower-cased, and an emptyworldis a400. The same applies to theworldwritten byPOST /loots,POST /killsand the timer create and reset requests, so a record is always found by the filter that names its world.- NPC level filters are named
npcLevelMinandnpcLevelMaxeverywhere. - A Member inside another resource is always
member: { id, name, avatar, userId }, withavatarset tonullwhen there is none. - Kill statistics (
/guilds/{guildId}/stats/killsand itstop-npcs,top-killers,npcs/{npcId}/killersandmembers/{memberId}reads, and/users/@me/stats/kills,/users/@me/kills/npcs):minLvlandmaxLvlbecomenpcLevelMinandnpcLevelMax, and a minimum above the maximum is a400.periodalso accepts90dand180d, and an omittedperiodnow means the last7dinstead of all time; sendperiod=allfor all-time statistics. An unknownnpcTypesornpcTypeis a400,searchis trimmed, limited to 100 characters and matches%and_literally, andworldis trimmed and lower-cased. limitontop-npcsandtop-killersis optional, defaults to 10 and 5, and must be 1 to 100.topNpcsLimiton/users/@me/stats/killsmust be 1 to 100.GET /users/@me/kills/npcsreturns{ items, nextCursor, total }instead of{ npcs, pagination }.GET /guilds/{guildId}/stats/kills/members/{memberId}replacespaginationwith top-leveltotalandnextCursor. On both,cursoris the opaquenextCursorof the previous page instead of an offset, and is valid only with the filters, sort and period it was issued for; any other cursor fails with codeINVALID_CURSOR.- Kill rankings (
memberRanking,top-killersentries, NPCkillers) and the member ofmembers/{memberId}carrymember: { id, name, avatar, userId }instead ofmemberId,memberName,memberAvatarandmemberUserId.
Unreleased: Public item and NPC catalog
The Search API adds two read-only endpoints that work without an API key or a
session: GET /public/catalog/items and GET /public/catalog/npcs. They serve
game facts only. No player, world or Organization appears in a response, and
neither does when Lootlog first saw an item or NPC, which would date boss kills.
- Each item lists
droppedBy, up to 20 NPCs it dropped from. A fight's drop counts for its only NPC, or for its only boss (elite II or III, hero, event hero, titan or colossus) when it fought a group. - Each
droppedByentry carriesretired:truewhen the NPC no longer drops the item, such as after a rework replaced its loot. An entry is retired when Lootlog has recorded the NPC's loot far too often since the item's last drop for the gap to be chance at the item's own drop rate. Lootlog records a fight only when it drops something above common, so the rate is per recorded fight, not per kill. Lootlog recomputes it daily, and a new drop clears it at once. - Each NPC lists
locations: the maps it is fought on, most fought first, each with itsshareof the NPC's latest 1000 recorded fights, from 0 to 1 and rounded to a hundredth. Maps under 1% of those fights are left out, so maps players only carry loot to stay off the list, while a hero keeps every map it spawns on. Lootlog recomputes them daily, so an NPC first seen today may have none yet. - Each item carries
elasticLevel:truewhen Margonem gives it the level of the character who loots it, as Lootlog saw it dropped at more than four levels. Itslvlandstatthen describe one of those levels, the one most recently seen.minLevelandmaxLevelfind an elastic item whenever the range reaches into the levels it comes at, 40 to 300. - Items sort by
lvl,nameorid:desc, which lists the items Margonem added most recently first. They filter byids,rarities,types,professions,minLevel,maxLevel,droppedBy(an NPCkey),dropSourceTypes,statKeys(a listed set of stats, all required;etiquettemarks event items),excludedStatKeys(stats an item must not have),event(an event's name and year as a slug, such aswakacje-2026ordzien-mlodego-smoka) andlegendaryBonuses(any of the listed bonuses, such asverycritorcurse); NPCs filter byids,keysandtypes. Both endpoints acceptgameVersion(ploren) to list one edition.limitis capped at 96. - Requests share the public rate limit of the main API's
/publicroutes.
Unreleased (breaking): Gains and best days removed from personal kill analytics
GET /users/@me/stats/kills/analytics no longer returns npcGains or the
bestDay of each entry in npcs. Release the SDK as a breaking version.
npcslists up to 500 monsters killed in the period, most killed first, instead of the top 20. Each entry still hasdeltaKillsanddeltaPercent; there is no separate ranking by growth.records.bestDay,records.bestWeekandrecords.bestMonthfor the whole period are unchanged.- The SDK's
UserKillAnalyticsResponseDtoOutputlosesnpcGains, and itsnpcsitems losebestDay.
Unreleased: Kill numbers in event kill history
GET /guilds/{guildId}/events/{eventId}/kill-history returns killNumber on
every event and member summary. It is the one-based position of the kill among
all recorded kills of its hero in the event, ordered by killedAt and then
id, so the hero's first kill is 1.
- Manual window closes are counted like other kills.
- The
heroIdandmemberIdfilters do not renumber kills: a member's list shows the hero's kill numbers, with gaps for kills the member missed.
Unreleased (breaking): Deprecated event kill lists removed
The event, member and hero kill lists deprecated in favour of
GET /guilds/{guildId}/events/{eventId}/kill-history are removed. There is no
compatibility window: once this API version is deployed, the removed routes
return 404, including for SDK 0.1.1 and earlier and any other client still
calling them. Migrate to /kill-history before the deployment. Release the SDK
as a breaking version.
| Removed GET route | Replacement query on /kill-history |
|---|---|
/guilds/{guildId}/events/{eventId}/kills | Optional heroId |
/guilds/{guildId}/events/{eventId}/members/{memberId}/kills | memberId, optionally heroId |
/guilds/{guildId}/events/{eventId}/heroes/{heroId}/kills | heroId |
- UUID cursors from the removed lists are not accepted by
/kill-history. Restart pagination without a cursor and pass itsnextCursorunchanged. - Lists return summaries with
participantCount. Read participants, scoring and map presence fromGET /guilds/{guildId}/events/{eventId}/heroes/{heroId}/kills/{killId}and its/timeline; both are unchanged. - The SDK no longer exports
eventsRankingControllerGetEventKillHistory,eventsRankingControllerGetMemberKillHistory,eventsRankingControllerGetHeroKillHistory, their URL helpers and parameter and error types,EventKillHistoryResponseDtoorEventMemberKillHistoryResponseDto. UselistEventKillHistory.
Unreleased (breaking): Time-ordered battle IDs and opaque list cursors
Battle IDs are now UUIDv7 values whose first 48 bits are the battle's
createdAt, so newer battles have greater IDs. Release the SDK as a breaking
version.
- Every existing battle has a new ID. Battle, list, timeline, analytics and
POST /battlesresponses return the new ID. Paths that take a battle ID, including/battles/public/{battleId}, still accept the old ID and return the battle with its new ID. Store the new ID: old IDs keep resolving only until the timelines of those battles are removed from object storage. GET /battles/@mecursors are opaque. PassnextCursororpreviousCursorback unchanged; do not build or parse them. Cursors issued before this change, and any other malformed cursor, are rejected with400instead of returning the first page.previousCursoris returned wheneverhasPrevistrue. The previous page holds thesizebattles directly before the current page. Battles saved since the first page was read appear on the pages before it, andhasPrevstaystrueuntil the newest battle is reached.createdAtis unchanged: the time Battlelog saved the battle, with millisecond precision.startDateandendDatefilters compare against it.- Raw battle data (
/battles/{battleId}/rawand its public variant) keeps its shape. For battles saved after this change,timestampis the battle'screatedAt.
Unreleased (breaking): Battle participants no longer return id
Battlelog identifies a participant by its battle and Margonem character, so the separate participant ID is removed. Release the SDK as a breaking version.
- Battle, battle list, public battle and timeline responses no longer return
warriors[].id. Identify a participant bybattleIdandoriginalId;originalIdis unique within a battle. statisticsis unchanged. Battlelog now derives it from the participants when it reads a battle instead of storing it, with the same values.POST /battlesrejects a fight event whosef.wkey differs from the warrior'soriginalIdwith400. Margonem keys player warriors by their ID, so payloads captured from the game are unaffected.
Unreleased: Relative NPC icons on loots, kills and timers
Margonem stores an NPC icon relative to its NPC image directory and adds the CDN address only to render it. Some clients report the rendered URL instead, so one observation of an NPC produced two revisions.
POST /loots,POST /killsandPOST /timers/autoare unchanged. The API now storesnpcs[].iconandnpc.iconrelative to the NPC image directory:https://micc.garmory-cdn.cloud/obrazki/npc/her/a.gif,//micc.garmory-cdn.cloud/obrazki/npc/her/a.gif,/obrazki/npc/her/a.gifand/her/a.gifare all stored asher/a.gif, and identify the same NPC revision.- A one-time repair rewrites stored icons of NPC revisions, kill statistics, timers, timer history and event heroes the same way. NPC revisions that differed only in the icon form are merged; a loot can therefore link a different NPC revision id with the same name, level and type.
- Loot, kill, timer and NPC search responses return the stored icon. Build the
image URL by prefixing
https://micc.garmory-cdn.cloud/obrazki/npc/. Search documents written earlier can still hold a full URL; use an absoluteiconas is.
Unreleased (breaking): Game version required, deprecated search fields removed
The one-time repair of per-edition revisions is complete: every loot and NPC search hit has a game version. The compatibility paths kept for the rollout are removed. Release the SDK as a breaking version.
POST /lootsrequiresgameVersion(ploren). A loot without it, or withnull, is rejected with400. The API no longer derives an edition fromworld. Derive it from the game page's hostname; when the host is not a recognized edition domain, do not submit the loot./npcsand/itemsno longer declare theworldquery parameter. The service ignores it if an older client still sends it. On/all,worldstill filters players.- NPC hits no longer return
world; useworlds.gameVersionon NPC hits is no longer nullable. - Notification rule responses, including the rule of a watched item, no longer
return
unresolvedSelections. The selections it would have reported were never recorded, so the array was always empty.
Unreleased: NPC and item revisions per game edition
Every world of a Margonem edition uses the same NPC and item ids. NPC revisions and search results are now one per edition instead of one per world, and every loot has a game version.
POST /loots: sendgameVersionwith every loot, derived from the game page's hostname. Omitting it, or sendingnull, is deprecated, and a later breaking release will reject such loots. Until then the API uses the edition ofworld:cronus,husariaandsteamrealmareen, every other world ispl. A value you send always wins.- NPC search (
/npcs, and the NPC hits of/all) returns one hit per game version, identity namespace, id and Margonem type, with a newworldslist of the edition's worlds where the NPC was observed.worldon an NPC hit is deprecated: it names the world of the latest observation.gameVersionis always set; the field stays nullable for compatibility. - The
worldquery parameter of/npcsand/itemsis deprecated and ignored, and on/allit only filters players. A later breaking release removes it from NPC and item search. Item hits were already one per edition, id and name with aworldslist. - A one-time repair gives existing loots and revisions an edition. Loot links from the other edition move to that edition's revision; an item link takes that edition's name and icon, or keeps its stored name when the edition has none. Loot, item and NPC ids are unchanged.
Unreleased: Unresolved notification selections and repaired loot links
Some saved notification selections do not identify what their rule matches: timer rules saved with an NPC id from the legacy search catalog, and watched items whose saved name came from the other game edition.
- Notification rule responses, including the rule of a watched item, add
unresolvedSelections. Each entry names thekind(npcoritem), the savedselectedIdandselectedName, areason, and for a timer rule an optionalsuggestedIdandsuggestedName. The array is empty for most rules. - The API never changes a rule's filters to resolve an entry. It disappears
when the rule is saved without that id, or when the watched item is added
again. Treat
suggestedIdas a suggestion; applying it is a normal rule update. - Saving or rebuilding a timer rule no longer schedules notifications for timers whose spawn window has already closed, and does not send a notification again for an occurrence that was already sent.
- A one-time repair moves some historical loot links to new NPC and item revisions: an NPC level observed on timers of the same edition at the time of the loot, or the name and icon of the loot's own edition. A loot's NPC level can therefore change, and with it which roles can read the loot. Loot and item ids are unchanged.
Unreleased: Item names, icons and instance stats per loot
A loot item now shows the name and icon it was reported with. Earlier, a later loot of an item with equal stats reused the first stored name and icon, so a Polish loot could show an English name, and an icon change was lost.
- Loot item
statincludes that loot's owncreated,gold,amountandopisvalues. Earlier it repeated the values of the first loot stored with the same stats. Loot items accepted before this change keep their storedstat. - Item search can return several hits with the same
id: one per game version and name, such as a Polish and an English name of one item. Deduplicate byidandnamerather than byidalone. POST /lootsis unchanged. Sendname,iconandstatexactly as the game shows them; do not translate them.
Unreleased: Margonem game version on loots and NPC search
Margonem runs a Polish edition on *.margonem.pl and an international edition
on *.margonem.com. The same template or item id can mean different things in
each edition, and a world name does not identify the edition.
POST /lootsaccepts an optional nullablegameVersion:ploren. Derive it from the game page's hostname: the edition domain or any subdomain of it. Sendnull, or omit the field, when the host is not a recognized edition domain; never default topl. The API validates the value but cannot verify where the client ran, so do not treat it as proof of anything else.- Retrying a loot, or submitting one another client already reported, keeps the game version it was first accepted with.
- NPC search hits add a nullable
gameVersion. Equal ids with different game versions are different NPCs. See "NPC and item revisions per game edition" for how the API now fills a missing game version.
Deploy the database migration, the search service, the API and the game client in that order. Older clients keep working and their loots stay unknown.
Unreleased: Timer NPC search follows timer visibility
GET /guilds/{guildId}/timers/npcs/search now applies the same visibility as
the timer list. Timers whose NPC type or level is outside the caller's roles
are omitted before limit is applied, so they no longer reveal an NPC's
identity, location or respawn configuration.
worldis optional. Without it, the search covers every world of the Organization, and each result addsworld.searchis optional whennpcIdsortemplateIdsis present. Both accept repeated positive integers:npcIdsmatches a timer's runtime NPC id andtemplateIdsmatches its template id. A request needs at least one ofsearch,npcIdsortemplateIds; otherwise it returns400.- Manual timers remain excluded.
Existing callers that send search and world keep their behavior, except
that hidden timers no longer appear. Generated clients include the new
parameters and field for the next SDK release.
Unreleased: Explicit NPC runtime and template identities
Margonem gives every NPC spawn a runtime id (npc.id, the battle
originalId) and every monster template a template id (npc.tpl). Several
spawns share one template.
POST /lootsaccepts optionalnpcs[].runtimeIdand nullablenpcs[].templateId. When either is present,npcs[].idis ignored for identity; keep sending it for older API revisions. SendtemplateId: nullwhen the template is unknown; never substitute the runtime id.POST /timers/autoaccepts optional nullablenpc.templateId.npc.idremains the runtime id and still identifies the timer, so spawns sharing a template keep separate timers.- Timer responses, timer history and
GET /guilds/{guildId}/timers/npcs/searchaddtemplateId, which is null until a client reports it. A later submission without it keeps the stored value. - Notification rules accept
npcTemplateIds, at most five together withnpcIdandnpcIds. A template id matches every timer observed with that template;npcIdandnpcIdsstill match a timer's own id. - Search NPC hits add
identityNamespace:template,runtime(a spawn observed without a template) orlegacy(an overloaded id from an older client). Equal ids in different namespaces are different NPCs.
Deploy the database migration, the search service, the API, the web app and
the game client in that order. Older clients keep working and write legacy
identities. Existing saved filters and legacy records are not remapped.
Unreleased: Conditional party observations
POST /messaging/party-gathering/{notificationId}/party-observation accepts
an optional positive integer expectedRevision. Use the latest room projection's
revision to prevent a delayed observation from overwriting a newer roster.
When supplied, a stale revision or a concurrent write returns
409 REVISION_CONFLICT. A successful conditional observation advances the room
revision even if the roster is unchanged and was observed moments ago. Requests
without the field keep their existing behavior, including returning the current
room without a new revision for an unchanged roster observed within 45 seconds.
After a conflict or an uncertain HTTP result, read the room again and retry the latest roster with its current revision. Bound both the requests and retry count; aborting HTTP does not cancel a server write. Deploy the API to every writer before relying on the precondition. No persistence migration is required. Generated clients include the optional field for the next SDK release.
Unreleased: Event hero lists keep history
PATCH /guilds/{guildId}/events/{eventId} no longer rebuilds the event's heroes
and maps when the request includes heroNpcs. Heroes are matched by npcName
and maps by mapId, so retained heroes and maps keep their IDs, kills, kill
points, tracking history and ranking entries. The list can add heroes, add
maps, rename maps and set a hero's npcId; omitting npcId keeps the stored
value.
As with the map create operation, a map added to an existing hero starts with
an open unassigned coverage gap.
A heroNpcs list that omits an existing hero or map now returns 400 and
changes nothing. Previously the omitted records were deleted with their history.
Repeated hero names, or a repeated mapId within one hero, also return 400.
Remove a hero with DELETE /guilds/{guildId}/events/{eventId}/heroes/{heroId}
and a map with
DELETE /guilds/{guildId}/events/{eventId}/heroes/{heroId}/maps/{mapId}.
Deleting a hero removes its maps, kills, kill points and tracking history, and now also its ranking entries, so event ranking totals match the remaining kill points. No migration is required.
Unreleased: Event kill history
GET /guilds/{guildId}/events/{eventId}/kill-history adds a paginated summary
of kills visible to the caller. It requires LOOTLOG_EVENTS_READ and supports
read-only API keys within their Organization scope. Optional query parameters
are heroId, memberId, cursor and limit. The limit is an integer from 1
to 100 and defaults to 20.
The response contains kind, data and nextCursor. kind is event unless
memberId is supplied, when it is member. Summaries include hero identity,
timestamps, manual closure status and participantCount. This count includes
all saved participant point records, including participants with
wasPresent: false. Member summaries also include the selected member and
their scoring and tracking data. Lists omit map presence, roles and other
participants' full records. Existing detail and timeline endpoints remain
available.
Treat the new cursor as opaque and pass it unchanged with the same Organization,
event and filters. Results use killedAt DESC, id DESC; the cursor remains usable
if its boundary kill is deleted. Invalid cursors, mismatched filters and invalid
limits return 400. Each request applies current access rules. Pagination does
not freeze a snapshot of the history.
The existing event, hero and member kill-list endpoints are deprecated. Their
response shapes and UUID cursors remain supported with corrected chronological
pagination. A missing, hidden or out-of-scope UUID cursor returns the same
400 Invalid cursor response. Inaccessible resources return 404.
Apply the database index migration and deploy the API before migrating clients to the new endpoint. The migration changes indexes only. Generated clients include the endpoint for the next SDK release; SDK publication remains a separate release step.
Unreleased — Realtime latency probe
The realtime v1 session.joined event and session.join response add an
optional capabilities list. Gateways that list connection.ping accept the
connection.ping command and answer it without side effects, including on
integration connections. The SDK realtime client adds probeLatency(), which
reports through subscribeHeartbeatLatency.
Deploy the gateway before relying on the probe. Clients send the command only when the capability is listed, so clients connected to older gateways keep the heartbeat measurement. No migration is required.
Unreleased — Notification schedules and role level ranges
Notification rule create and update requests accept scheduledUntil: null.
For a recurring rule, null removes the end date; omitting the field preserves
it. Switching to a one-time schedule clears recurring fields, including the end date.
Send only fields relevant to the selected schedule type. For daily and weekly
schedules, omit scheduledAt to calculate the next occurrence from the selected
time, weekday and timezone when the schedule changes.
Role update requests reject lvlRangeFrom and lvlRangeTo values that are not
integers from 0 to 500, and ranges where lvlRangeTo is lower than
lvlRangeFrom, with 400 Bad Request. A role saved earlier with a reversed range
must receive a corrected range on its next update.
Deploy the API before clients begin sending null. Existing valid requests remain
compatible, and no database migration is required. Generated SDK types include
these changes for the next SDK release.
2026-09-17 — SDK 0.1.1
Documentation update with runnable organization, timer and loot examples, filtering, cursor pagination and error handling. No runtime or API contract changes; no migration is required.
2026-09-17 — SDK 0.1.0
First npm release of @lootlog/sdk:
- Typed HTTP functions for the main, activity, battlelog and search APIs.
- A shared realtime client for server-side integrations.
- Public OpenAPI JSON exports and bundled TypeScript declarations.
Install with npm install @lootlog/sdk or bun add @lootlog/sdk. See the
SDK guide for configuration and usage. This package release does
not change the game client API version or introduce new HTTP endpoints.
Portal baseline
Documentation for the public game client API v1, an HTTP reference in Scalar, personal API keys, and an SDK for server integrations. The development environment supports verification before production releases.
The public game client object retains apiVersion: 1. Published contract changes will be documented here with migration instructions. Check the reference generated from service schemas for current HTTP operation parameters.