> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agg.market/llms.txt
> Use this file to discover all available pages before exploring further.

# Avatar Upload

> Upload and set a user avatar with presigned S3 URLs

<Info>
  Avatar endpoints require a signed-in user session. See [Authentication](/recipes/authentication)
  for the initial sign-in flow.
</Info>

Avatar upload is a two-step process: request a presigned upload URL, then upload the file directly
to S3. After upload, confirm or poll for the processed avatar before updating the UI.

## Step 1: Get Upload URL

```typescript theme={null}
// POST /users/me/avatar
const { uploadUrl, assetKey, cdnUrl } = await client.createAvatarUploadUrl("image/jpeg");

// uploadUrl = presigned S3 PUT URL (valid for 10 minutes)
// cdnUrl   = where the processed image will be served from
```

## Step 2: Upload the Image

Upload the raw file bytes directly to the presigned URL:

```typescript theme={null}
const file = inputElement.files[0]; // from an <input type="file">

await fetch(uploadUrl, {
  method: "PUT",
  headers: { "Content-Type": file.type },
  body: file,
});
```

## Step 3: Wait for Processing

The image is processed asynchronously after upload. If you want the client to explicitly finish the
flow, retry `updateUser({ confirmAvatar: true })` until the processed asset is ready:

```typescript theme={null}
async function confirmAvatar(maxAttempts = 10) {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      const profile = await client.updateUser({ confirmAvatar: true });
      if (profile.avatarUrl) {
        return profile.avatarUrl;
      }
    } catch (err: any) {
      if (err.status !== 400) throw err;
    }

    await new Promise((r) => setTimeout(r, 1000));
  }

  throw new Error("Avatar processing timed out");
}
```

If you prefer not to confirm explicitly, you can also poll `GET /users/me` until `avatarUrl`
appears after processing finishes:

```typescript theme={null}
async function waitForAvatar(maxAttempts = 10) {
  for (let i = 0; i < maxAttempts; i++) {
    const profile = await client.getCurrentUser();
    if (profile.avatarUrl) {
      return profile.avatarUrl;
    }
    await new Promise((r) => setTimeout(r, 1000)); // wait 1s between polls
  }
  throw new Error("Avatar processing timed out");
}

const avatarUrl = await waitForAvatar();
```

## Constraints

| Constraint      | Value                           |
| --------------- | ------------------------------- |
| Max file size   | 5 MB                            |
| Allowed types   | JPEG, PNG, GIF, WebP            |
| Processing time | Usually 1-3 seconds             |
| CDN variants    | 44px, 48px, 60px, original, raw |

## Removing an Avatar

```typescript theme={null}
await client.updateUser({ removeAvatar: true });
```

## Error Handling

```typescript theme={null}
try {
  const { uploadUrl } = await client.createAvatarUploadUrl(file.type);

  const uploadRes = await fetch(uploadUrl, {
    method: "PUT",
    headers: { "Content-Type": file.type },
    body: file,
  });

  if (!uploadRes.ok) {
    throw new Error(`Upload failed: ${uploadRes.status}`);
  }

  const avatarUrl = await confirmAvatar();
  console.log("Avatar ready:", avatarUrl);
} catch (err: any) {
  if (err.status === 415) {
    console.error("Unsupported file type");
  } else {
    console.error("Avatar upload failed:", err);
  }
}
```
