eBay API integration in PHP: search items with the Browse API and OAuth
Advertisement
Showing live eBay listings on your own site (a product search, a niche affiliate store, a price tracker) means using eBay's developer API. Older tutorials, including an earlier version of this post, used the Finding API (svcs.ebay.com/services/search/FindingService/v1) with a simple application id in the URL. eBay announced that all Finding API calls would be decommissioned on 5 February 2025 and points developers to the Browse API instead (eBay's API deprecation page). If you are copying code that mentions findItemsAdvanced or FindingService, it will no longer work. This guide shows the current approach in PHP: get an OAuth token, search with the Browse API, display the results safely, and handle errors.
How this was tested. I wrote the client below, checked it with php -l, and ran it against a small local mock server that follows eBay's documented request and response shapes. I did not call the real eBay service in this session because it needs your own keys. Before going live, test with eBay's sandbox, and compare field names with the Browse API reference.
What you need
- A free account in the eBay Developers Program (developer.ebay.com).
- An application keyset: a Client ID (App ID) and a Client Secret (Cert ID). There is a Sandbox keyset for testing and a Production keyset for real data. eBay may ask you to complete extra compliance steps (for example about account-deletion notifications) before it enables production keys; follow what the developer portal tells you.
- PHP 8 with the
curlextension (php -m | grep curl) andjson.
Treat the secret like a password: keep it in an environment variable or a config file outside the web root, never in the code you commit, and never in JavaScript that runs in the browser. If it ever leaks (for example in a screenshot or a public repository), generate a new keyset.
How the Browse API works
The flow has two requests:
- Get an application access token by sending your Client ID and Secret to eBay's identity service using the OAuth client credentials grant. The Browse API reads public listing data, so this application-level token is enough; no eBay user has to log in.
- Call the search endpoint with that token as a Bearer header.
| Purpose | Request |
|---|---|
| Token | POST https://api.ebay.com/identity/v1/oauth2/token with Authorization: Basic base64(clientId:secret), form body grant_type=client_credentials&scope=https://api.ebay.com/oauth/api_scope |
| Search | GET https://api.ebay.com/buy/browse/v1/item_summary/search?q=camera&limit=10 with Authorization: Bearer <token> and X-EBAY-C-MARKETPLACE-ID: EBAY_US |
For the sandbox, use the host api.sandbox.ebay.com and your sandbox keys. The token is valid for a limited time (the response carries expires_in, commonly 7,200 seconds), so cache it rather than requesting a new one for every search.
The PHP client
<?php
declare(strict_types=1);
final class EbayClient
{
private ?string $token = null;
private int $tokenExpires = 0;
public function __construct(
private string $clientId,
private string $clientSecret,
private string $apiBase = 'https://api.ebay.com',
private string $marketplace = 'EBAY_US',
private string $cacheFile = __DIR__ . '/ebay_token.json',
) {}
/** Application access token (client credentials grant), cached until shortly before it expires. */
private function token(): string
{
if ($this->token !== null && time() < $this->tokenExpires) {
return $this->token;
}
if (is_file($this->cacheFile)) {
$cached = json_decode((string) file_get_contents($this->cacheFile), true);
if (is_array($cached) && ($cached['expires_at'] ?? 0) > time() + 60) {
$this->token = $cached['token'];
$this->tokenExpires = $cached['expires_at'];
return $this->token;
}
}
$ch = curl_init($this->apiBase . '/identity/v1/oauth2/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'Authorization: Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret),
],
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'client_credentials',
'scope' => 'https://api.ebay.com/oauth/api_scope',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$data = json_decode((string) $body, true, flags: JSON_THROW_ON_ERROR);
if ($status !== 200 || empty($data['access_token'])) {
throw new RuntimeException('eBay token request failed: HTTP ' . $status . ' ' . ($data['error_description'] ?? ''));
}
$this->token = $data['access_token'];
$this->tokenExpires = time() + (int) $data['expires_in'] - 60;
file_put_contents($this->cacheFile, json_encode(['token' => $this->token, 'expires_at' => $this->tokenExpires]));
return $this->token;
}
/** @return array{total:int, items:list<array<string,mixed>>} */
public function search(string $query, int $limit = 10, int $offset = 0, ?string $filter = null, ?string $sort = null): array
{
$params = ['q' => $query, 'limit' => $limit, 'offset' => $offset];
if ($filter) { $params['filter'] = $filter; }
if ($sort) { $params['sort'] = $sort; }
$ch = curl_init($this->apiBase . '/buy/browse/v1/item_summary/search?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->token(),
'X-EBAY-C-MARKETPLACE-ID: ' . $this->marketplace,
'Accept: application/json',
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$data = json_decode((string) $body, true, flags: JSON_THROW_ON_ERROR);
if ($status !== 200) {
$msg = $data['errors'][0]['message'] ?? 'unknown error';
throw new RuntimeException("eBay search failed: HTTP $status $msg");
}
return ['total' => (int) ($data['total'] ?? 0), 'items' => $data['itemSummaries'] ?? []];
}
}
A few details worth explaining:
- The secret comes in through the constructor, not a constant in the class. Load it from the environment:
new EbayClient(getenv('EBAY_CLIENT_ID'), getenv('EBAY_CLIENT_SECRET')). http_build_query(..., PHP_QUERY_RFC3986)encodes spaces as%20, which APIs expect, and it makes thefiltervalue with brackets and commas safe.- The token cache keeps the token in memory for the request and in a small file between requests, with a one-minute safety margin. On shared hosting, put the cache file outside the web root. At larger scale, use Redis or your framework's cache.
- Errors are exceptions, with the HTTP status and eBay's message, so your page can log them and show a friendly notice instead of a blank screen. Using
JSON_THROW_ON_ERRORmeans malformed responses raise aJsonExceptiontoo. CURLOPT_TIMEOUTstops a slow API from hanging your page forever.
Using it and showing results
<?php
require __DIR__ . '/EbayClient.php';
$client = new EbayClient(getenv('EBAY_CLIENT_ID'), getenv('EBAY_CLIENT_SECRET'));
$query = trim($_GET['q'] ?? 'camera');
try {
$res = $client->search($query, limit: 12, filter: 'price:[5..100],priceCurrency:USD', sort: 'price');
} catch (Throwable $e) {
error_log($e->getMessage());
exit('Sorry, search is unavailable right now.');
}
echo '<p>', (int) $res['total'], ' results</p>';
foreach ($res['items'] as $item) {
$title = htmlspecialchars($item['title'], ENT_QUOTES, 'UTF-8');
$price = htmlspecialchars($item['price']['value'] . ' ' . $item['price']['currency'], ENT_QUOTES, 'UTF-8');
$url = htmlspecialchars($item['itemWebUrl'], ENT_QUOTES, 'UTF-8');
$img = htmlspecialchars($item['image']['imageUrl'] ?? '', ENT_QUOTES, 'UTF-8');
echo "<div class='item'>", $img ? "<img src='$img' alt=''>" : '', "<a href='$url'>$title</a> <b>$price</b></div>";
}
Always escape the values you print. Listing titles come from sellers, and a title containing HTML would otherwise run in your visitors' browsers (an XSS bug). In my test, a title like Vintage <b>Camera</b> & Case was printed as Vintage <b>Camera</b> & Case, which is what you want. Not every item has an image, so the code uses ?? for optional fields; do the same for any field you are not certain about.
What the response looks like
A successful search returns JSON with the total, paging fields and an itemSummaries array. Each summary typically includes itemId, title, price (value and currency), image.imageUrl, itemWebUrl, condition and the seller's location, among other fields. In the mock run the client printed:
total: 2
Vintage <b>Camera</b> & Case | 49.99 USD | https://i.ebayimg.com/a.jpg | https://www.ebay.com/itm/111
Camera strap | 9.50 USD | (no image) | https://www.ebay.com/itm/222
token requests: 1
eBay token request failed: HTTP 401 client authentication failed
eBay search failed: HTTP 400 The 'q' query parameter is required
That output shows the points that matter: two searches in a row made one token request (the cache works), wrong credentials fail loudly, and a missing query is reported with the API's own message.
Useful search options
| Parameter | Example | Meaning |
|---|---|---|
q |
q=vintage camera |
keywords |
limit, offset |
limit=50&offset=50 |
paging (check the documented maximum limit) |
filter |
price:[10..100],priceCurrency:USD |
price range, condition, buying options and more |
sort |
sort=price or -price |
ascending or descending |
category_ids |
category_ids=625 |
restrict to a category |
The marketplace header (EBAY_US, EBAY_GB, EBAY_DE, EBAY_IN...) chooses the site and currency. The old Finding API's global ids such as EBAY-DE map to these.
Good practice
- Cache search results for a few minutes (a file, Redis, or your framework's cache keyed by the query). It makes pages faster and keeps you inside eBay's call limits, which depend on your keyset and can change, so read the limits in your developer account.
- Never call the API from JavaScript in the browser with your secret. Call it from your PHP and give the browser your own JSON.
- Handle zero results and errors with clear messages.
- Follow eBay's API license and branding rules, especially if you monetise the page with the eBay Partner Network affiliate links. Read the terms before you publish.
- Log failures with
error_logand watch for authentication errors, which usually mean an expired or wrong keyset. - Do not scrape eBay pages as a workaround. Use the API.
Common errors
| Symptom | Likely cause |
|---|---|
HTTP 401 invalid_client on the token call |
wrong Client ID or Secret, or using sandbox keys against the production host (or the reverse) |
| HTTP 401 on search | missing or expired token, or the wrong scope |
| HTTP 400 on search | a missing q (or other required filter), or a bad filter string |
| HTTP 403 | the keyset is not enabled for that API or marketplace |
| HTTP 429 | too many calls; cache results and slow down |
Empty itemSummaries |
no matches, or filters that are too tight |
Old svcs.ebay.com code returns errors |
the Finding API is retired; migrate to the Browse API |
Next steps
If you are new to calling web services from PHP, the free PHP course explains the language basics, and the PHP interview questions for experienced developers include a worked example of building and calling a small JSON API with cURL. When your integration is ready, the guide to deploying PHP code from Git to shared hosting shows how to release it, and the Google Translate API post uses the same pattern of API key, request and JSON response.