Oase Public API
The Oase Public API lets your own systems manage your shirts on Oase: add shirts to your locker, put them up for sale or take them off again, and follow where each one stands and what your sales went for.
It works for private sellers and for shops. A key always belongs to one account, either you or a shop you're an admin of, and only ever works with that account's shirts and sales. Shirts created with a shop's key belong to the shop; Oase records them as added by the admin who created the key.
Getting started
- Create a key. For your own account, go to Account settings. For a shop, switch to the shop and open its Shop page (shop admins only). You'll also find both under Settings → Developers in the account menu. Give the key a name you'll recognise, such as "Inventory sync".
- Copy it straight away. The key is shown once. If you lose it, revoke it and create a new one.
- Make a request:
curl https://public-api.oase.ai/v1/me \
-H "Authorization: Bearer oase_pk_..."
{ "type": "shop", "name": "Test Shop" }
Authentication
Send the key on every request, in either header:
Authorization: Bearer oase_pk_...
X-API-Key: oase_pk_...
Keep keys on your server. Never put one in a website, an app or a public repository: anyone who has it can read your shirts and sales, and list or unlist your shirts. If a key may have leaked, revoke it in Oase; it stops working immediately.
Base URL: https://public-api.oase.ai
Endpoints
| Method | Path | Does |
|---|---|---|
GET | /v1/me | Who the key belongs to |
GET | /v1/items | Your shirts, newest first |
GET | /v1/items/:id | One of your shirts |
POST | /v1/images | Uploads a photo for a new shirt |
GET | /v1/clubs?search= | Finds clubs |
GET | /v1/players?search= | Finds players |
GET | /v1/seasons | Lists seasons |
POST | /v1/items | Adds a shirt to your locker |
POST | /v1/items/:id/listing | Puts a shirt up for sale |
POST | /v1/items/:id/unlist | Takes a shirt off the market |
GET | /v1/sales | Your paid sales, newest first |
GET | /v1/sales/:id | One of your sales |
Items
GET /v1/items lists the shirts you own, each with where it stands and its
listing, if it has one. A shirt that sells moves to its buyer, so it leaves
this list and shows up under sales instead.
{
"data": [
{
"id": "cmgx1b2c30001",
"title": "Erik Huseklepp Brann 2007/08 home",
"club": "Brann",
"season": "2007/08",
"player": "Erik Huseklepp",
"kitType": "home",
"size": "l",
"condition": 9,
"verified": true,
"createdAt": "2026-10-04T15:08:04.701Z",
"status": "in_auction",
"listingType": "auction",
"price": 500,
"currentBid": 650,
"bidCount": 2,
"currency": "NOK",
"listedAt": null,
"auctionStartsAt": "2026-10-08T15:00:00.000Z",
"auctionEndsAt": "2026-10-09T15:00:00.000Z"
}
],
"nextCursor": null
}
price is the fixed price, or an auction's start price. currentBid and
bidCount are only set for auctions; listedAt only for fixed price.
status | Meaning |
|---|---|
in_locker | Not for sale (also an auction that ended without bids) |
waiting_for_verification | Listed, waiting for Oase to check the shirt |
waiting_for_approval | Oase adjusted the listing; waiting for you to accept |
for_sale | Fixed price, can be bought now |
auction_scheduled | Auction set up, not started yet |
in_auction | Auction running |
awaiting_payment | Sold, waiting for the buyer's payment |
Adding a shirt
A new shirt needs at least two photos and the ids of its club and season (and player, if it has a name on the back). It goes into your locker, not for sale yet.
1. Upload the photos, one per request, front first. Send the file as
multipart/form-data in a field named file, or as the raw body with its
Content-Type. JPEG, PNG or WebP, at most 10 MB and 40 megapixels, at
least 400 pixels on each side. Oase turns photos upright (phone photos often
arrive sideways), scales them down and answers with the photo's URL:
curl -X POST https://public-api.oase.ai/v1/images \
-H "Authorization: Bearer oase_pk_..." \
-F "file=@front.jpg"
{ "url": "https://oasepbassets.blob.core.windows.net/images/0b6f...-api__thumbed.jpg" }
2. Look up the club, season and player. Each search returns up to 25
matches, best first, as { "data": [{ "id": "...", "name": "Brann" }] }.
search needs at least two letters for clubs and players; seasons can be
listed without it.
curl "https://public-api.oase.ai/v1/clubs?search=brann" \
-H "Authorization: Bearer oase_pk_..."
The API only uses clubs, players and seasons Oase already knows. If one is missing, add the shirt in the app, or leave the player out and mention them in the description.
3. Create the shirt:
curl -X POST https://public-api.oase.ai/v1/items \
-H "Authorization: Bearer oase_pk_..." \
-H "Content-Type: application/json" \
-d '{
"clubId": "cmgx0brann001",
"seasonId": "cmgx0season0708",
"playerId": "cmgx0huseklepp",
"kitType": "home",
"size": "l",
"condition": 9,
"description": "Match worn, signed on the back",
"images": ["https://oasepbassets.blob.core.windows.net/images/0b6f...-api__thumbed.jpg", "..."]
}'
| Field | Notes |
|---|---|
clubId, seasonId | Required, from the searches above |
playerId | Optional |
kitType | home, away or third |
size | xs, s, m, l, xl, xxl or xxxl |
condition | 1 (worn) to 10 (as new) |
description | Optional, up to 2000 characters |
images | 2 to 12 URLs from POST /v1/images, front first |
The answer is the new shirt (201), in the same shape as GET /v1/items/:id,
with status in_locker. Fields Oase doesn't know are rejected rather than
ignored, so a typo shows up as a 400.
Putting a shirt up for sale
POST /v1/items/:id/listing lists a shirt from your locker at a fixed price
or as an auction. Prices are whole kroner (or your currency's whole units).
curl -X POST https://public-api.oase.ai/v1/items/cmgx1b2c30001/listing \
-H "Authorization: Bearer oase_pk_..." \
-H "Content-Type: application/json" \
-d '{ "type": "fixed_price", "price": 1500 }'
An auction takes its start price, end and, optionally, when it starts:
{
"type": "auction",
"startPrice": 500,
"bidIncrement": 50,
"startsAt": "2026-10-20T18:00:00+02:00",
"endsAt": "2026-10-27T20:00:00+01:00",
"antiSnipeSeconds": 300
}
| Field | Notes |
|---|---|
type | fixed_price or auction |
price | Fixed price only, required |
startPrice | Auction only, required: the opening bid |
bidIncrement | Auction only: the smallest raise between bids (default 50) |
startsAt | Auction only: when it starts, at most 60 days ahead. Leave it out to start now |
endsAt | Auction only, required: 1 hour to 30 days after the start |
antiSnipeSeconds | Auction only: a bid this close to the end extends the auction by this much, 30-600 (default 300) |
Dates are ISO 8601 with a time zone, such as 2026-10-20T18:00:00Z or
2026-10-20T20:00:00+02:00.
The answer is the shirt (201). Oase checks every shirt before it's sold the
first time: until then its status is waiting_for_verification, and it
goes on sale by itself once approved. Shirts Oase has already verified go on
sale straight away.
To sell, your account needs the same as in the app: payouts set up, your
country, a phone number and a sender address for shipping. If something is
missing you get 403 with cannot_sell and a message saying what to add. A
shirt can only have one listing at a time: listing one that's already
listed gives 409 with already_listed.
Taking a shirt off the market
POST /v1/items/:id/unlist ends the listing or auction and puts the shirt
back in your locker, as long as nobody has a claim on it yet. The answer is
the shirt, back to in_locker. Otherwise you get 409 with:
error | Why |
|---|---|
has_bids | The auction has bids |
checkout_in_progress | A buyer is paying, or has paid |
not_listed | The shirt isn't listed |
Sales
GET /v1/sales lists your paid sales, with what the buyer paid and how the
parcel is getting on.
{
"data": [
{
"id": "cmgx9z8y70004",
"itemId": "cmgx1b2c30002",
"title": "Brann 2007/08 home",
"club": "Brann",
"season": "2007/08",
"player": null,
"kitType": "home",
"saleType": "fixed_price",
"price": 1500,
"currency": "NOK",
"soldAt": "2026-10-02T15:08:04.703Z",
"shippingStatus": "delivered"
}
],
"nextCursor": null
}
shippingStatus | Meaning |
|---|---|
awaiting_shipment | Paid; no shipping code yet |
booked | Shipping code ready; not handed in yet |
in_transit | Bring has the parcel |
delivered | Delivered or collected |
Prices are in whole units of currency (kroner for NOK). Dates are ISO 8601
in UTC.
Paging
Lists return up to 50 results by default. Ask for between 1 and 100 with
limit. When there is more, nextCursor is set: pass it as cursor to get
the next page. It is null on the last page.
curl "https://public-api.oase.ai/v1/items?limit=100&cursor=cmgx1b2c30001" \
-H "Authorization: Bearer oase_pk_..."
Rate limits
Each key can make 60 requests a minute, reads and writes together. Every response tells you where you stand:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1791471600
Over the limit you get 429 with a Retry-After header (seconds). Reset is a
Unix timestamp.
Errors
Errors are JSON with a stable error code and a readable message:
{ "error": "invalid_api_key", "message": "Send a valid API key as `Authorization: Bearer <key>`." }
| Status | error | When |
|---|---|---|
400 | invalid_request | A parameter or field is invalid; the message names it |
400 | invalid_photo | The photo isn't JPEG, PNG or WebP, or is too large or too small |
401 | invalid_api_key | Missing, unknown or revoked key |
403 | cannot_sell | Your account isn't ready to sell yet; the message says what to add |
403 | account_blocked | The account behind the key can't add shirts |
404 | not_found | No such endpoint, or not one of your shirts or sales |
409 | already_listed, has_bids, checkout_in_progress, not_listed | The shirt can't be listed or unlisted right now |
429 | rate_limited | Too many requests; wait for Retry-After |
500 | internal_error | Something went wrong on our side |
503 | uploads_unavailable | Photos can't be stored right now; try again |
Request log
To keep Oase safe and to help when something goes wrong, Oase keeps a log of the requests made with your keys for 90 days: when, which endpoint, the result, the IP address and the user agent. It never stores what you sent or what the API answered.
What the API never returns
To keep buyers safe if a key leaks, the API never includes anything about your buyers: no names, addresses or contact details, and no tracking numbers (a parcel's tracking page shows where it is going). Responses also leave out photos.
Reference
The full, interactive reference with every field is at public-api.oase.ai/docs.
Questions or ideas? Write to hei@oase.ai.