Tokens API
Every proxy token: list and read, usage for one token or many, and for pay-per-GB proxies create, change, reissue credentials, and delete. Unlimited tokens that can move also have location, rotation, and a new IP on request.
List and read#
GET /api/v1/tokens
Authorization: Bearer lit_...
| Parameter | Type | Rules |
|---|---|---|
limit |
integer | Integer 1-100; default 50. |
cursor |
string | The previous page's page |
type |
string | unlimited or ppg. |
status |
string | active, expired, or disabled. |
packageId |
integer | An owned package's public numeric ID. An unknown or foreign value returns an empty list. A malformed value is INVALID |
Results are ordered newest first. Keep requesting the returned page.nextCursor until it is null.
GET /api/v1/tokens/{tokenId}
Authorization: Bearer lit_...
Returns one token in the identical shape as a GET /tokens list element, including its username and password. An unknown, unowned, or unparseable tokenId is 404 TOKEN_NOT_FOUND; the response never discloses another user's token.
Examples#
An unlimited datacenter token, on a package with allowed IPs and a movable location:
{
"id": 12345,
"type": "unlimited",
"status": "active",
"disabledReason": null,
"label": "Production crawler",
"externalRef": null,
"createdAt": "2026-08-01T10:15:00.000Z",
"expiresAt": "2026-12-01T00:00:00.000Z",
"allowanceBytes": null,
"allowanceReservedBytes": null,
"ingress": {
"hub": "us-11",
"protocol": "http",
"mode": "assigned",
"host": "hub-us-11-1.litport.net",
"port": 31337,
"status": "available"
},
"packageId": 45,
"ipAuth": { "host": "hub-us-11-1.litport.net", "port": 31338 },
"location": { "country": "us", "region": "new-york", "city": "new-york-city" },
"rotation": { "intervalSec": 600, "options": [600, 1800, 3600], "changeable": true },
"username": "proxy-username",
"password": "proxy-password"
}
A pay-per-GB token, locked to one pool, with a reference, an expiry, and a traffic cap:
{
"id": 12399,
"type": "ppg",
"status": "active",
"disabledReason": null,
"label": "Residential crawler",
"externalRef": "customer-1042",
"createdAt": "2026-08-01T10:15:00.000Z",
"expiresAt": "2026-12-31T23:59:59.999Z",
"allowanceBytes": 5000000000,
"allowanceReservedBytes": 1200000000,
"ingress": {
"hub": "example-hub",
"protocol": "http",
"mode": "request-selected",
"supportedProtocols": ["http", "https", "socks5"]
},
"poolSelection": {
"mode": "fixed",
"pool": { "authKey": "residential-main", "name": "Residential Main" }
},
"packageId": null,
"ipAuth": null,
"username": "proxy-username",
"password": "proxy-password"
}
Example identifiers and hubs in this documentation are illustrative.
Field semantics#
type is ppg or unlimited. status is active, expired, or disabled. It is never hub-unavailable; that value belongs to ingress.status, below.
disabledReason says why a token is not serving traffic. It is null while status is active.
status |
disabledReason |
Meaning |
|---|---|---|
disabled |
owner |
Switched off, by you (enabled: false) or, for a package's token, by its package. |
disabled |
ban |
Switched off because the account is blocked. |
expired |
expired |
Past expiresAt. |
disabled |
allowance |
Switched on, but all of its traffic cap is used. Raise or remove the cap to start it again. |
disabled |
balance |
Switched on, but the account balance can't pay for it. Top up to use it again. |
The status filter on GET /tokens selects by the token's own on/off switch and its expiry only. It does not find a token that is disabled for allowance or balance, so read disabledReason on the results.
externalRef is your own reference for a pay-per-GB proxy, or null. expiresAt is when the token stops, or null. allowanceBytes is the traffic cap in bytes, or null for no cap, and allowanceReservedBytes is how much of it has been set aside so far, or null without a cap.
For a PPG token, ingress.mode is request-selected: there is no single host or port, because the hub, protocol and pool are chosen per request or export. ingress.hub and ingress.protocol are dashboard/export preferences, and ingress.supportedProtocols lists the choices. Take the host from a pool's hubs and the port from its httpPort/socks5Port in Pay-per-GB.
For an unlimited token, ingress.mode is assigned: the token resolves to exactly one endpoint, always reported as ingress.host and ingress.port. ingress.status is available or hub-unavailable. hub-unavailable means the token names a hub that is no longer in service: ingress.host and ingress.port are then null, and the credentials cannot be used until support moves the token to a current hub.
packageId is the public numeric package ID when the token belongs to an owned package, otherwise null; read the package with Packages. ipAuth is {host, port}, the token's passwordless endpoint, or null when the token has none right now. It accepts connections only from the package's allowed IPs.
location is present only on a token whose location can move; Location lists which tokens qualify. Its shape depends on kind: a datacenter or ISP token reports {country, region, city}; a mobile token reports {country, city, carrier}.
rotation is {intervalSec, options, changeable}, present on every token that rotates on a timer. See Rotation.
rotateUrl is the Change-IP link of a Private Device or a rotating datacenter/ISP token, present only when the token has one. Keep it as private as the password next to it. See Change-IP link below, and Change IP for the ways to get a new IP.
username and password are the proxy credentials for the token. Use them with the endpoint shown in ingress or the dashboard.
If the account has turned on Use neutral addresses, the hostnames this API returns (ingress.host, ipAuth.host), the pool authKey, and the rotateUrl link carry the neutral names. A pool name you send is accepted in either form. For an HTTPS proxy use ingress.httpsHost (an unlimited token on the http protocol): the certificate is issued for that name. It is the hub's own name, and equals ingress.host unless the account uses neutral addresses. For a pay-per-GB token take httpsHostname from the pool's hubs.
Create and manage proxies#
Pay-per-GB proxies can be created, changed, and deleted through the API, and the credentials of any proxy you own can be reissued. The dashboard's pay-per-GB proxy form follows the same rules. Every call here needs a Bearer API key, and writes count toward the key's limit of 120 requests per 60 seconds.
A package's proxies belong to their package, which creates and ends them and switches them on and off. For those you can change the label and reissue the credentials, nothing else.
Reselling is allowed on every account. You answer for how your customers use their proxies: abuse by one of them can block your whole account.
Create a proxy#
POST /api/v1/tokens
Authorization: Bearer lit_...
Content-Type: application/json
Send a JSON object, {} at the least. A request with no body creates nothing.
| Field | Rules |
|---|---|
pool |
Optional. A pool's authKey from Pay-per-GB. Omit it, or send null, for a proxy that picks its pool per request. A pool you can't select is 400 INVALID_POOL. |
label |
Optional. Up to 255 characters. |
externalRef |
Optional, and worth always sending. 1 to 128 printable characters, unique among your live proxies. |
expiresAt |
Optional. An ISO 8601 date-time with a zone, such as 2026-12-31T10:, or a date, such as 2026-12-31, which means the end of that day in UTC. It must be after now. The proxy stops then. |
allowance |
Optional. The traffic cap in bytes, an integer of at least 1. |
curl -X POST 'https://litport.net/api/v1/tokens' \
-H "Authorization: Bearer $LITPORT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"pool": "residential-main", "label": "Residential crawler", "externalRef": "customer-1042", "expiresAt": "2026-12-31", "allowanceBytes": 5000000000}'
The answer is 201 and the proxy in the shape shown under Examples, with its username and password. Build the proxy username from them as described in Pay-per-GB proxy parameters.
Always send an externalRef. It is your own reference, such as a customer ID or an order number. A create that repeats an externalRef you already use answers 200 with that proxy and changes nothing, even if the rest of the body differs. So if a create times out, or you lose the answer, send it again with the same externalRef and you get the proxy that exists. Without an externalRef, a repeat can create a second proxy. A deleted proxy gives its externalRef back, so you can use it again.
Creating needs a positive balance (INSUFFICIENT_BALANCE) and an account that isn't blocked (ACCOUNT_BANNED). An account can hold a limited number of pay-per-GB proxies (TOKEN_LIMIT_REACHED); contact support to raise it.
An account can create 60 new proxies per minute, counting all its API keys and the dashboard together. A repeat of an externalRef you already use doesn't count. Past the limit, a create answers 429 RATE_LIMITED with Retry-After, and nothing is created: send the same request again after the wait.
Change a proxy#
PATCH /api/v1/tokens/{tokenId}
Authorization: Bearer lit_...
Content-Type: application/json
Send only the fields you want to change, and at least one. A field you leave out stays as it is. null clears label, pool, expiresAt, and allowanceBytes.
| Field | Rules |
|---|---|
label |
Up to 255 characters, or null. |
enabled |
false switches the proxy off, true switches it on. Switching on needs a positive balance and an account that isn't blocked. |
pool |
A pool's authKey, or null for a proxy that picks its pool per request. A proxy that signs in by allowed IP keeps its pool: null is 409 POOL. |
expiresAt |
A date-time or date after now, as for create, or null for no expiry. |
allowance |
The traffic cap in bytes, an integer of at least 1, or null to remove it. |
The answer is 200 and the proxy as it is now. A body that changes nothing writes nothing. A new pool applies to new connections; open connections keep the pool they started on and are charged at its price.
Switching off, relabelling, and changing the expiry or the cap always work, even with an empty balance, so you can cut a customer off at any time. So do reissuing credentials and deleting. For a package's proxy, label is the only field; any other is 400 FIELD_NOT_ALLOWED.
Reissue credentials#
POST /api/v1/tokens/{tokenId}/credentials
Authorization: Bearer lit_...
Content-Type: application/json
{"requestId": "reissue-2026-11-04-a"}
Issues a new username, password, and change-IP key for any proxy you own, pay-per-GB or package. The answer is 200 and the proxy in the shape shown under Examples, with the new username and password, and a new rotateUrl where the proxy has one. The body is optional.
- The old password stops working, and its open connections close.
- The old change-IP link stops working.
- A package's allowed IPs keep working, and their connections stay open. Allowed IPs belong to the whole package, not to one proxy, so reissuing one proxy can't cut off a customer who connects from an allowed IP. Share a package between customers with passwords only.
requestId is 1 to 64 characters: letters, digits, underscore, or hyphen. Use a new requestId for each reissue you mean. Send the same one only to retry a call you got no answer to: a repeat returns the credentials the first call issued and issues nothing new, whether the first call has finished or is still running. Without a requestId, every call issues a new pair, so don't repeat it blindly.
Delete a proxy#
DELETE /api/v1/tokens/{tokenId}
Authorization: Bearer lit_...
Deletes a pay-per-GB proxy. The answer is 204 with no body. The proxy stops working and its open connections close. Its history stays: usage for many tokens still lists it, marked deleted, and GET /tokens/{tokenId}/usage still answers for it. GET /tokens and GET /tokens/{tokenId} no longer return it. Deleting a proxy that is already deleted is 204 again.
A package's proxy can't be deleted: 409 NOT_SUPPORTED. It ends with its package.
Traffic cap#
A pay-per-GB proxy can have a traffic cap, allowanceBytes: the most traffic it may use, in bytes (1 GB is 1,000,000,000 bytes). Use it to give each customer, team member, or tool a fixed amount. A cap is optional. In the dashboard it is the Traffic limit (GB) field of the proxy.
- What counts. All traffic between the client and the hub, in both directions, protocol overhead included. So the cap is never less than the traffic charged in usage: a proxy never gets more charged traffic than its cap.
- Where it stops. A proxy can stop a little short of its cap, but never goes over it. Litport sets traffic aside for a proxy ahead of use, and what goes unused isn't given back.
allowanceReservedBytes. How much of the cap has been set aside so far. It is not the same number as charged usage: it can run ahead of the traffic delivered, and it is never taken off what you are charged. Read charged traffic from usage.- Billing doesn't change. The cap only refuses traffic.
- When it is used up.
statusisdisabled,disabledReasonisallowance, and new connections get proxy error 24 (HTTP403). Raise or remove the cap to use the proxy again. - Changing a cap. Adding, raising, lowering, or removing a cap ends the proxy's open connections once; clients reconnect under the new cap. A cap lowered below what has already been set aside stops the proxy as soon as the traffic already set aside is used. Nothing is taken back.
Proxy write errors#
| Status | Code | Meaning |
|---|---|---|
400 |
INVALID |
The body isn't a JSON object, has a field the call doesn't take, or (PATCH) has no field at all. |
400 |
FIELD |
PATCH sent a package's proxy a field other than label. |
400 |
INVALID, INVALID, INVALID, INVALID, INVALID, INVALID |
The named field is out of range or the wrong type. |
400 |
INVALID_POOL |
pool isn't a pool you can select. |
402 |
INSUFFICIENT |
Creating or switching on needs a positive balance. |
403 |
ACCOUNT |
Creating or switching on isn't allowed while the account is blocked. |
404 |
TOKEN |
Unknown, unowned, or malformed tokenId. |
409 |
NOT |
DELETE on a package's proxy. |
409 |
TOKEN |
The account has the most pay-per-GB proxies it may hold. |
409 |
ALLOWANCE |
The cap couldn't be saved. Send the same request again. |
409 |
POOL |
A proxy that signs in by allowed IP can't drop its pool. |
409 |
TOKEN |
The proxy can no longer be edited, for example because it was deleted while the write ran. Nothing changed. |
409 |
CONCURRENT |
Another write to this proxy landed at the same moment. Send the same request again. |
429 |
RATE_LIMITED |
Creating: more than the account's limit of new proxies in the last minute (or the key's request limit). Nothing was created. Wait for Retry-After, then send the same request again. |
Every code has a heading on API errors.
Usage#
Read customer-visible traffic for one owned token.
GET /api/v1/tokens/{tokenId}/usage
Authorization: Bearer lit_...
Query parameters#
| Parameter | Rules |
|---|---|
from |
Inclusive canonical UTC ISO-8601 timestamp. |
to |
Exclusive canonical UTC ISO-8601 timestamp; must be after from. |
groupBy |
hour, day, or month; default day. |
include |
Comma-separated domains and/or charges. charges requires groupBy=hour. |
When the range is omitted, the API uses the 30 complete UTC days before the current UTC day. Hour ranges are limited to 31 days, day ranges to 366 days, and month ranges to 36 months.
Example#
curl --get 'https://litport.net/api/v1/tokens/12345/usage' \
-H "Authorization: Bearer $LITPORT_API_KEY" \
--data-urlencode 'from=2026-08-01T00:00:00Z' \
--data-urlencode 'to=2026-08-03T00:00:00Z' \
--data-urlencode 'groupBy=day'
{
"data": {
"tokenId": 12345,
"deleted": false,
"range": {
"from": "2026-08-01T00:00:00.000Z",
"to": "2026-08-03T00:00:00.000Z",
"groupBy": "day",
"timezone": "UTC"
},
"trafficBasis": "customer_chargeable",
"summary": {
"requests": 42,
"uploadedBytes": "12000",
"downloadedBytes": "88000",
"totalBytes": "100000",
"cache": {
"hits": 12,
"misses": 4,
"bypasses": 2,
"hitRate": 0.75,
"freeUploadedBytes": "0",
"freeDownloadedBytes": "24000",
"freeTotalBytes": "24000",
"savedCostUnits": "0.0324"
}
},
"buckets": [
{
"start": "2026-08-01T00:00:00.000Z",
"end": "2026-08-02T00:00:00.000Z",
"requests": 42,
"uploadedBytes": "12000",
"downloadedBytes": "88000",
"totalBytes": "100000",
"cache": {
"hits": 12,
"misses": 4,
"bypasses": 2,
"hitRate": 0.75,
"freeUploadedBytes": "0",
"freeDownloadedBytes": "24000",
"freeTotalBytes": "24000",
"savedCostUnits": "0.0324"
}
}
]
}
}
Usage semantics#
The interval is half-open: [from, to). Buckets are ordered by start time and are sparse; a missing bucket means no stored aggregate row, not an API error.
For PPG tokens, trafficBasis is customer_chargeable, so the traffic totals match the traffic shown to the customer. For fixed-price/unlimited tokens, it is observed; those existing traffic totals remain the observed transfer basis. Large byte totals are decimal strings so clients can preserve exact values. requests is a JSON integer.
Cache benefit#
Every bucket and the summary contain cache. hits, misses, and bypasses are JSON integers; hitRate is hits / (hits + misses) and is 0 when there were no hits or misses. freeUploadedBytes, freeDownloadedBytes, and freeTotalBytes are non-negative base-10 decimal strings, with freeTotalBytes equal to the two directional values added together. Legacy periods without cache telemetry report zeroes for these fields.
For PPG tokens only, cache.savedCostUnits is a non-negative base-10 decimal string for the audited charge avoided at the price effective when the traffic occurred. The field is deliberately omitted, not set to 0 or null, for unlimited tokens because no per-GB customer charge was avoided.
The token must belong to the authenticated user. An unknown or unowned token returns the same 404 TOKEN_NOT_FOUND response. A deleted token still answers, with deleted: true, so a customer's last bill can be read after the proxy is gone.
Charge breakdown#
For a PPG token, request include=charges&groupBy=hour to add the persisted charge to each hourly bucket. The expansion is rejected for unlimited tokens. A versioned charge contains the pool's public key and name, raw and charged upload/download bytes, contributing hubs and protocols, snapshotted base/effective rates and discount, and the exact amount deducted. A row without snapshots is labeled Legacy PPG usage; the API does not guess its pool or rate from current token settings.
Charge components are the billing record. Upload and download are both charged. A reserved charge can grow while late traffic for the hour arrives; a finished charge is final.
Domain breakdown#
Request include=domains to add domainBreakdown to data:
{
"domainBreakdown": {
"limit": 150,
"totalDestinationCount": 151,
"displayedCount": 150,
"truncated": true,
"range": {
"from": "2026-08-01T00:00:00.000Z",
"to": "2026-08-03T00:00:00.000Z",
"timezone": "UTC",
"periodClamped": false,
"endClamped": false
},
"items": [{
"protocol": "tcp",
"destination": "example.com",
"port": 443,
"requests": 42,
"uploadedBytes": "12000",
"downloadedBytes": "88000",
"totalBytes": "100000",
"lastSeenAt": "2026-08-01T12:00:00.000Z",
"firstSeenAt": "2026-08-01T00:00:00.000Z"
}],
"other": {
"protocol": null,
"destination": "Other",
"port": null,
"requests": 1,
"uploadedBytes": "1",
"downloadedBytes": "2",
"totalBytes": "3",
"lastSeenAt": null,
"firstSeenAt": null
}
}
}
The response includes at most the 150 destinations with the highest total traffic. Every remaining destination and telemetry overflow is combined into other; truncated reports when that grouping occurred. TCP requests are successful request counts and UDP requests are inbound datagrams. Domain data is stored as whole-hour UTC buckets and is limited to the most recent 31 days of the requested interval. The returned UTC range is the effective bucket interval: a past partial start is rounded down to its hour and a past partial end is rounded up to its next hour. An end clamped to the current time remains at that exact time, so the response does not claim coverage past the current time. The range also reports periodClamped or endClamped when adjusted.
Usage for many tokens#
Read the usage of many proxies in one call, a day at a time. Deleted proxies are included, so you can settle the last bill of a customer who has left.
GET /api/v1/usage
Authorization: Bearer lit_...
Usage list parameters#
| Parameter | Rules |
|---|---|
from |
Inclusive canonical UTC ISO-8601 timestamp. |
to |
Exclusive canonical UTC ISO-8601 timestamp; must be after from. |
tokenIds |
Proxy IDs, comma-separated or as a repeated parameter, at most 100. Omit it for every proxy. An ID that isn't yours is skipped. A malformed list is 400 INVALID. |
limit |
Integer 1-100; default 50. The number of proxies in a page. |
cursor |
The previous page's page, sent back unchanged. |
When the range is omitted, the API uses the 30 complete UTC days before the current UTC day. A range is limited to 92 days (400 INVALID_TIME_RANGE beyond it); read a longer period in several calls. This call is limited to 10 requests per 60 seconds per API key (429 RATE_LIMITED), and RateLimit-* and Retry-After report that limit.
Usage list example#
curl --get 'https://litport.net/api/v1/usage' \
-H "Authorization: Bearer $LITPORT_API_KEY" \
--data-urlencode 'from=2026-10-01T00:00:00Z' \
--data-urlencode 'to=2026-10-03T00:00:00Z' \
--data-urlencode 'tokenIds=12399,12400'
{
"data": {
"range": {
"from": "2026-10-01T00:00:00.000Z",
"to": "2026-10-03T00:00:00.000Z",
"groupBy": "day",
"timezone": "UTC"
},
"tokens": [
{
"tokenId": 12399,
"type": "ppg",
"deleted": false,
"label": "Residential crawler",
"externalRef": "customer-1042",
"trafficBasis": "customer_chargeable",
"summary": {
"requests": 42,
"uploadedBytes": "12000",
"downloadedBytes": "88000",
"totalBytes": "100000",
"costUnits": "0.00045"
},
"buckets": [
{
"start": "2026-10-01T00:00:00.000Z",
"end": "2026-10-02T00:00:00.000Z",
"requests": 42,
"uploadedBytes": "12000",
"downloadedBytes": "88000",
"totalBytes": "100000",
"costUnits": "0.00045"
}
]
},
{
"tokenId": 12400,
"type": "ppg",
"deleted": true,
"label": null,
"externalRef": "customer-0977",
"trafficBasis": "customer_chargeable",
"summary": {
"requests": 0,
"uploadedBytes": "0",
"downloadedBytes": "0",
"totalBytes": "0",
"costUnits": "0"
},
"buckets": []
}
]
},
"page": { "nextCursor": null }
}
Usage list semantics#
A page holds up to limit proxies, newest first, each with a summary for the range and one bucket per UTC day that has traffic. The interval is half-open, [from, to), and buckets are sparse, as for one token. Keep requesting page.nextCursor until it is null; a page can hold fewer than limit proxies, because deleted proxies with no usage are left out.
Credentials never appear here. trafficBasis, the byte strings, and requests mean what they mean for one token. There is no cache benefit, domain breakdown, or hourly charge in this list; read those with GET /tokens/{tokenId}/usage.
costUnits is the amount charged, in balance units, as a decimal string. A pay-per-GB proxy carries it on each day and in its summary; a bucket's costUnits is null for a day with traffic but no recorded charge yet, and a charge for the hour still running can change. It is null for a package's proxy, which isn't charged per GB.
Deleted proxies are listed with deleted: true and the externalRef they had. A deleted proxy with no usage in the range appears only when you name it in tokenIds. A live pay-per-GB proxy always appears. A package's proxy appears unless it expired before from.
Location#
A datacenter or ISP token, and a mobile token whose city or carrier can change, report a location field and support this API. Which tokens qualify, and how a change behaves:
| Token | Changes | Wait |
|---|---|---|
| Datacenter |
Any time | None |
| Datacenter |
A new IP in the chosen location | Once a week |
| Datacenter |
Move back to the saved location | None |
| Multi-location mobile (paid add-on) | Any time | None |
| Standard US Shared Mobile | City and carrier together | Once per 48 hours |
| Everything else (PPG, Private Device, non-US mobile, a day plan, or a legacy token with no port type) | Not supported | None |
GET /api/v1/tokens/{tokenId}/location
Authorization: Bearer lit_...
{
"data": {
"tokenId": 12345,
"kind": "datacenter",
"mode": "rotating",
"target": { "country": "us", "region": "new-york", "city": "new-york-city" },
"effective": {
"country": "us", "region": "new-york", "city": "new-york-city",
"labels": { "region": "New York", "city": "New York City" }
},
"status": "ready",
"canChangeNow": true,
"nextChangeAt": null
}
}
kind is datacenter, isp, or mobile. mode is rotating or static. target is the saved choice; a datacenter/ISP region/city can be "any". effective is the actual location of the IP the token holds right now, or null when it is not yet known. status is pending for a short time while a change propagates, ready once it has, package-inactive when the token's package is not active, or allowance-exhausted, only for an older Multi-location package that still has a traffic cap. A datacenter or ISP token reports two more values, judged from where its IP is right now:
fallback: the proxy is working on an IP outside its saved location, because the location had no free IP. A rotating proxy goes back by itself when an IP there is free. A static one stays where it is until you move it back, because an IP that changes unasked would break allowlists on the sites it is used with.unavailable: the proxy's IP isn't working right now; it is being moved to a working one, which usually takes a few minutes.
fallback is a newer value, so a client that switches on status needs to handle it. For a mobile token unavailable still means the target location currently has no working device (the token keeps its last one rather than losing its route).
List available locations#
GET /api/v1/tokens/{tokenId}/location/options?level=region
GET /api/v1/tokens/{tokenId}/location/options?level=city®ion=new-york
Datacenter/ISP: level=region, then level=city®ion=…. Mobile: level=city, then level=carrier&city=…. Each option is {"value": …, "label": …}, and the list holds only options this token can use right now.
Change location#
Datacenter/ISP:
curl -X PUT 'https://litport.net/api/v1/tokens/12345/location' \
-H "Authorization: Bearer $LITPORT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"region": "new-york", "city": "new-york-city"}'
Mobile:
curl -X PUT 'https://litport.net/api/v1/tokens/98765/location' \
-H "Authorization: Bearer $LITPORT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"city": "seattle", "carrier": "verizon"}'
Let the server choose, for either kind:
curl -X PUT 'https://litport.net/api/v1/tokens/12345/location' \
-H "Authorization: Bearer $LITPORT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"random": true}'
Send exactly the fields for that token's kind: datacenter/ISP is region and city; mobile is city and carrier. country is optional and must be "us" when sent. No precondition header is required or honored: the latest committed PUT becomes the token's desired target, and a PUT identical to the currently saved target succeeds without a new change.
A rotating datacenter/ISP token moves to a matching IP at once. A static one gets a new IP in the chosen location, using its once-a-week allowance; a repeat inside that week is 409 CHANGE_LIMIT with nextChangeAt. A Multi-location token moves at once, with no wait. A standard US Shared Mobile token can change once per 48 hours; a repeat inside that window is also 409 CHANGE_LIMIT.
For any mobile token, the city is always kept: if the chosen carrier has no working phone in that city, the proxy uses another carrier in the same city instead, and effective shows the carrier actually in use.
{"random": true} is sent alone, with no other field. The server picks a location this proxy can use right now, from the same values List available locations shows: a different city when there is one, and for a mobile proxy, when its city is the only one, another carrier in the same city. It never picks the proxy's current target. The pick is saved as the target like any other change, with the same checks and the same waits (a static datacenter/ISP proxy uses its once-a-week allowance, a standard US Shared Mobile proxy its 48-hour one, and CHANGE_LIMIT carries nextChangeAt as usual), and target in the answer shows where the proxy went. If a pick turns out to have no free IP or phone, the next one is tried, up to three. If there is no other location, or none of the three can take the proxy, the answer is 409 LOCATION_UNAVAILABLE and nothing changes.
A random change is not safe to repeat blindly: where no wait applies, each repeat moves the proxy again. After a lost response, read GET /tokens/{tokenId}/location before you send it again.
Move back#
POST /api/v1/tokens/{tokenId}/location/return
Authorization: Bearer lit_...
For a static datacenter or ISP token whose status is fallback: it takes a free IP in its saved location, on the same host, port, username and password. It is free: it doesn't use the once-a-week allowance, and the saved target doesn't change. The request takes no body (an empty {} is accepted). Returns the same shape as Location, with the new effective IP. The IP changes, so an allowlist on a target site that held the old IP stops matching.
A rotating token has no such request: it goes back by itself when an IP in its saved location is free. Any other token answers 409 NOT_SUPPORTED.
A repeat after a success is 409 NOT_IN_FALLBACK and gives no second IP, so the request is safe to send again after a lost response.
| Status | Code | Meaning |
|---|---|---|
400 |
INVALID |
The request has a body that isn't empty. |
404 |
TOKEN |
Unknown, unowned, or malformed tokenId. |
409 |
NOT |
Not a static datacenter or ISP token, or it has no saved location. |
409 |
PACKAGE |
The token or its package is not active. |
409 |
NOT |
The token isn't outside its saved location (a repeat after success lands here), or its IP isn't working yet. Nothing changed. |
409 |
LOCATION |
The saved location has no free IP yet. Nothing changed; try again later. |
409 |
CONCURRENT |
Another change to this token landed at the same moment. Read the location and send the request again only while status is still fallback. |
Location errors#
| Status | Code | Meaning |
|---|---|---|
400 |
INVALID |
The body has the wrong fields for this token's kind, random isn't true, or random was sent with other fields. |
400 |
INVALID |
Not a supported US location or carrier, or "any" for a standard Shared Mobile token. |
400 |
INVALID / INVALID |
Options: a level this token doesn't use, or a missing region/city. |
404 |
TOKEN |
Unknown, unowned, or malformed tokenId. |
409 |
NOT |
This token's location can't be changed. |
409 |
PACKAGE |
The token or its package is not active. |
409 |
CHANGE_LIMIT |
Inside the weekly or 48-hour wait. The error carries nextChangeAt and a matching Retry-After. |
409 |
LOCATION |
No usable IP or working phone there right now, or, with random, no other location is available. Nothing changed. |
409 |
CONCURRENT |
Another change to this token landed at the same moment. Retry the same PUT. |
Rotation#
PUT /api/v1/tokens/{tokenId}/rotation
Authorization: Bearer lit_...
Content-Type: application/json
{"intervalSec": 600}
{
"data": { "tokenId": 12345, "intervalSec": 600, "options": [600, 1800, 3600], "changeable": true }
}
intervalSec must be one of options, the same values the token's own rotation field lists. changeable is true for a rotating datacenter/ISP token, a standard US Shared Mobile token, and a US Private Device. A Private Device's options start at 2 minutes. A Multi-location token always reports its current phone's interval with changeable: false, because the phone the proxy is on sets its own timer; its PUT also answers 409 NOT_SUPPORTED, as does any other non-rotating token.
Rotation errors#
| Status | Code | Meaning |
|---|---|---|
400 |
INVALID |
The body is not exactly {"interval. |
400 |
INVALID |
intervalSec is not one of the token's options. |
404 |
TOKEN |
Unknown, unowned, or malformed tokenId. |
409 |
NOT |
The token doesn't rotate, or its interval is set by the phone (Multi-location) |
409 |
PACKAGE |
The token or its package is not active. |
New IP#
POST /api/v1/tokens/{tokenId}/new-ip
Authorization: Bearer lit_...
The Get new IP button on the proxy in the dashboard and the token's Change-IP link are the no-code ways to do the same; see Change IP. This endpoint covers datacenter and ISP tokens only. A Private Device changes IP with its Change-IP link. The request takes no body. The new IP stays in the token's saved location. A token whose status is fallback or unavailable (outside its location, or on an IP that stopped working) gets one in its saved location if one is free there, otherwise in the same region, then anywhere in the US. Returns the same shape as Location, with a new effective IP.
A static token uses its once-a-week allowance. The service reuses the token's own saved location on every attempt, including a retry.
A rotating token gets a new IP at any time. The new IP is always different from the current one, and the automatic timer restarts: the next automatic change comes rotation.intervalSec seconds after this one. New connections use the new IP within about 5 seconds; open connections keep the old IP until they reconnect. Two requests must be at least the shortest value in the token's rotation.options apart; a repeat inside that wait is 409 CHANGE_LIMIT.
On a static token this is the one write that is not safe to repeat blindly. If a response is lost or times out, read GET /tokens/{tokenId}/location before retrying: a nextChangeAt later than the current time means the change went through. Sending the POST again inside that week does not give a second IP; it returns 409 CHANGE_LIMIT.
On a rotating token a lost response can't be confirmed from GET /tokens/{tokenId}/location: its canChangeNow and nextChangeAt are about the location and say nothing about this wait. Sending the POST again after Retry-After is harmless; it just gives another new IP. A 409 CHANGE_LIMIT right after a lost response does not prove the change happened, because a 409 LOCATION_UNAVAILABLE answer also uses up the wait.
New IP errors#
| Status | Code | Meaning |
|---|---|---|
404 |
TOKEN |
Unknown, unowned, or malformed tokenId. |
409 |
NOT |
The token is not a datacenter or ISP proxy. |
409 |
PACKAGE |
The token or its package is not active. |
409 |
CHANGE_LIMIT |
Static: this week's replacement is already used. Rotating: inside the wait since the last request. The error carries nextChangeAt and a matching Retry-After. |
409 |
LOCATION |
No other usable IP in the saved location right now. Nothing changed; on a rotating token the request still uses up the wait. |
409 |
CONCURRENT |
Another change to this token landed at the same moment. Static: read the location, then retry. Rotating: send the request again once the wait has passed. |
Change-IP link#
rotateUrl changes the IP without an API key: send a GET to it from a script or an app, or open it in a browser. A link opened as a page in a browser tab shows a short page in the customer's language. Any other client gets the JSON below, and &format=json always returns JSON, for example for an app that drives a real browser. Anyone who has the link can change the IP, so keep it as private as the password. Reset next to the link in the dashboard issues a new link, and the old one stops working at once. On a rotating datacenter/ISP token it follows the same rules as POST /tokens/{tokenId}/new-ip above: the wait between changes, a new IP in the saved location, and the timer restart. Change IP is the guide to the link and the button. Every JSON answer is HTTP 200 with a body from this table:
| Body | Meaning |
|---|---|
{"status": "ok"} |
Done. A rotating datacenter |
{"error": "too |
Too soon after the last change. Send it again after retry seconds. Without retry, the link was called more than 20 times in a minute, or a Private Device changed its IP less than a minute ago. |
{"error": "noOtherIp"} |
No other IP in the token's saved location right now. Nothing changed, and the wait still applies. |
{"error": "invalid |
The link is unknown, or its token is expired, disabled, or in a package that is not active. |
{"error": "internal |
Something failed on our side. Try again later. |