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

# ID Conversion

# ID Conversion

Convert between X (formerly Twitter) usernames, numeric user IDs, and profile URLs. Three lightweight utility endpoints, one request each. Every new account includes 100 free requests (no card required), so you can start converting right away.

> **Note:** For a fuller explainer on usernames, user IDs, and profile links, see [Twitter ID Converter: Username, User ID, and Profile Link](https://api.sorsa.io/blog/twitter-id-converter) on the blog.

> **No-code option:** for one-off conversions, use the free [Sorsa ID Converter](https://api.sorsa.io/playground/id-converter) web tool. Paste a username, ID, or profile URL and get the result instantly, no API key required.

***

## Why User IDs Matter

A username (handle) can be changed at any time, and once released, can be claimed by someone else. A numeric user ID is assigned at account creation and never changes. If you are building anything that stores or references X accounts, work with user IDs:

* **Handle changes do not break your system.** The ID points to the same account regardless of how many times the user renames.
* **Indexes are faster.** Integer keys outperform variable-length string keys.
* **Cross-time joins are clean.** When matching accounts across datasets collected at different times, IDs are the only safe key.
* **Some endpoints are ID-based.** `/info-batch` takes a `user_ids` array (it also accepts `usernames`), and list and community endpoints reference the target list or community by its numeric ID.

## How User IDs Are Formatted

X uses Snowflake IDs: 64-bit integers that pack a timestamp, a machine ID, and a sequence number into a single value. Tweet IDs have used this format since 2010.

User IDs are different. X kept assigning sequential integer IDs to accounts for years after Snowflake launched, and only migrated user IDs to the Snowflake format around 2020. This is why older accounts have short IDs (Jack Dorsey's account is `12`) while accounts created after 2020 have 19-digit IDs. The practical consequence: you cannot decode a creation timestamp from older user IDs. If you need a registration date for an older account, fetch the profile and read `created_at`.

***

## Endpoint 1: Username to User ID

```http theme={null}
GET /v3/username-to-id/{user_handle}
```

Converts a handle (without `@`) into the permanent numeric user ID.

```bash theme={null}
curl "https://api.sorsa.io/v3/username-to-id/elonmusk" \
  -H "ApiKey: YOUR_API_KEY"
```

```json theme={null}
{"id": "44196397"}
```

```python theme={null}
import requests

API_KEY = "YOUR_API_KEY"

def username_to_id(handle: str) -> str:
    resp = requests.get(
        f"https://api.sorsa.io/v3/username-to-id/{handle}",
        headers={"ApiKey": API_KEY},
    )
    resp.raise_for_status()
    return resp.json()["id"]

print(username_to_id("elonmusk"))  # "44196397"
```

```javascript theme={null}
async function usernameToId(handle) {
  const resp = await fetch(
    `https://api.sorsa.io/v3/username-to-id/${handle}`,
    { headers: { "ApiKey": "YOUR_API_KEY" } }
  );
  return (await resp.json()).id;
}
```

## Endpoint 2: User ID to Username

```http theme={null}
GET /v3/id-to-username/{user_id}
```

Resolves a numeric user ID back to the account's current handle. Useful for making stored IDs human-readable, or for detecting that an account has been renamed since you last looked.

```bash theme={null}
curl "https://api.sorsa.io/v3/id-to-username/44196397" \
  -H "ApiKey: YOUR_API_KEY"
```

```json theme={null}
{"handle": "elonmusk"}
```

```python theme={null}
def id_to_username(user_id: str) -> str:
    resp = requests.get(
        f"https://api.sorsa.io/v3/id-to-username/{user_id}",
        headers={"ApiKey": API_KEY},
    )
    resp.raise_for_status()
    return resp.json()["handle"]
```

## Endpoint 3: Profile Link to User ID

```http theme={null}
GET /v3/link-to-id?link={profile_url}
```

Extracts the permanent user ID from a full profile URL. Useful when processing links from spreadsheets, bookmarks, or scraped pages and you need to normalize them into IDs.

```bash theme={null}
curl "https://api.sorsa.io/v3/link-to-id?link=https://x.com/elonmusk" \
  -H "ApiKey: YOUR_API_KEY"
```

```json theme={null}
{"id": "44196397"}
```

```python theme={null}
def link_to_id(profile_url: str) -> str:
    resp = requests.get(
        "https://api.sorsa.io/v3/link-to-id",
        headers={"ApiKey": API_KEY},
        params={"link": profile_url},
    )
    resp.raise_for_status()
    return resp.json()["id"]
```

> **Tip:** if you need both the user ID and the full profile, skip the conversion step and call [`/info`](https://docs.sorsa.io/api-reference/users-data/user-profile) with the `username` parameter. It returns the complete profile (including `id`) in a single request. See [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) for more patterns like this.

***

## Common Patterns

### Batch Conversion

When you have a list of handles (CRM export, competitor list, spreadsheet) and need to convert them all to IDs:

```python theme={null}
import requests
import time

API_KEY = "YOUR_API_KEY"
BASE = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": API_KEY}


def batch_username_to_id(handles, pause=0.05):
    """
    Resolve a list of handles to user IDs.
    Returns: dict mapping handle -> id (or None if lookup failed).
    """
    results = {}
    for handle in handles:
        handle = handle.strip().lstrip("@")
        try:
            resp = requests.get(f"{BASE}/username-to-id/{handle}", headers=HEADERS, timeout=10)
            if resp.status_code == 200:
                results[handle] = resp.json()["id"]
            elif resp.status_code == 404:
                results[handle] = None  # Account does not exist or is suspended
            elif resp.status_code == 429:
                time.sleep(1)
                retry = requests.get(f"{BASE}/username-to-id/{handle}", headers=HEADERS, timeout=10)
                results[handle] = retry.json()["id"] if retry.status_code == 200 else None
            else:
                results[handle] = None
        except requests.RequestException:
            results[handle] = None
        time.sleep(pause)
    return results


handles = ["NASA", "SpaceX", "Tesla", "OpenAI", "stripe"]
id_map = batch_username_to_id(handles)

for handle, uid in id_map.items():
    print(f"@{handle} -> {uid or '(not found)'}")
```

The reverse direction works the same way: swap the URL for `/id-to-username/{user_id}` and read the `handle` field from the response. This is useful for refreshing a database that may have stale display names.

### Normalizing Mixed Input

When users submit account references in different formats (some as handles, some as URLs, some as IDs), normalize everything to user IDs. The check below skips the API call entirely when the input is already an ID:

```python theme={null}
def normalize_to_id(value: str) -> str:
    """
    Accepts a handle, an @handle, a profile URL, or a numeric ID.
    Returns the numeric user ID.
    """
    value = value.strip().lstrip("@")

    if value.isdigit():
        return value

    if "x.com/" in value or "twitter.com/" in value:
        return link_to_id(value)

    return username_to_id(value)


# All four return the same ID
for source in ["elonmusk", "@elonmusk", "https://x.com/elonmusk", "44196397"]:
    print(normalize_to_id(source))
```

Use this as the first step in any ingestion pipeline so downstream logic always works with the stable identifier.

### Detecting Handle Changes

If you stored both the user ID and the handle at collection time, you can periodically re-resolve the IDs and detect which accounts have renamed:

```python theme={null}
def detect_renames(records):
    """
    records: list of {"user_id": str, "stored_handle": str}
    Returns: list of accounts that have renamed.
    """
    changes = []
    for record in records:
        try:
            current = id_to_username(record["user_id"])
        except requests.HTTPError:
            continue  # Deleted, suspended, or transient error

        if current and current.lower() != record["stored_handle"].lower():
            changes.append({
                "user_id": record["user_id"],
                "old_handle": record["stored_handle"],
                "new_handle": current,
            })
        time.sleep(0.05)
    return changes
```

For a richer audit, the [`/about`](https://docs.sorsa.io/api-reference/users-data/account-about-info) endpoint returns `username_change_count` and `last_username_change_at`. That tells you not just the current handle but how many times the account has renamed and when the most recent change happened.

***

## Next Steps

* [Followers & Following](https://docs.sorsa.io/followers-and-following): most follower-extraction workflows start with ID conversion.
* [Audience Geography](https://docs.sorsa.io/Audience-Geography): the `/about` endpoint accepts `user_id` and returns country data plus username change history.
* [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage): avoid unnecessary conversion calls by using `/info` directly when you need the full profile.
* [API Reference](https://docs.sorsa.io/api-reference-guide): full specification for `/username-to-id`, `/id-to-username`, `/link-to-id`, and every other Sorsa endpoint.
