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:
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=launchwireor 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.
{ "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:
{ "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" }alreadylists 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_changedcompare the addresses X serves the pictures from before and after Save (nullwhen unreadable).- A dry run answers
ProfileEditDryRunwithcurrent,would_change,alreadyandchecks(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 /accountsshows it). The old handle no longer works asaccount_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).
{ "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.
{
"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 (
impressionsis X's "Displayed";posts,replies_sentandquotes_sentare what the account wrote). A type with no entry on a day is0;new_followsandunfollowsarenullon a day X's follow chart doesn't list. engagements,engagement_rateandsharesare alwaysnull: 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?