Skip to content
Docs

Profile And Analytics

Edit the X profile (pictures, name, bio, location, website, handle), read it back, and read the account's own analytics by day.

Early access. Tell us what you're building and we'll set you up. Contact us

Edit The Profile#

PATCH /users/me/edit (or POST; accounts.write, optional Idempotency-Key), Unipile's "Edit own profile". Give any of these fields; the ones you leave out stay as they are:

Field Unipile's name What X takes
name 1 to 50 characters, one line.
bio summary Up to 160 characters, line breaks kept. "" clears it.
location Up to 30 characters, one line. "" clears it.
website custom_link: { url, type: "WEBSITE" } One https address, up to 100 characters, a host with a dot, no spaces or credentials. It can't be emptied.
avatar picture A JPEG or PNG of at most 2 MB.
banner cover_picture A JPEG or PNG of at most 5 MB (the header photo).
username The new handle: 4 to 15 letters, digits or _, with or without @.
dry_run true reads everything and presses nothing.

Characters are counted as X counts them (an emoji is 2). account_id goes in the body. A field the profile doesn't have (headline, experience…) or another type than X answers 501 errors/feature_not_implemented, with nothing changed; dry_run and every field are checked before the browser opens (400 errors/invalid_parameters).

Pictures go as files in a multipart/form-data body, as Unipile sends them:

Shell
curl -sS -X PATCH "https://api.lempile.com/users/me/edit" \
  -H "X-API-KEY: $LEMPILE_API_KEY" \
  -H "Idempotency-Key: rebrand-janedemo-1" \
  -F account_id=janedemo -F type=X \
  -F name="LAUNCH WIRE" -F bio="Every launch worth knowing, as it ships." \
  -F location="" -F website="https://launchwire.example" \
  -F avatar=@avatar.png -F banner=@header.jpg -F username=launchwire

or in JSON, each picture as { name, content_type, data_base64 } (the file's bytes in base64, no data: prefix). Either way, the type is read from the file's own bytes: a GIF or WebP answers 400.

JSON
{ "account_id": "janedemo", "name": "LAUNCH WIRE", "username": "@launchwire",
  "avatar": { "name": "avatar.png", "content_type": "image/png", "data_base64": "iVBORw0KGgo…" } }

The browser reads X's Edit profile, gives it the pictures (through X's own crop window, as is), types each field that isn't already as asked, checks the others unchanged, presses Save, and reads everything back on a fresh window. The handle comes last, on X's Change username page: typed, X's own check that it's free waited for, saved, read back. The answer says what X keeps:

JSON
{ "object": "ProfileEdited", "provider": "X", "account_id": "11111111-1111-4111-8111-111111111111",
  "changed": { "name": "LAUNCH WIRE", "bio": "Every launch worth knowing, as it ships.",
    "avatar": "https://pbs.twimg.com/profile_images/2/new.jpg", "username": "launchwire" },
  "previous": { "name": "Jane Demo", "bio": "Building in public.",
    "avatar": "https://pbs.twimg.com/profile_images/1/old.jpg", "username": "janedemo" },
  "already": ["location", "website"], "avatar_changed": true, "banner_changed": true, "username": "launchwire" }
  • already lists the fields that were already as asked: nothing was typed for them. Pictures are always given again (X can't tell they're the same).
  • avatar_changed / banner_changed compare the addresses X serves the pictures from before and after Save (null when unreadable).
  • A dry run answers ProfileEditDryRun with current, would_change, already and checks (save_button, photo_inputs, username_field, password_required).
  • After a new handle, Lempile's own record of the account follows in the same request (GET /accounts shows it). The old handle no longer works as account_id: use the Lempile id.
  • On a verified account, a new name, avatar or handle removes the checkmark until X reviews the account again. X asks at Save, and Lempile confirms that notice: the edit is what you asked. Any other sheet X shows is left alone, and the answer quotes it (X said: …).

website alone is the Website edit, as before: its answer is { "object": "ProfileEdited", "provider": "X", "website": "…", "previous_website": "…", "already": false }, and a dry run's { "object": "ProfileEditDryRun", "website": "…", "would": "set" | "already_set" }.

Time. A website or a few words take under a minute. Pictures and a handle can take a few minutes: the request waits 140 seconds, then answers 504 errors/timeout while the browser goes on (up to 10 minutes). Send an Idempotency-Key and read GET /writes/{key} until it's done or failed; without one, read the profile (GET /users/me) before trying again.

HTTP Type Meaning
400 errors/invalid_parameters, errors/invalid_* A field X wouldn't take. Nothing was changed.
403 errors/password_required X asks for the account's password to change the handle. Lempile never types it: the handle wasn't changed.
409 errors/username_taken X says the handle isn't free. The handle wasn't changed.
422 errors/username_field_not_found X's Change username page shows no Username field, as while an X Premium check is under review.
422 errors/username_refused, errors/username_check_failed, errors/username_not_kept X refused the handle, never said, or kept the old one.
422 errors/profile_photo_input_not_found, errors/profile_photo_not_taken X's file picker or crop window didn't show. Nothing was saved.
422 errors/profile_field_not_found, errors/profile_fields_changed, errors/profile_save_unavailable, errors/typing_failed The Edit profile window wasn't as expected. Nothing was saved.
422 errors/profile_value_not_kept Saved, but X keeps another value (detail says which, and what X said).
502 errors/profile_unconfirmed, errors/username_unconfirmed Saved, but not read back: not sure. Read the profile before trying again.
504 errors/timeout Still running, or not sure: read the receipt.

A refusal after the Edit profile part was saved (the handle failed) carries done: what X keeps from that part.

Read The Profile#

GET /users/me?account_id= (accounts.read) on an X account: the profile as X's Edit profile window and Change username page show it. The browser reads them and presses nothing (it's a dry run of the edit).

JSON
{ "object": "AccountOwnerProfile", "provider": "X", "account_id": "11111111-1111-4111-8111-111111111111",
  "name": "LAUNCH WIRE", "username": "launchwire", "public_identifier": "launchwire",
  "public_profile_url": "https://x.com/launchwire", "bio": "Every launch worth knowing, as it ships.",
  "location": null, "website": "https://launchwire.example",
  "profile_picture_url": "https://pbs.twimg.com/profile_images/2/new.jpg",
  "cover_picture_url": "https://pbs.twimg.com/profile_banners/9/1700000000",
  "can_change_username": true }

Empty fields are null. can_change_username is false when X's Change username page shows no Username field or asks for the password. When X shows another handle than Lempile has on record, the record follows.

Daily Analytics#

GET /accounts/{account_id}/analytics?from=YYYY-MM-DD&to=YYYY-MM-DD (accounts.read): the account's own analytics by UTC day, read from its analytics page (x.com/i/account_analytics, which needs X Premium). Complete days only: to is capped at yesterday (UTC); at most 365 days.

JSON
{
  "account_id": "11111111-1111-4111-8111-111111111111", "object": "AccountAnalytics", "provider": "X",
  "from": "2026-11-01", "to": "2026-11-01", "read_at": "2026-11-07T08:00:00Z",
  "days": [{ "date": "2026-11-01", "impressions": 1520, "likes": 34, "replies": 6, "reposts": 2, "quotes": 0,
    "bookmarks": 3, "profile_visits": 41, "posts": 3, "replies_sent": 12, "quotes_sent": 0,
    "new_follows": 4, "unfollows": 1, "engagements": null, "engagement_rate": null, "shares": null }],
  "totals": { "impressions": 1520, "...": "the same keys, added up" },
  "followers": { "total": 812, "verified": 40, "active": 300 }
}
  • Each count is X's own daily series (impressions is X's "Displayed"; posts, replies_sent and quotes_sent are what the account wrote). A type with no entry on a day is 0; new_follows and unfollows are null on a day X's follow chart doesn't list.
  • engagements, engagement_rate and shares are always null: X has them by the hour only.
  • 422 errors/analytics_unavailable: X doesn't show this account analytics yet (for instance under its follower floor). 502 errors/analytics_not_loaded: no Premium, or the page didn't finish. Never an empty answer.

Was this page helpful?