Download the PHP package proxy-seller/user-api-php without Composer

On this page you can find all versions of the php package proxy-seller/user-api-php. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package user-api-php

Proxy-Seller Client API SDK for PHP

This package is a small Client API v2 client. It builds requests, adds the API key, parses the common {status,data,errors} envelope and supports raw/binary responses.

Breaking changes against 1.x are listed in CHANGELOG.md.

Install

Pin the major. 1.x is the client for /personal/api/v1/ and is a different API, so a bare composer require proxy-seller/user-api-php can resolve to 1.x and none of this document applies.

Configuration

Do not cut the payment timeout short: a large order can take the server well over 30 seconds, and a call that times out may still have created and paid for the order. Payment calls always wait at least moneyTimeout, whatever timeout says — see Timeouts and retries on payments.

Nothing else is required — the client talks to https://proxy-seller.com/personal/api/v2/ by default. It also paces its own requests to stay under the API's limits; see Rate limits and the request queue.

Paying for orders. Every order and renewal needs a payment system, and order/make accepts only two: balance (the account balance) and paddle_subscription (the saved card, which needs an active card subscription). Set the code once, or pass it per call:

setPaymentCode() and setPaymentId() only set the client default. A payment named in the call itself — paymentId or paymentCode in the options array, or the $paymentId argument of balanceAdd() — wins, and then the client's pair is not sent at all, neither the id nor the code. So setPaymentCode('paddle_subscription') followed by a call with ['paymentId' => 'balance'] pays from the balance, not with the card. Within one level the old rule stands: given both a code and an id in the same call, the code wins. An empty value (null, '') in the call counts as not given.

balancePaymentsList() is not where order payments come from. It lists the systems for topping up the balance with balanceAdd(), never contains the balance itself, and none of its entries can pay for an order. For a top-up an id is unavoidable: several payment systems share the same internal code (a single cryptomus covers "USDT (TRC-20)", "All cryptocurrencies" and more), so the code cannot tell them apart — see Balance and auto top-up. Everywhere else you use human-readable codes.

Pointing the client at another host (local testing) `baseUrl` must include `/personal/api/v2/`. You may also inject an already configured Guzzle-compatible client through the `client` option — useful for a custom transport, tracing, TLS settings or local stubs. On payments and writes the SDK adds two request options to whatever client is used: `curl` with `CURLOPT_FRESH_CONNECT => true`, and on payments `timeout` (see [Timeouts and retries on payments](#timeouts-and-retries-on-payments)). For a Guzzle client the SDK reads its `curl` and `timeout` defaults through `getConfig()` and merges with them — your cURL options stay in place, and your timeout is only ever raised, never lowered; a Guzzle client without a timeout keeps waiting without a limit. A custom transport without `getConfig()` receives the options as they are, and should merge a request-level `curl` array with its own defaults.

IDs in v2 are strings, not numbers

Every identifier the API returns — orderId, IP address ids, auth ids, paymentId — is an ObjectId: a 24-character hex string such as 66f0c2a1b4d3e5f6a7b8c9d0. Do not cast them to int, do not compare them numerically, and store them as strings:

Casting to int truncates an ObjectId to a meaningless number (often 0), which silently returns the wrong page of data instead of failing.

One exception: resident list ids are numeric (64-bit integers), not ObjectIds. They are used by residentListRename(), residentListRotation(), residentListDelete(), residentSubUserListRename() and proxyDownloadResident($id).

Current order API

Every *Id argument accepts either an ObjectId or the matching stable code. The server tries the value as an id first and falls back to a code lookup when it is not a valid id. Codes therefore go in positionally — there is no need for a chain of nulls and an options array just to carry them:

The remaining nulls above are genuine optional values (authorization, coupon), not placeholders.

The final options array is for fields without a positional argument: uptime, mixId/mixCode, generateAuth, and protocol outside the IPv6 helpers. The explicit *Code keys (countryCode, periodCode, operatorCode, mixCode, tarifCode, paymentCode) still work and win over the paired *Id, so use them when a self-documenting payload matters more than a short call.

rotationCode is a trap. It is the one field without a code lookup: the server checks that the value is an integer and copies it into rotationId unchanged, so '5m' fails with Set existed [rotationCode] from reference and nothing else happens. Pass the number of minutes — in rotationId, which is what rotationCode would become anyway.

What you can pass, and where to get it

Every field in the reference is called id, and its value is a readable code — not an ObjectId. Read id, put it in the matching *Id argument. That is the whole rule:

Argument Pass this Read it from
countryId alpha-3 country code, e.g. USA (upper-cased server-side, so usa works) reference/list → country[].id
periodId period code, e.g. 1m (lower-cased server-side) reference/list → period[].id
operatorId mobile operator code — exact match, case-sensitive reference/list/mobile → country[].operators.dedicated[] / .shared[] → id
rotationId minutes as an integer, 0 = By Link — the one id that is a number, not a code reference/list/mobile → country[].operators.*[].rotations[].id — that value is the minute count (name is "5 minutes" / "By Link")
mix (first argument of orderCalcMix / orderMakeMix) mix package code — exact match reference/list/mix → quantities[].id, e.g. europe-2-mix_IPv4
tarifId resident tariff code — exact match, e.g. 1-gb reference/list/resident → items.tarifs[].id
paymentId / paymentCode balance or paddle_subscription (the saved card) — the only two accepted for orders and renewals these two codes, no lookup needed (see "Paying for orders" above); balance/payments/list lists top-up systems for balanceAdd() only

ObjectIds are still accepted everywhere if you happen to have them; the reference simply no longer publishes them. Code resolution happens in order/calc, order/make, prolong/calc and prolong/make.

ipv4, ipv6 and isp orders need a goal (customTargetName) — what you use the proxies for. The SDK checks it locally so the server does not have to answer Incorrect goal (code 14). A mix order needs one only when the server cannot tell which package you mean, so naming the package removes the need for it.

For a fully custom payload use orderCalc(array $payload) or orderMake(array $payload).

Listing orders

Every filter is optional. Query filters and response fields use snake_case names such as start_date and is_extend; the full filter set is order_id, start_date, end_date, status, is_extend, auto_order, page, limit, sort_by, order. order_id matches either of the two order identifiers described below.

data is not a flat list but a metadata + items pair, and metadata is always present: without limit it reports total_pages => 1, current_limit => 0 and the whole list in items. summ and items[]['price'] are strings with the currency already in them ('$25.00'), auto_order and is_extend are 'Y'/'N' rather than booleans, and the dates are ISO 8601 with offset (2026-09-01T14:15:26+00:00) strings. id is a numeric order ID sent as a string; the ObjectId is order_id — the same value proxyList() returns as order_id, and the one to pass when renewing ipv6, mix and mix_isp (see Renewing proxies).

The optional X-Fingerprint header

order/make accepts an optional X-Fingerprint header, used for anti-fraud checks and affiliate attribution when present. It is not required: orders placed with an API key are created without it in every section, residential and scraper included. The SDK sends the header whenever a value is configured and never refuses an order when it is missing.

Any opaque string is accepted — the server does not validate its shape — but if you send one, make it a stable identifier of your installation. The SDK deliberately does not generate one: a value randomized per process would break the anti-fraud and affiliate attribution the header exists for. An empty value counts as unset, and no header is sent.

Renewing proxies

What a renewal is addressed by depends on the type:

What to pass per type, and which proxyList() field it comes from:

Type Pass this Built from Sent as
ipv4, isp the plain address, "1.2.3.4" — or the proxy id ip — or id ips — or ids
mobile "ip:port_http:port_socks", e.g. "1.2.3.4:50100:50101" — or the proxy id ip, port_http, port_socks — or id ips — or ids
ipv6, mix, mix_isp the order id order_id (the same value as in orderList()) orderIds

The SDK routes each value by its shape: a value with a dot or a colon is an address and goes to ips, anything else is an id and goes to ids for ipv4 / isp / mobile and to orderIds for ipv6 / mix / mix_isp. To set a field yourself, pass ids, ips or orderIds in the final options array; empty lists are never sent.

For ipv4, isp and mobile pass either ids or addresses in one call, not both. Given both ids and ips, the server renews by ids and ignores ips, so the addresses would silently drop out of a paid renewal. The SDK therefore throws \InvalidArgumentException — "Mixing proxy ids and addresses in one call is not supported: pass either ids or addresses" — before anything is sent, whether the mix sits in one list or is split between the list and the options array. Renew ids and addresses in two calls. For ipv6, mix and mix_isp a mixed list is still routed (ids to orderIds, addresses to ips), and the server rejects the address part itself.

The server refuses a selection field of the wrong kind instead of guessing, with code 0: ids or ips for ipv6 / mix / mix_isp fails with [ids] is not applicable for ipv6: prolong by [orderIds] ([ips] … for addresses), and orderIds for ipv4 / isp / mobile fails with [orderIds] is not applicable for ipv4: prolong by [ids]. An order that is not yours or has no active proxies of that type — or an empty list — fails the whole request with Incorrect orderIds (code 29), and nothing is renewed. quantity and items in the prolongCalc() answer show what a renewal actually covers.

prolongMake() answers orderId, orderIds, total, listBaseOrderNumbers and balance. orderIds lists every renewed order — one request can renew several — and orderId is orderIds[0]. listBaseOrderNumbers holds one base order number per renewed order (per package for mix / mix_isp).

prolongMake() throws ApiException when the balance is short — the renewal did not happen. Check the price with prolongCalc() first if you want to handle that gracefully.

orderSeparatorIds and orderSeparatorId are gone from the renewal body — the server no longer reads them. Passing either in the options array throws \InvalidArgumentException that names the replacement, orderIds, instead of dropping it silently. The list is Api::PROLONG_REMOVED_FIELDS.

Automatic renewal

prolongMake() charges you now. autoprolong/* only arms a charge that happens later, without you present — a separate branch of the API, not a flag on prolong.

The selection works exactly as in Renewing proxies: ids (proxy ids) or ips (addresses) for ipv4, isp and mobile (one kind per call — a mix throws), orderIds (order ids) for ipv6, mix and mix_isp, and the removed orderSeparatorIds / orderSeparatorId are refused by name.

paymentId is mandatory for calc and enable — the charge happens while you are away, so the payment system cannot be guessed. Only balance and paddle_subscription are accepted: a one-off Paddle checkout needs a browser redirect a headless client cannot complete. paddle_subscription charges the card saved on the account: pass subscriptionId only when the account has several saved cards (the server answers Set [subscriptionId]), with one card it is picked automatically.

Residential packages renew as a package, not as addresses — send no selection:

For resident pass no selection at all. A non-empty $ids list, or a non-empty ids, ips or orderIds in the options array, throws \InvalidArgumentException — "resident auto-prolong applies to the whole package: do not pass proxy or order ids" — before anything is sent. The SDK deliberately does not strip the selection: a disable meant for a few addresses would otherwise switch off auto-renewal for the whole package. (The server refuses such a body as well: any of ids, ips and orderIds fails with [ids] is not applicable for resident: auto-prolong applies to the whole package.) An empty list is fine, and a period is neither needed nor sent.

Three things about the answers before you parse them:

scraper has no auto-renewal: it is extended by buying traffic through order/make.

Replaces resident/autorenew/{enable,disable,calculate}, removed from the server.

Errors

Business errors arrive with HTTP 200. The envelope carries them in errors[], so status codes alone tell you nothing. Catch ApiException to retain both layers:

The same ApiException covers what is not a business error:

The API key never shows up in an error. It is a segment of the URL path, and cURL messages and some error pages repeat that path — the front even lower-cases it. The SDK replaces the key with *** everywhere in an ApiException (message, getResponseBody(), getErrors(), getData()), ignoring case and in its URL-encoded forms too, and var_dump($api) / print_r($api) show the base URL masked. The Guzzle client returned by getClient() is untouched: its base_uri still holds the key, so do not dump it into logs.

Access errors are a fixed triple — read the whole array

A bad API key, a caller IP outside the key's allowlist and an exceeded request limit (1000 requests per calendar minute per key) all come back as HTTP 200 with the same three-element errors array:

Because the exception message is built from errors[0], it always reads Error api key in all three cases. The API itself never answers this with HTTP 429 — a 429 comes only from the edge in front of it, and the SDK retries it (see Rate limits and the request queue). Never branch on the message alone:

Validation bounds live in customData

Some validation errors carry the acceptable values alongside the message. getCustomData() returns the customData of the first error that has one:

Calculation responses (order/calc, prolong/calc, autoprolong/calc) with status=error, useful data, and an empty errors array are returned as warning data instead of causing a parser failure. Inspect $api->getLastResponseStatus() if this distinction matters. Payments and writes are strict: for them only status: "success" is a success, and the same shape throws ApiException — see Timeouts and retries on payments.

Rate limits and the request queue

The API accepts up to 1000 requests per minute per key and answers anything above that with the access-error triple, which cannot be told apart from a wrong key or a blocked IP. So the client paces its own requests — by default, with nothing to set up:

What goes where — by the endpoint a method calls, not by its HTTP method:

Kind Methods
payment: lane, 2 s apart orderMake() and every orderMake*() helper, prolongMake(), balanceAdd()
write: lane, 1 s apart autoProlongEnable(), autoProlongDisable(), authAdd(), authAddIp(), authChange(), authDelete(), proxyReplace(), proxyCommentSet(), balanceAutoTopupSet(), residentListAdd(), residentListRename(), residentListRotation(), residentListTools(), residentListDelete(), residentSubUserCreate(), residentSubUserUpdate(), residentSubUserDelete(), residentSubUserListAdd(), residentSubUserListRename(), residentSubUserListRotation(), residentSubUserListTools(), residentSubUserListDelete()
read: window only everything else — lists and get calls, orderCalc*(), prolongCalc(), autoProlongCalc(), referenceList(), downloads, residentGeo*(), consumption and traffic statistics

Change the defaults, or switch the queue off, with the rateLimit config key:

'rateLimit' => false is short for ['enabled' => false], and 'rateLimit' => true means all defaults — the same as leaving the key out. In an array, omitted keys keep their defaults. An unknown key or an invalid value — anything other than an array or a boolean, too — throws \InvalidArgumentException when the client is created, so a typo cannot silently fall back to a default.

The queue belongs to one Api instance. Separate instances and separate processes using the same key know nothing about each other. Under php-fpm or mod_php nothing survives from one web request to the next, so every request starts with a new, empty queue, and requests served in parallel run in separate processes. The queue therefore helps scripts, workers and daemons that make several calls in one process — create the instance once there and reuse it. When several processes share a key they can still exceed the limits together, and the server may then answer with the access-error triple or with code 57. Both reach you as ApiException; the SDK does not retry them.

Waiting blocks. The SDK waits with usleep(), so the call simply returns later; there is no busy-waiting. Calls on one instance run one after another, so writes cannot overlap. If your transport yields (fibers, an event loop) and a second write starts on the same instance while the first is still in flight, the second is not sent: it throws \LogicException.

For tests, rateLimit also accepts clock — a callable returning the current time in milliseconds (monotonic) — and sleeper, a callable that receives the milliseconds to wait. Together they let a test run the queue on fake time.

Timeouts and retries on payments

order/make (every orderMake*()), prolong/make (prolongMake()) and balance/add (balanceAdd()) move money. They get their own timeout, moneyTimeout — 120 seconds by default — because the server builds some orders synchronously: a large MIX order takes about a second per country. Every other call keeps timeout, 30 seconds by default. A payment waits for the longer of the two, so a short timeout never cuts it off; 0 in either means no limit, as in Guzzle.

A timeout, a dropped connection or a 5xx on a payment means the outcome is unknown. The request may have reached the server, and the order may have been created and paid for — or the balance topped up — although no answer came back. The SDK never repeats such a call itself (its only automatic retry is HTTP 429 from the edge, which means the request never reached the API), and neither does the transport (see Rate limits and the request queue). Do not repeat it blindly either — check first:

Call Check before repeating
orderMake*() orderList(['sort_by' => 'date_insert', 'order' => 'desc', 'limit' => 10]) — is the order there?
prolongMake() proxyList() — have the end dates moved? — or orderList(['is_extend' => 'Y'])
balanceAdd() balance(); an unpaid payment link charges nothing, so asking for a new one is safe

What reaches you is always an ApiException, and when the outcome is unknown its message says so — "the request may have been executed — check before retrying":

A business error — getErrors() is not empty, e.g. insufficient funds — and a 429 left after all retries are answers from the API or its edge: the call was refused. Writes (auth*(), proxyReplace(), autoProlongEnable() and the rest of the write row above) follow the same strict rules; an unknown outcome there costs no money, but re-read the state before repeating.

Under php-fpm the web server has limits of its own: nginx's fastcgi_read_timeout (60 seconds by default) or php-fpm's request_terminate_timeout can end the PHP request before a 120-second payment returns — the order then completes on the server while your script never learns about it. Place orders from a CLI worker or a queue job, or raise those limits.

Balance and auto top-up

balance/add accepts only paymentId. Unlike order and prolong endpoints, it does not resolve a paymentCode, so balanceAdd() throws \InvalidArgumentException when only a code is configured instead of sending paymentId: null and returning the opaque Set existed [paymentId] — pass the top-up system's id explicitly, as above, even when setPaymentCode('balance') is configured for orders. The internal balance itself is not in the payment list — you cannot top up the balance with the balance — and the list is for top-ups only: orders and renewals are paid with balance or paddle_subscription.

Auto top-up charges a saved Paddle payment method when the balance drops below threshold:

Allowed keys are enabled, threshold, amount, subscriptionId. Anything else raises \InvalidArgumentException locally — the server ignores unknown JSON fields, so a typo such as daily_count_cap would otherwise look like a successful call that changed nothing. set returns the state after saving, so no second get is needed.

dailyCountCap and monthlyAmountCap are gone. Removed from the contract on 2026-08-18: the server silently ignores them and they are absent from the response. The SDK now rejects them by name for exactly the reason above — otherwise balanceAutoTopupSet(['dailyCountCap' => 3]) would report success and change nothing.

Validation runs server-side on the merged result, which means changing one field can fail because of another one that was already stored. Error codes: 49 feature unavailable, 50 threshold below minimum, 51 amount below minimum, 52 amount does not cover the threshold, 53 no saved payment method, 56 saved card expired. Codes 54 and 55 were removed together with the caps and are not reused. Bounds come back in customData (minAmount, minThreshold).

Proxy replacement

proxyReplace($ids, $type, $comment) — $type is the reason for the replacement, not a proxy type:

Valid reasons: NOT_WORK, INCORRECT_LOCATION, CANT_CHANGE_NETWORK, LOW_SPEED, CUSTOM. CUSTOM requires a non-empty $comment. Both rules are checked locally before the request leaves.

Downloads, prolong and resident subusers

Local verification

The suite is offline: Api accepts an injected HTTP client through the client config key, so the tests drive the SDK with canned envelopes and inspect the request it built. They answer "does the SDK assemble and parse correctly", not "is the server up" — nothing is mocked away that the SDK itself is responsible for. Two transport tests are the exception that proves it: the re-send they guard against is libcurl's own, so they start a small HTTP server on 127.0.0.1 in a separate PHP process (tests/Support/drop_after_body_server.php, via proc_open) and are skipped without the curl extension.

Every assertion follows the behaviour of the v2 API server, not of this README: docs can drift from the server without anyone noticing, a test cannot. What is covered:

To exercise a locally running API server, point the local baseUrl shown above at it (port 7995 in that example), use a development API key and call read-only endpoints first (balance, authList, residentList).


All versions of user-api-php with dependencies

PHP Build Version
Package Version
Requires php Version ^7.2.5 || ^8.0
guzzlehttp/guzzle Version ^7.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package proxy-seller/user-api-php contains the following files

Loading the files please wait ...