Topolo Docs Topolo Platform
Open workspace

Upload my profile photo

Upload a PNG, JPEG, or WebP image up to 2 MiB for the signed-in user’s platform-wide profile.

Outcome

Upload a PNG, JPEG, or WebP image up to 2 MiB for the signed-in user’s platform-wide profile.

Captures exercise real Auth/workspace handlers and canonical isolated SQL. Identity, Brand responses and the object store are simulated.

Before you start

  • Use the intended authenticated human. API keys, service/delegated and app/surface-scoped identities cannot edit a global human photo. Third-party credential-bound OAuth requires explicit Auth users:write consent; organization context does not select another user.

  • Retain the prior preset/Brand reference or original image before a mutation, review the exact plan and its compensation, and read canonical state after an unknown result before retrying.

  • Uploaded raster URLs are public to anyone holding the URL and permit a five-minute browser cache. Use an image suitable for public access; replacing/removing stored bytes cannot recall downloaded or cached copies.

  • Use a complete PNG, JPEG or WebP raster up to 2 MiB decoded. Match MIME and signature and omit a data-URL prefix. Signature validation does not prove full rendering or appropriate dimensions.

Discover the target

topolo whoami --json
topolo actions get app_topolo_auth.me.avatar.upload --detail --json

Read me.get for the same authenticated human, or workspaces.list for the exact target app/organization, to resolve its current photo and uploaded UUID.

Input fields

FieldRequiredMeaning
fileNameYesNonempty original-file label. The stored avatar is identified by an opaque id; its original filename is not returned or retained as an identity.
contentBase64YesRaw standard base64 raster bytes, without a data-URL prefix. Decoded content may not exceed 2 MiB. Preserve the intended original image and avoid pasting private images into public logs.
contentTypeYesimage/png, image/jpeg or image/webp; it must match the detected raster signature. Signature validation is not full image decoding or a dimensions check.

Effects

  • Creates a public raster and changes only the verified human’s platform-wide photo URL.

  • Attempts cleanup after identity SQL failure and deletes previous uploaded bytes after the write.

Interpret the response

The target is the verified human user, with no userId/organization selector in input. Its photo is platform-wide across applications and organizations. API keys, service/delegated identities and app/surface-scoped sessions cannot edit it. A normal organization context does not change another person’s profile.

Third-party credential-bound OAuth requires explicit Auth users:write consent; profile consent alone is refused. Canonical context-RBAC sessions follow their separate runtime authority. Permission denial is not evidence of expired OAuth and does not authorize a credential-context change.

userId acknowledges the actual authenticated target. avatarUrl is the current public opaque URL, or null after removal. Read me.get for the same identity and verify profilePicture, then verify the returned image separately. An existing user is required; the action does not provision one.

Identity SQL and old-object cleanup are not one transaction. A failure after the canonical write can leave the new profile photo in place. Read the same identity before retrying. Restore a previous photo with its saved original bytes or separately reviewed removal when the old state was empty; restoration creates a new public URL.

Uploads accept only nonempty filenames and correctly encoded base64 PNG, JPEG or WebP signatures up to 2 MiB decoded. SVG, mismatched MIME, invalid base64 and excessive bytes are refused. The handler checks signature/size, not whether the full raster renders or its dimensions are appropriate.

Store creation precedes canonical identity/workspace SQL. If the canonical write fails, the handler attempts to delete the newly uploaded object. Failure of that cleanup is a server error and can leave an unreferenced object. Success does not prove Cloudflare durability, downstream rendering or cache refresh; verify these independently.

Avatar browser URLs are public bearer-by-possession URLs. Their binary responses permit a five-minute public cache. Removing or replacing stored bytes cannot recall copies already cached or downloaded. Do not use this feature for confidential images.

Worked examples

All ids, public URLs and raster bytes below are synthetic local fixtures. Resolve authorized live targets and review the exact unchanged plan before execution. These raw target responses do not prove the live gateway, provider persistence, public rendering or cache invalidation.

Authenticated human uploads a platform-wide personal photo

Input · payload.json:

{
  "fileName": "fixture-pixel.png",
  "contentType": "image/png",
  "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
}

CLI:

topolo actions validate app_topolo_auth.me.avatar.upload --data @payload.json --json
topolo actions plan app_topolo_auth.me.avatar.upload --data @payload.json --confirm --json
topolo actions call app_topolo_auth.me.avatar.upload --data @payload.json --confirm --detail --json

MCP call after topolo_validate_action and topolo_plan_action on this same input:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.me.avatar.upload",
    "input": {
      "fileName": "fixture-pixel.png",
      "contentType": "image/png",
      "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
    },
    "confirm": true,
    "detail": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Profile photo updated",
  "data": {
    "userId": "usr_avatar_owner",
    "avatarUrl": "https://auth.topolo.dev/api/avatars/00000000-0000-4000-8000-000000000003"
  },
  "timestamp": "2026-10-03T00:00:00.000Z"
}

File upload convenience · use the same reviewed bytes and published upload contract:

{}

Save this scope as scope.json; the CLI derives fileName/contentBase64 from the reviewed local file. Validate and plan its exact derived payload before confirming:

topolo actions upload app_topolo_auth.me.avatar.upload --file <reviewed-raster-file> --content-type image/png --data @scope.json --confirm --json

MCP upload after the same review:

{
  "tool": "topolo_upload_action",
  "arguments": {
    "actionId": "app_topolo_auth.me.avatar.upload",
    "fileName": "fixture-pixel.png",
    "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII=",
    "contentType": "image/png",
    "input": {},
    "confirm": true
  }
}

Input · payload.json:

{
  "fileName": "fixture-pixel.png",
  "contentType": "image/png",
  "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
}

CLI:

topolo actions validate app_topolo_auth.me.avatar.upload --data @payload.json --json
topolo actions plan app_topolo_auth.me.avatar.upload --data @payload.json --confirm --json
topolo actions call app_topolo_auth.me.avatar.upload --data @payload.json --confirm --detail --json

MCP call after topolo_validate_action and topolo_plan_action on this same input:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.me.avatar.upload",
    "input": {
      "fileName": "fixture-pixel.png",
      "contentType": "image/png",
      "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
    },
    "confirm": true,
    "detail": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Profile photo updated",
  "data": {
    "userId": "usr_avatar_owner",
    "avatarUrl": "https://auth.topolo.dev/api/avatars/00000000-0000-4000-8000-000000000004"
  },
  "timestamp": "2026-10-03T00:00:00.000Z"
}

File upload convenience · use the same reviewed bytes and published upload contract:

{}

Save this scope as scope.json; the CLI derives fileName/contentBase64 from the reviewed local file. Validate and plan its exact derived payload before confirming:

topolo actions upload app_topolo_auth.me.avatar.upload --file <reviewed-raster-file> --content-type image/png --data @scope.json --confirm --json

MCP upload after the same review:

{
  "tool": "topolo_upload_action",
  "arguments": {
    "actionId": "app_topolo_auth.me.avatar.upload",
    "fileName": "fixture-pixel.png",
    "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII=",
    "contentType": "image/png",
    "input": {},
    "confirm": true
  }
}

Verify the result

  • Read me.get with the same pinned human identity and match profilePicture to data.avatarUrl.

  • Read avatars.content for the discovered uploaded UUID and compare decoded bytes/MIME; verify browser rendering and persistence independently.

Errors and recovery

API-key identity cannot edit a human global photo

Input:

{
  "fileName": "fixture-pixel.png",
  "contentType": "image/png",
  "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
}

Captured local refusal · HTTP 403 · application/json:

{
  "success": false,
  "error": {
    "message": "A personal user session is required to change your photo",
    "code": "FORBIDDEN",
    "details": null,
    "timestamp": "2026-10-03T00:00:00.000Z"
  }
}

Input:

{
  "fileName": "fixture-pixel.png",
  "contentType": "image/png",
  "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
}

Captured local refusal · HTTP 403 · application/json:

{
  "success": false,
  "error": {
    "message": "OAuth consent must include Auth users:write to change your photo",
    "code": "FORBIDDEN",
    "details": null,
    "timestamp": "2026-10-03T00:00:00.000Z"
  }
}

Missing authenticated user is not created by an upload

Input:

{
  "fileName": "fixture-pixel.png",
  "contentType": "image/png",
  "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
}

Captured local refusal · HTTP 404 · application/json:

{
  "success": false,
  "error": {
    "message": "User not found",
    "code": "NOT_FOUND",
    "details": null,
    "timestamp": "2026-10-03T00:00:00.000Z"
  }
}

Identity SQL failure removes the newly uploaded uncommitted object

Input:

{
  "fileName": "fixture-pixel.png",
  "contentType": "image/png",
  "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
}

Captured local refusal · HTTP 500 · application/json:

{
  "success": false,
  "error": {
    "message": "Could not update your profile photo",
    "code": "SERVER_ERROR",
    "details": null,
    "timestamp": "2026-10-03T00:00:00.000Z"
  }
}

Published recovery guidance

  • state_conflict (HTTP 409): Read the current resource state, reconcile the intended change, and retry only if still valid.

  • AUTH_ERROR (HTTP 401): Verify the pinned human identity and actual credential expiry. An invalid identity does not authorize another target or context.

  • FORBIDDEN (HTTP 403): Use the intended human session and, for third-party credential-bound OAuth, explicit Auth users:write consent. API keys or delegated/app-scoped identities cannot edit a human global photo. Respect refusal.

  • NOT_FOUND (HTTP 404): Resolve the existing authenticated human. Photo actions do not provision users.

  • VALIDATION_ERROR (HTTP 400): Validate the upload filename, base64, decoded size and matching raster MIME/signature. Removal accepts exactly {}.

  • SERVER_ERROR (HTTP 500): Read me.get for the same pinned human before reviewing a retry. Identity SQL and old-object deletion can fail separately; the new state may already be stored. Preserve sanitized evidence for the authorized maintainer.

Recovery boundaries

Retain the previous original before replacing it. Read me.get after an unknown result. Separately review reuploading that original, or me.avatar.remove if the prior state was empty. Restoration creates a new opaque URL; it does not revoke cached or downloaded copies.

Discovery and evidence

This record belongs to Auth. Candidate human/machine URLs become valid only after native publication. Real canonical core/access migrations, identity/workspace SQL, raster validation, Brand response parsing and JSON/binary response construction were exercised. In-memory image storage, identities, seating, Brand workspace discovery and HTTP responses, ids/clock and fault injection are simulated. No live user photo, workspace avatar, Brand publication, OAuth state or Cloudflare object was changed.