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.
/api/ v1/ draftsLists the owner’s drafts.
/api/ v1/ draftsSaves a draft.
/api/ v1/ drafts/ {id}Reads an owner’s draft.
/api/ v1/ drafts/ {id}Updates an owner’s draft.
/api/ v1/ drafts/ {id}Deletes an owner’s draft.
/api/ v1/ profileThe owner's profile.
/api/ v1/ profile/ imagesThe 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.
Query
| Field | Type | Notes |
|---|---|---|
kindrequired | string | AVATAR or BANNER. |
/api/ v1/ profile/ images/ {id}Forgets an old picture. The one on the profile right now stays where it is.
/api/ v1/ profileChange the display fields. Send only what changes; the rest is left as it stands.
Body
| Field | Type | Notes |
|---|---|---|
display_name | string | Up to 60 characters. |
bio | string | Up to 500 characters. |
pronouns | string | Up to 30 characters. |
thought | string | Up to 100 characters. |
youtube_username | string | Up to 60 characters. |
instagram_username | string | Up to 30 characters. |
twitter_username | string | Up to 15 characters. |
library_visibility | string | Who sees the library: PUBLIC, FOLLOWERS or PRIVATE. It belongs to the profile, not to each game. |
/api/ v1/ profile/ layoutThe 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.
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
}
}
}
}/api/ v1/ profile/ layoutSaves 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.
Body
| Field | Type | Notes |
|---|---|---|
layoutrequired | object | The 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. |
versionrequired | integer | The 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" } }/api/ v1/ profile/ layoutGoes 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.
Example response
{ "data": { "reset": true } }/api/ v1/ profile/ layout/ publishPublishes 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.
Body
| Field | Type | Notes |
|---|---|---|
versionrequired | integer | The version of the draft the editor is showing, 1 or more. |
Example response
{ "data": { "version": 4, "published_at": "2026-10-09T18:22:10.000Z" } }/api/ v1/ profile/ layout/ mediaThe 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.
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 }
}/api/ v1/ profile/ layout/ mediaUploads 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.
Body
| Field | Type | Notes |
|---|---|---|
imagerequired | file | The 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"
}
}/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.
Query
| Field | Type | Notes |
|---|---|---|
force | boolean | true 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 3columnson a 12 column grid (thespans add up to 12), each column holding up to 12 blocks, one under the other.canvas: an area of fixed proportions, withaspect(height over width, from 0.2 to 3) andphone(scaleshrinks the whole area on a phone,stackstacks 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 placedelements:boxgivesx,y,wandhas fractions of the area's width, withwandhfrom 0.001, and no element crosses its edge (the editor keeps anything that is not a line at 0.02 or more). Every element hasrotation,opacity,locked,visibilityand atype:image,text,shapeorblock, which embeds astats,social,heading,shortcutsorimageblock. 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.