What this API is
Phoneo gives your storefront a live, read-only feed of the inventory belonging to the
shops Phoneo has assigned to you. If you run an online store at, say,
yourstore.in, this is how yourstore.in lists Phoneo stock: your server
pulls the catalog, stores it on your side, and renders it.
A shop is only ever assigned to you after its owner has agreed to share their stock with you. To get access, see Getting set up.
Every endpoint is a GET. There are no write endpoints — none, anywhere.
Your key cannot create, update or delete anything inside Phoneo.
It cannot mark an item sold, change a price, or place an order. This is a property of the
API itself, not a permission you were given: no non-GET route exists on
/api/partner/v1, and a test in Phoneo's own suite fails the build if one is
ever added.
| Base URL | https://super.phoneo.in/api/partner/v1 |
|---|---|
| Auth | Header X-Partner-Key |
| Format | JSON — send Accept: application/json |
| Rate limit | Per key, default 120 requests/minute (429 when exceeded) |
| Direction | You pull. Phoneo never calls your servers. |
Authentication
Send your key on every request:
curl "https://super.phoneo.in/api/partner/v1/ping" \
-H "X-Partner-Key: phn_live_a1b2c3d4e5f6g7h8_XXXXXXXXXXXX" \
-H "Accept: application/json"
Keep the key on your server. Never put it in browser JavaScript, a mobile app bundle, or a public repository. Anyone holding it can read the full catalog of every shop mapped to you.
If it leaks, email support@phoneo.in immediately. Phoneo can switch the key off at once and send you a new one.
Errors
| Status | error | What happened |
|---|---|---|
| 401 | invalid_key | Key is wrong, or the header is missing |
| 401 | key_revoked | Key was revoked or has expired |
| 403 | ip_not_allowed | Request came from an IP outside your allowlist |
| 403 | ability_not_registered | The endpoint exists, but Phoneo has not added it to the permission list yet |
| 403 | ability_disabled | The permission exists but is currently switched off for everyone |
| 403 | ability_missing | The permission is live, but your key was not given it |
| 400 | invalid_key_format | The product key in the URL is not source_type:id |
| 404 | not_found | Item does not exist, or is not in your scope |
| 422 | — | A query parameter failed validation |
| 429 | — | Rate limited — honour Retry-After |
The errors in the table with an error code come back as
{"success": false, "error": "…", "message": "…"}. Branch on
error, never on message — the message is plain English for
your logs and its wording may change. Two responses have a different shape:
422—{"message": "…", "errors": {"limit": ["…"]}}, one entry per failing parameter.429—{"message": "Too Many Attempts."}with aRetry-Afterheader in seconds.
The three 403 permission errors are deliberately kept apart, because the
fix is different for each — and none of them is something you can fix on your side. Quote the exact
error string to support@phoneo.in:
ability_not_registered and ability_disabled are settings on Phoneo's end,
ability_missing means your key needs that permission added.
Key rotation
When Phoneo rotates your key, the new one reaches you the same way the first one did — a one-time link by email (see Getting set up). Your current key keeps working until you open that link, and for 48 hours after. Deploy the new key inside that window and your site never goes down. Build for this: read the key from config, not from a constant in code.
The three kinds of stock
Phoneo sells three different things, and they behave differently. Your catalog will contain
all three mixed together, distinguished by source_type:
source_type | What it is | Notes |
|---|---|---|
used_phones |
A second-hand phone | Every one is a unique physical handset. Carries condition,
battery_health and what came in the box. Usually has a real photo gallery. |
new_phone_units |
One unit of a brand-new phone | condition is always "new". variant holds the
storage size. Often only one or two catalogue images. |
other_item_units |
One unit of an accessory or other item | Chargers, cases, earbuds. condition is "other". |
An item appears only when the shop has marked it visible to everyone and it is
for sale. Many shops leave that switched off for new phones and accessories, so expect most of
your catalog to be used_phones. Code for all three anyway — a shop can switch them on
at any time.
Product key
These live in three separate tables, so a plain numeric id is not unique. Every item is identified by a composite key:
used_phones:1042
new_phone_units:88
other_item_units:310
Store this key on your side. It is what you pass to
/products/{key} and to the availability check, and it never changes for the life
of the item.
stock_quantity is only ever 1 or 0. Five
identical chargers are five separate keys, not one row with a quantity of five. If you want
to group them on your site into a single product page, group by
brand + model + variant + color yourself.
Endpoints
GET /ping
Check your key and IP allowlist. Needs no particular permission — a valid key is enough, so this is the first call to make when setting up.
{
"success": true,
"message": "pong",
"partner": {
"name": "Your Store",
"key_id": "a1b2c3d4e5f6g7h8",
"abilities": ["catalog.*"],
"rate_limit_per_min": 120
},
"shops_available": 2,
"time": "2026-08-29T12:00:00+05:30"
}
GET /shops
The shops assigned to you. A shop is identified by its handle — that is what
you pass to /products?shop=.
{
"success": true,
"data": [
{ "handle": "mobile-point", "name": "Mobile Point", "is_online": true }
]
}
GET /products
The catalog. This one endpoint does both the initial full import and the incremental delta sync.
| Parameter | Type | Meaning |
|---|---|---|
shop | string | Limit to one shop, by handle |
source_type | string | used_phones · new_phone_units · other_item_units |
main_category_id | int | Filter by category |
updated_since | ISO 8601 | Delta mode — also returns removed items.
Any offset works (Z, +05:30, -04:00); URL-encode the + as %2B.
Timestamps Phoneo returns are in IST (+05:30). |
cursor | int | paging.next_cursor from the previous page |
limit | int | Default 100, maximum 500 |
{
"success": true,
"data": [
{
"key": "used_phones:1042",
"source_type": "used_phones",
"status": "active",
"shop": { "handle": "mobile-point", "name": "Mobile Point", "is_online": true },
"title": "Apple iPhone 13 128GB Blue",
"slug": "apple-iphone-13-128gb-blue",
"brand": "Apple",
"model": "iPhone 13",
"variant": "128GB",
"color": "Blue",
"condition": "Good",
"description": "Single owner, no scratches",
"sale_price": "45999.00",
"price_hidden": false,
"currency": "INR",
"stock_quantity": 1,
"main_category_id": 3,
"main_category": {
"id": 3,
"name": "Mobiles",
"thumb": "https://img.phoneo.in/Phoneo/2025/Image/Enjoyer/E5/MainCat/MainCat-3/Thumb/b.jpg"
},
"thumb": "https://img.phoneo.in/2026/Image/E5/UsedPhone/UP-1042/Thumb/a.jpg",
"images": ["https://img.phoneo.in/…", "https://img.phoneo.in/…"],
"attributes": {
"battery_health": "89%",
"bill_box_cable_warranty": "Box only",
"offer": "Festive"
},
"updated_at": "2026-08-29T11:42:10+05:30"
}
],
"paging": { "limit": 100, "next_cursor": 1042, "has_more": true }
}
Pagination
Pass paging.next_cursor back as cursor on the next request, and
stop when has_more is false. The cursor is stable — new items
arriving mid-walk will not make you skip or repeat a row.
Removed items — read this part twice.
When a phone sells, is hidden by the shop, or is deleted, it simply stops appearing in
/products. A plain re-fetch will never tell you it is gone, so a naïve
integration keeps a sold handset on sale forever.
The only way to learn about removals is the delta call:
GET /products?updated_since=2026-08-29T06:00:00%2B05:30
In that response the item comes back with
"status": "removed" and "stock_quantity": 0. Take it off your site
as soon as you see it.
GET /products/{key}
One item, in the same shape as a listing row. For example
/products/used_phones:1042.
An item outside your scope returns 404, the same as one that
does not exist — deliberately, so the endpoint cannot be used to probe for other shops' ids.
GET /products/availability
Check up to 200 items at once. Call this before you confirm any order. Your catalog copy may be minutes old, and the same handset can sell over the counter in the shop at any moment.
GET /products/availability?keys=used_phones:1042,other_item_units:310
{
"success": true,
"data": [
{ "key": "used_phones:1042", "status": "active", "available": true,
"sale_price": "45999.00", "price_hidden": false,
"updated_at": "2026-08-29T11:42:10+05:30" },
{ "key": "other_item_units:310", "status": "removed", "available": false,
"sale_price": "1499.00", "price_hidden": false,
"updated_at": "2026-08-29T10:10:00+05:30" }
]
}
keys may also be sent as an array
(keys[]=…&keys[]=…). A key that is unknown or outside your scope comes back
as "status": "unknown" with available: false — for your purposes the
decision is the same either way: do not sell it.
GET /categories
The list of categories that currently hold a product you can see — see the Categories section.
Pricing & the hidden-price rule
sale_price is always a string, and it comes in two shapes:
"45999.00"— a real price"X"— the shop has chosen to hide this item's price
So never feed sale_price straight into a number parser. Branch on
price_hidden first:
if (product.price_hidden) {
showEnquiryButton(); // "Price on request"
} else {
showPrice(parseFloat(product.sale_price));
}
Price-hidden items are still real, in-stock products — list them,
just without a number. Currency is always INR. This is the shop's selling
price; any margin of yours is yours to add.
Categories
Each shop groups its stock into categories of its own (for example "Mobiles",
"Accessories"). A product carries its category twice: as the raw id
main_category_id for your own joins, and as a ready-made main_category
object with the name and image filled in, so you do not need a second lookup just to render a
breadcrumb.
| Field | Type | Meaning |
|---|---|---|
main_category.id | int | Same value as main_category_id |
main_category.name | string | Display name, as the shop wrote it |
main_category.thumb | string · null | Full image URL, or null when the shop has not set one |
Both main_category_id and main_category are
null when a shop has not categorised an item — and today that is most
items. Put uncategorised products in a general section rather than hiding them, or
they become unreachable on your site.
GET /categories returns every category you can currently see:
{
"success": true,
"data": [
{
"id": 3,
"name": "Mobiles",
"thumb": "https://img.phoneo.in/Phoneo/2025/Image/Enjoyer/E5/MainCat/MainCat-3/Thumb/b.jpg",
"sub": []
},
{
"id": 7,
"name": "Accessories",
"thumb": null,
"sub": []
}
]
}
Only categories that actually contain a product you can see are listed, so this
list shrinks and grows with the catalog. sub is always an empty array — Phoneo no
longer has sub-categories (see the Changelog). The key is kept so that
code written against the earlier nested shape keeps working.
How to keep your site in sync
- Once, at setup: walk
GET /productswith the cursor untilhas_moreisfalse. Store every item'skeyandupdated_at. - Every 5–15 minutes: call
GET /products?updated_since=<the newest updated_at you hold>. Upsert rows withstatus: "active"; remove rows withstatus: "removed". - At checkout, every time:
GET /products/availabilityfor the items in the basket. - Once a day: re-walk the full catalog and reconcile against your copy. This is the safety net that catches anything the delta missed.
updated_since by a few minutes rather than using the exact last
timestamp. Re-processing an item you already have costs nothing; missing one costs you a sale
or leaves a sold phone on your site.
What you will never receive
These fields are deliberately excluded, and no parameter will turn them on:
- IMEI and serial numbers (
IMEI,SN,WSN) - Purchase price, sold price, or any margin figure
- Supplier, vendor or customer details of any kind
- Private/internal notes and tags
- HSN/SAC and other tax fields
- The shop's numeric id, phone number or address
If your integration genuinely needs something in this list, raise it with Phoneo as a scope question. It is not a setting anyone can flip on the API.
Getting set up
- Request access. Email support@phoneo.in with your company name, your website, the email address the key should go to, the Phoneo shops whose stock you want to list, and — if you have them — your servers' fixed outbound IPs.
- Shop consent. Phoneo confirms with each shop owner before assigning their shop to you. You only ever see shops that have agreed.
- Receive your key. Phoneo emails you a one-time link. Open it and press
Reveal my API key. The key is created at that moment and shown once
— copy it straight into your server's configuration. It cannot be looked up again, only
replaced.
- The link expires after 24 hours and works only once.
- If the link says it has already been used and you did not open it, email support@phoneo.in straight away — someone else may have opened it. Phoneo will switch that key off and send you a new link.
- If it has expired, ask for a new one. Each new link cancels the previous one.
- Confirm the connection with
GET /ping. - Do the full catalog walk, then put the 5–15 minute delta job on a schedule.
A Postman collection and environment file are available alongside this document. Questions: support@phoneo.in.
Changelog
| Date | What changed | What you need to do |
|---|---|---|
| 2026-09-24 |
Keys arrive by one-time link; clearer errors.
|
Nothing, if you branch on error. If you ever sent
updated_since with an offset ahead of IST, run one full catalog walk to catch
anything a delta missed. |
| 2026-09-24 |
Sub-categories removed. Phoneo is retiring sub-categories across the product from 1 October 2026; shops now organise stock with one level of category only.
No product values changed: |
Stop reading category_id / category; use
main_category. Stop sending the category_id filter; use
main_category_id. |
| 2026-08-29 | First release of v1. | — |

