uloggdDevelopers
Developer portaluloggd
Get startedResourcesArcade
Developer portaluloggdIdentityCatalogProfileLibraryReviewsJournalListsAwardsScreenshotsSocialAccountNotificationsCommentsLikesHistoryPeopleMineralsArcade

Profile

The owner's own profile.

No scope here reaches anybody else's. Sending only the fields that change leaves the rest as they stand.

GET/api/v1/drafts

Lists the owner’s drafts.

Scope profile.read · Counted against the read allowance

POST/api/v1/drafts

Saves a draft.

Scope profile.write · Counted against the write allowance

GET/api/v1/drafts/{id}

Reads an owner’s draft.

Scope profile.read · Counted against the read allowance

PATCH/api/v1/drafts/{id}

Updates an owner’s draft.

Scope profile.write · Counted against the write allowance

DELETE/api/v1/drafts/{id}

Deletes an owner’s draft.

Scope profile.write · Counted against the write allowance

GET/api/v1/profile

The owner's profile.

Scope profile.read · Counted against the read allowance

GET/api/v1/profile/images

The pictures the account has used before, so one can be gone back to without finding the file again. Putting a new one up is the image pipeline's job, not this one's.

Scope profile.read · Counted against the read allowance

Query

FieldTypeNotes
kindrequiredstringAVATAR or BANNER.
DELETE/api/v1/profile/images/{id}

Forgets an old picture. The one on the profile right now stays where it is.

Scope profile.write · Counted against the write allowance

PATCH/api/v1/profile

Change the display fields. Send only what changes; the rest is left as it stands.

Scope profile.write · Counted against the write allowance

Body

FieldTypeNotes
display_namestringUp to 60 characters.
biostringUp to 500 characters.
pronounsstringUp to 30 characters.
thoughtstringUp to 100 characters.
youtube_usernamestringUp to 60 characters.
instagram_usernamestringUp to 30 characters.
twitter_usernamestringUp to 15 characters.
library_visibilitystringWho sees the library: PUBLIC, FOLLOWERS or PRIVATE. It belongs to the profile, not to each game.
GET/api/v1/profile/layout

The owner's Profile Studio layout: draft (the copy being edited), published (what visitors see, or null), version (the version the next save must name), published_version, published_at, updated_at, customized and draft_incomplete (the stored draft held something this build cannot read and comes without it). With nothing saved, draft is the default layout, version is 0 and customized is false. A stored document this build cannot read is shown as the default but under its real version, so the next save succeeds. media resolves every image uploaded for the layout, by id, to url, width, height and animated: all of the owner's, since the draft may name any of them.

Scope profile.read · Counted against the read allowance

Example response

{
  "data": {
    "draft": { "schema": 2, "page": { ... }, "sections": [ ... ] },
    "published": null,
    "version": 3,
    "published_version": null,
    "published_at": null,
    "updated_at": "2026-10-09T18:20:00.000Z",
    "customized": true,
    "draft_incomplete": false,
    "media": {
      "k3Jd8Qp2Lm": {
        "url": "https://media.uloggd.com/layout/.../....avif",
        "width": 1600, "height": 900, "animated": false
      }
    }
  }
}
PUT/api/v1/profile/layout

Saves the draft. Visitors keep seeing the published copy until POST /api/v1/profile/layout/publish. version is the version the editor loaded; if another tab saved first, the answer is 409 conflict with the current version in error.version, so the editor reloads rather than overwrites. An invalid document is 400 invalid_request with error.issues (up to 20, each with code, message and, where it applies, path and type). Answers version and updated_at; saving a draft identical to the current one does not change the version.

Scope profile.write · Counted against the write allowance

Body

FieldTypeNotes
layoutrequiredobjectThe document: schema 2, page (width, density, background) and 1 to 12 sections, each of a kind. A flow section has 1 to 3 columns whose spans add up to 12, and each column up to 12 blocks. Every block has id, type, visibility (mobile, tablet, desktop), style and config. type is one of these and nothing else: identity, shortcuts, social, stats, showcase, games, reviews, activity, lists, screenshots, image, comments, text, heading, divider, spacer. A canvas section is an area of fixed proportions (aspect, height over width, from 0.2 to 3; phone, scale or stack) holding up to 24 freely placed elements, in the order they are drawn and read: each has id, type (image, text, shape or block, the last only with stats, social, heading, shortcuts or image), box (x, y, w, h as fractions of the area's width, inside its edges; w and h from 0.001), rotation, opacity, locked, visibility and config. background, on the page, a section or a block's style, is an image: media (or null, which draws nothing), fit, position, dim (0 to 80, how much it is darkened) and tone (auto, light or dark: the theme the site's own colours take over the image; light and dark fix it, and auto counts an image with a dim of 50 or more as dark and leaves the visitor's theme alone below that; with no image drawn, tone changes nothing). There is no background colour and no text colour: text, a text element's included, follows the theme; only a shape has colours, fill and stroke (#rrggbb or null). A line shape may have a thickness (0.001 to 0.05 of the area's width); without one, strokeWidth counts in pixels at a width of 1000px. Texts and embedded blocks are laid out at a reference width of 1000px (a text of size 0.05 is 50px) and scaled with the area; under phone scale a text has no minimum size, and under phone stack the elements follow their position (top to bottom and, along a row, left to right) and a text stays between 12px and 40px. An image is always a reference, never a URL: { kind: media, id } (uploaded through /api/v1/profile/layout/media), { kind: screenshot, id } (one of the owner's screenshots) or { kind: cover, slug } (a game's cover). identity is required and always visible. At most 40 blocks, 80 elements and 48000 bytes of UTF-8. A schema 1 document (column sections only, no backgrounds) is still accepted: it is upgraded to 2 and stored that way. Under either schema, the colours that no longer exist are dropped rather than refused: color on a background (a background that only had a colour, with no media, becomes null), textColor on a block's style, and color, background and radius on a text element's config. No answer carries those keys.
versionrequiredintegerThe version the editor loaded: 0 when nothing is saved yet (also after a DELETE).

Example response

{ "data": { "version": 4, "updated_at": "2026-10-09T18:21:02.000Z" } }
DELETE/api/v1/profile/layout

Goes back to the default layout: removes the draft and the published copy, for the owner and for everyone. Doing it twice is not an error. After it, the next save names version 0.

Scope profile.write · Counted against the write allowance

Example response

{ "data": { "reset": true } }
POST/api/v1/profile/layout/publish

Publishes the saved draft at the given version, which becomes what visitors see. If the draft has changed since, 409 conflict with the current version in error.version; with no saved layout, 404 not_found. Publishing does not change the version.

Scope profile.write · Counted against the write allowance

Body

FieldTypeNotes
versionrequiredintegerThe version of the draft the editor is showing, 1 or more.

Example response

{ "data": { "version": 4, "published_at": "2026-10-09T18:22:10.000Z" } }
GET/api/v1/profile/layout/media

The images the owner uploaded for the layout, newest first: id, url, width, height, bytes, animated, created_at and in_use (the draft or the published layout names the id). quota carries limit (15), count, bytes_limit (10485760, 10 MB) and bytes. Session only; a key gets 403.

Scope profile.read · Counted against the read allowance

Example response

{
  "data": [
    {
      "id": "k3Jd8Qp2Lm", "url": "https://media.uloggd.com/layout/.../....avif",
      "width": 1600, "height": 900, "bytes": 184320, "animated": false,
      "created_at": "2026-10-11T19:02:00.000Z", "in_use": true
    }
  ],
  "quota": { "limit": 15, "count": 1, "bytes_limit": 10485760, "bytes": 184320 }
}
POST/api/v1/profile/layout/media

Uploads an image to use in the layout, as multipart/form-data with the file in image (JPEG, PNG, WebP, GIF or AVIF, up to 15 MB; an animated GIF or WebP stays animated). It is shrunk to fit 1600x1600, never enlarged, and optimized: a still image becomes an AVIF of about 300 KB and never over 1 MB, an animation a WebP of at most 3 MB and 10 seconds. It goes through the same content check as an avatar, is stored on the image host and gets an id, which is what the layout document names. Answers 201 with id, url, width, height, bytes, animated and created_at. A refused image is 400 invalid_request with a reason: invalid_format (not a JPEG, PNG, WebP, GIF or AVIF, or the content is not the type sent), too_large (over 15 MB), invalid_image (it could not be read), too_many_frames (over 300 frames), too_many_pixels (over 40 million pixels, counting every frame), animation_too_long (over 10 seconds), unsupported_animation (an animated PNG or AVIF, or an animation without each frame's duration), too_heavy (still over 1 MB, or 3 MB animated, after optimization) or sensitive_image (the content check refused it). 409 with reason quota_exceeded (and limit, count, bytes_limit, bytes) when the account already holds 15 images or 10 MB, or this one, once optimized, would take it past 10 MB; 429 from the allowance of 30 uploads per 10 minutes, or with reason busy when processing is full; 403 with reason mfa_required on a session that still owes its second factor; 500 internal with reason upload_unavailable, screening_unavailable or upload_failed when the image host or the content check fails. Session only.

Scope profile.write · Counted against the write allowance

Body

FieldTypeNotes
imagerequiredfileThe file, as a multipart form field.

Example response

{
  "data": {
    "id": "k3Jd8Qp2Lm", "url": "https://media.uloggd.com/layout/.../....avif",
    "width": 1600, "height": 900, "bytes": 184320, "animated": false,
    "created_at": "2026-10-11T19:02:00.000Z"
  }
}
DELETE/api/v1/profile/layout/media/{id}

Deletes a layout image. While the draft or the published layout still names it, the answer is 409 conflict with reason in_use, unless force=true, which deletes it anyway (the layout keeps a gap where it was). 404 when there is no image of yours with that id; 403 with reason mfa_required on a session that still owes its second factor. Session only.

Scope profile.write · Counted against the write allowance

Query

FieldTypeNotes
forcebooleantrue to delete even while a layout still names it.

Example response

{ "data": { "id": "k3Jd8Qp2Lm", "deleted": true } }

The Profile Studio layout

A profile can be composed from blocks arranged in sections and columns, and from free areas where images, text and shapes stay where the owner puts them. The arrangement is one JSON document per profile, kept in two copies: the draft, which only the owner reads and which every PUT replaces, and the published copy, which is what visitors see. Saving never changes what visitors see; POST /api/v1/profile/layout/publish copies the saved draft over the published one. With nothing saved, the draft is the default layout, the profile everybody already knows, and published is null (visitors see the default). DELETE goes back to it for everyone. When the stored draft holds something this build cannot read (a block type from a newer release), draft_incomplete is true and draft comes without it: saving over it deletes it.

Every block's type is one of a closed set, and anything else is refused: identity, shortcuts, social, stats, showcase, games, reviews, activity, lists, screenshots, image, comments, text, heading, divider, spacer. A block's config holds options and references (a game slug, a screenshot's public id), never markup, script or a URL. identity, the profile's header, is required and cannot be hidden.

The document

The current document is schema 2. page holds the width (width), the space between sections (density) and an optional background (background), and sections holds 1 to 12 sections, each of a kind:

  • flow: 1 to 3 columns on a 12 column grid (the spans add up to 12), each column holding up to 12 blocks, one under the other.
  • canvas: an area of fixed proportions, with aspect (height over width, from 0.2 to 3) and phone (scale shrinks the whole area on a phone, stack stacks its elements in a column in the order they are seen: top to bottom and, along a row, left to right). It holds up to 24 freely placed elements: box gives x, y, w and h as fractions of the area's width, with w and h from 0.001, and no element crosses its edge (the editor keeps anything that is not a line at 0.02 or more). Every element has rotation, opacity, locked, visibility and a type: image, text, shape or block, which embeds a stats, social, heading, shortcuts or image block. The list's order is the drawing order (the last one sits on top) and also the order a screen reader reads them in.

Texts and embedded blocks are laid out at a reference width of 1000px and then scaled with the area, as one picture: a text of size 0.05 is 50px at that width, and a box with a w of 0.4 is 400px. So they break their lines in the same places at every width. Under phone scale, a text shrinks with the area on a phone, with no minimum size; under stack, it becomes text at the column's width again, at 60% of its size at the reference width, between 12px and 40px. The editor keeps the height of a text's, an embedded block's and a line's box fitted to what it holds.

A line shape is a stroke across the middle of its box, with an optional thickness (0.001 to 0.05 of the area's width), which scales with the area and is never thinner than 1px. A line saved before thickness existed keeps using strokeWidth, counted in pixels at a width of 1000px. On a rectangle or a circle, strokeWidth is the border, in pixels.

The page, a section and a block's style may have a background, which is always an image: media (the image, or null, which draws nothing), fit, position, dim (0 to 80, how much the image is darkened so text stays readable) and tone (auto, light or dark: the theme the site's own colours take over the image, so text stays readable in any theme). light and dark fix that theme; auto takes dark when dim is 50 or more and, below that, leaves the visitor's theme alone. With no image drawn, tone changes nothing. There is no background colour and no text colour: text, a text element's included, always follows the theme, and only a shape has colours of its own (fill and stroke, #rrggbb or null). An image is never a URL but a reference: { "kind": "media", "id": "..." } for an image uploaded under Layout images, { "kind": "screenshot", "id": "..." } for one of the owner's screenshots and { "kind": "cover", "slug": "..." } for a game's cover. A reference to somebody else's picture, or to one that is gone, draws nothing.

The whole profile holds at most 40 blocks (those in columns and those embedded in free areas, together) and 80 elements. A schema 1 document, from before free areas and backgrounds, is still accepted by a PUT: it is upgraded to 2 (every section becomes flow, with no background, and the image block's screenshot becomes its source) and stored that way, so the next GET answers schema 2.

A PUT that still sends the colours of before is not refused: they are dropped, under either schema. They are color on a background (a background that had a colour and no media becomes null), textColor on a block's style, and color, background and radius on a text element's config. A document stored with them is read without them, so no GET carries them.

Versions

version counts changes to the draft. GET answers the current one (0 when nothing is saved), and every save and publish names the version it was based on. If the draft changed in the meantime, in another tab or another device, the answer is a conflict that carries the current version, so the editor can offer to reload instead of overwriting somebody's work:

{
  "error": {
    "code": "conflict",
    "message": "The layout changed elsewhere. Reload it before saving.",
    "version": 5
  }
}

A save of a draft identical to the stored one keeps the same version. After a DELETE, the next save names version 0 again.

Validation

A document is checked in full before it is stored: its shape, unique ids, column spans adding up to 12, the per type and total block limits, the element limits, elements inside their area, the required header, and at most 48000 bytes. A refusal lists what is wrong, up to 20 issues, each with a stable code (shape, duplicate_id, spans, too_many_blocks, too_many_of_type, too_many_elements, out_of_bounds, required_missing, required_hidden, too_large), a message for people, and where it applies the path inside the document and the block type:

{
  "error": {
    "code": "invalid_request",
    "message": "The layout is not valid.",
    "issues": [
      {
        "code": "spans",
        "message": "The columns of a section must add up to 12.",
        "path": ["sections", 1, "columns"]
      }
    ]
  }
}

Layout images

A layout can show images the owner uploaded for it, and the document names them by id, never by URL. POST /api/v1/profile/layout/media takes the file as multipart/form-data in the image field (JPEG, PNG, WebP, GIF or AVIF, up to 15 MB), shrinks it to fit 1600x1600 and optimizes it: a still image becomes an AVIF of about 300 KB, never over 1 MB, and an animation (GIF or WebP) a WebP of up to 3 MB and 10 seconds. Then it runs it through the same content check as an avatar (a sensitive image is refused, since a profile page has no way to veil it), stores it on the image host and answers with the id, the url, width, height, bytes and animated.

A refused image is 400 invalid_request with a reason: invalid_format, too_large, invalid_image, too_many_frames, too_many_pixels, animation_too_long, unsupported_animation, too_heavy or sensitive_image (the table above says what each one means). too_heavy is an image still too heavy after optimization, nearly always a long or large animation:

{
  "error": {
    "code": "invalid_request",
    "message": "That image is still too heavy after optimization. Use a shorter or smaller animation.",
    "reason": "too_heavy"
  }
}

An account keeps up to 15 images and 10 MB (10485760 bytes); past that the answer is 409 with reason: "quota_exceeded", limit, count, bytes_limit and bytes. An account that already held more when the limit came down keeps everything it has, but uploads again only once it has removed enough to be under it.

GET /api/v1/profile/layout/media lists the images with in_use, which says whether the draft or the published layout names the id, and the quota beside data. DELETE /api/v1/profile/layout/media/{id} removes one; while a layout still names it the answer is 409 with reason: "in_use", and ?force=true removes it anyway. An image no layout has named for seven days is removed on its own.

These three endpoints are session only: a key gets 403, as under Account, and the upload and the removal answer 403 with reason: "mfa_required" while the session still owes its second factor. In the reads, media resolves each id to url, width, height and animated: in GET /api/v1/profile/layout, every image of the owner's; in GET /api/v1/profiles/{username}/layout, only those the published document names, so an image uploaded for the draft does not show before it is published.

Visitors read the published layout through GET /api/v1/profiles/{username}/layout, under People.

Catalog

Games, from the same catalog the site reads.

Library

What the owner is playing, has played, and wants to play.