API writes
Writes act for a signed-in user. Sign in first: Authentication.
API=https://example.com/api/v1TOKEN=sca_… # an access token, or a personal key| Endpoint | Scope | Does |
|---|---|---|
POST /listings |
listings:write |
Post a listing. 201. |
PATCH /listings/{id} |
listings:write |
Edit your listing. |
DELETE /listings/{id} |
listings:delete |
Delete your listing. 204. |
POST /photos |
listings:write |
Upload a photo before its listing exists. 200 with a token, no Location. |
POST /listings/{id}/photos |
listings:write |
Add a photo to your listing. 201. It counts as an edit (listing.updated). |
GET /listings/{id}/photos/{photo} |
listings:read |
One photo. The Location of an added photo. |
DELETE /listings/{id}/photos/{photo} |
listings:write |
Remove a photo. 204. |
POST /listings/{id}/comments |
comments:write |
Comment on a listing. 201. |
GET /comments/{id} |
listings:read |
One comment: an approved one on a listing you can see, or your own. The Location of a new comment. |
DELETE /comments/{id} |
comments:write |
Delete your own approved comment. 204. |
GET /account/alerts |
alerts:read |
Your saved searches. |
POST /account/alerts |
alerts:write |
Save a search. |
GET /account/alerts/{id} |
alerts:read |
One saved search. The Location of a new one. |
DELETE /account/alerts/{id} |
alerts:write |
Stop a saved search. 204. |
GET /account/conversations |
conversations:read |
Your conversations, newest activity first. meta has your unread total. |
POST /account/conversations |
conversations:write |
Write to a listing’s seller. 201. |
GET /account/conversations/{id} |
conversations:read |
One of your conversations. The Location of a new one. |
GET /account/conversations/{id}/messages |
conversations:read |
Its messages, newest first, or the newer ones with after. Marks nothing read. |
POST /account/conversations/{id}/messages |
conversations:write |
Reply. 201. |
POST /account/conversations/{id}/read |
conversations:write |
Mark it read. |
POST /account/conversations/{id}/block |
conversations:write |
Block it: nobody writes in it until you unblock. |
DELETE /account/conversations/{id}/block |
conversations:write |
Lift your own block. |
POST /account/conversations/{id}/report |
conversations:write |
Report it to the admins. |
GET /cart/quote |
listings:read |
Price a cart. Needs no sign-in. |
GET /account/orders |
orders:read |
Your orders, newest first. |
POST /account/orders |
orders:write |
Place an order. 201. |
GET /account/orders/{id} |
orders:read |
One of your orders. The Location of a new one. |
POST /account/orders/{id}/pay |
orders:write |
Start paying a pending card order again; next says where to go. |
POST /account/orders/{id}/cancel |
orders:write |
Cancel a pending bank transfer or cash on delivery order. |
GET /account/cart |
cart:read |
Your saved cart: variant ids and quantities, never prices. |
PUT /account/cart |
cart:write |
Replace your saved cart. A variant that cannot be bought is dropped. Up to 50 lines, 20 of each. 204. |
POST /users |
none | Sign up, when the site allows it. 201. The password needs at least 5 characters. |
A write that is not allowed gets 403. A user who is not the owner of a live listing gets
403 not_owner. A listing the user cannot see is a 404.
What every write does
Section titled “What every write does”- Bodies are JSON. Send
Content-Type: application/json. Bodies are limited to 1 MB; photo uploads to 16 MB. An unknown member is a422, not ignored. - A
201has aLocationheader with the new resource’s address, and aGETof that address works. See Location targets. - The saved resource is in
data, in the owner’s view. warningssits next todatawhen something did not go as asked. See Warnings.- One transaction. A listing save that fails part way leaves nothing behind.
- The same rules as the web forms. Moderation, the listing limit, spam checks, bans, e-mails and plugin hooks all run, because the API calls the same code.
- Write calls also count in the
api_writebucket.
Location targets
Section titled “Location targets”Every Location can be read with GET.
| Created by | Location |
|---|---|
POST /listings |
/listings/{id} |
POST /listings/{id}/photos |
/listings/{id}/photos/{photo} |
POST /listings/{id}/comments |
/comments/{id} |
POST /account/alerts |
/account/alerts/{id} |
POST /account/orders |
/account/orders/{id} |
POST /account/keys |
/account/keys/{id} |
POST /users has no Location either: see Sign up.
POST /photos answers 200, not 201, and has no Location. A staged token is not a
resource you can read back.
Post a listing
Section titled “Post a listing”curl -i -X POST $API/listings \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6f1c2a80-4d1e-4a53-9c6b-0d9a8f3e51aa" \ -d '{ "category_id": 12, "title": "Vintage Hardcover Classics Collection", "description": "20 books in good condition.", "price": "90.00", "currency": "USD", "country": "US", "city": "Austin", "contact_phone": "+1 555 0100", "show_email": false, "custom_fields": { "4": "Blue" }, "photo_tokens": ["9f2b6c1e0a7d4e3b8c5a1d2e3f405162"] }'HTTP/1.1 201 CreatedLocation: https://example.com/api/v1/listings/412{ "data": { "id": 412, "status": "pending", "title": "Vintage Hardcover Classics Collection", "…": "…" }, "warnings": [ { "code": "listing_pending", "message": "The listing goes live once it is activated or approved." } ]}A site that sells listing slots may save the listing without making it live. Then data.status is unpaid, the listing_unpaid warning is set, and payment says what it costs. The listing is hidden from everyone but its owner until it is published. Its published_at is when it was saved and its expires_at is the payment deadline.
{ "data": { "id": 412, "status": "unpaid", "title": "Vintage Hardcover Classics Collection", "…": "…" }, "warnings": [ { "code": "listing_unpaid", "message": "The listing is saved and waits for payment. Publish it with credits or at payment.publish_url." } ], "payment": { "credits": 2, "balance": 0, "shortfall": 2, "publish_url": "https://example.com/…" }}Required: category_id, title, description.
| Member | Type | Notes |
|---|---|---|
category_id |
integer | A category of the site. |
title |
string, up to 1000 | In locale, or the site’s default language. |
description |
string, up to 20000 | The same. |
locale |
string | The language of title and description, such as en_US. Must be one of the site’s languages. |
translations |
object | {"de_DE": {"title": "…", "description": "…"}} for other languages. |
price |
string, number or null |
A decimal such as "12.50". null for no price. |
currency |
string | Three capitals, such as USD. |
country |
string | A two-letter code. Empty clears it. |
region_id, city_id |
integer or null |
Ids from GET /countries/…/regions and the like. An unknown id is a 422. |
region, city |
string | A name, when there is no id. |
city_area, address, zip |
string | |
lat, lng |
number or null |
|
contact_phone |
string, up to 45 | |
show_email |
boolean | Show the seller’s e-mail on the listing. |
custom_fields |
object | Custom field id to value. See below. |
photo_tokens |
array, up to 50 | Tokens from POST /photos, added in this order. |
photo_urls |
array, up to 20 | Web addresses the site downloads, when the site allows it. |
ext |
object | Plugin members, under the plugin’s slug. |
custom_fields are limited to the listing’s category and cleaned as the web form cleans them.
A field of another category is dropped without an error, so an edit can move the listing. An id
that is no field is a 422 with unknown, and a value of the wrong shape one with type. A date is "2026-01-31" or an
RFC 3339 date-time, and a date range is {"from": "2026-01-01", "to": "2026-01-31"}. A date that
cannot be read is a 422. null removes a value.
Plugins can change the body before it is saved with the api_listing_input filter.
What can refuse a post
Section titled “What can refuse a post”| Answer | Why |
|---|---|
422 validation_failed |
A bad member, or the web form’s own refusal: a missing field, or the posting wait. errors points at the member. |
422 listing_limit |
The user has reached the site’s listing limit and the site does not take payment at the end of posting. detail says which. Otherwise the listing is saved and payment says what it costs. |
403 banned |
The account or address is banned from posting. |
403 wrong_credential |
The site lets only signed-in users post, and the caller is not one. |
403 feature_disabled |
The site is a shop: only an admin adds listings. |
429 rate_limited |
Over the hourly cap. |
Edit a listing
Section titled “Edit a listing”PATCH sends only what changes. Everything not sent keeps its value.
curl -X PATCH $API/listings/412 \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/merge-patch+json" \ -d '{ "price": "75.00", "custom_fields": { "4": null } }'Send application/merge-patch+json or application/json. The answer repeats Accept-Patch.
An empty patch ({}) changes nothing and answers 200 with the current resource. This holds for every PATCH except PATCH /admin/areas/{id}, where name is required.
The patch applies to the write shape above, the same members POST takes, not to the
nested shape a GET answers with. Send "price": "75.00", not "price": {"amount": "75.00"}.
| In the body | Effect |
|---|---|
| A member | Replaces that value. |
null on any member that can be empty: price, the location members, contact_phone, lat, lng |
Clears it, as "" does. |
translations: {"de_DE": null} |
Removes that language. |
custom_fields: {"4": null} |
Removes that custom field value. |
translations |
Merges by locale. Other locales stay. |
photo_tokens, photo_urls |
Adds photos. It never replaces or removes any. |
The answer is the saved listing, with 200. If the site holds edited listings for review,
the status becomes disabled until an admin approves it and warnings says listing_pending.
An edit to a listing still waiting for its activation link stays pending and also says
listing_pending.
Write members and where they read back
Section titled “Write members and where they read back”A write body is flat; the listing you read back nests some of the same values. A GET
cannot be sent back as a PATCH as it is: map these members.
| Write member | Reads back as |
|---|---|
category_id |
category.id |
price, currency |
price.amount, price.currency |
country |
location.country.code |
region_id, region |
location.region.id, location.region.name |
city_id, city |
location.city.id, location.city.name |
city_area |
location.city_area.name |
address, zip, lat, lng |
location.address, location.zip, location.lat, location.lng |
contact_phone |
contact.phone |
custom_fields: {"4": "Blue"} |
custom_fields: [{"id": 4, "value": "Blue", …}] |
owner_id (admin) |
seller.id |
contact_name, contact_email (admin) |
contact.name, contact.email |
A comment’s author_name and author_email read back as author.name and author.email.
Delete a listing
Section titled “Delete a listing”curl -X DELETE $API/listings/412 -H "Authorization: Bearer $TOKEN"204, no body. The listing and its photos are removed as the web delete does it.
Publish a listing
Section titled “Publish a listing”A listing with status: "unpaid" waits for payment. POST /listings/{id}/publish puts it live. Only its owner may call it, with a credential that holds both listings:write and orders:write: either path spends money.
curl -i -X POST $API/listings/412/publish \ -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: $(uuidgen)"With an empty body the site takes the listing’s credits from the user’s balance. To buy the credits that are missing, send a package and a payment method. The site starts an order, as POST /account/orders does. Follow next to pay, then watch GET /account/orders.
{ "package": 5, "gateway": "offline" }| Answer | When |
|---|---|
200 |
The listing is published. data.status is active, or pending if the site checks new listings. |
200 |
The listing was already published. Nothing is spent. |
200 with order and next |
A package and gateway were sent. The order is started and the listing stays unpaid until it is paid. payment is set. |
422 insufficient_credits |
The balance is too low. payment says the price and the shortfall. |
403 insufficient_scope |
The credential lacks orders:write. |
404 |
Not your listing, or it does not exist. |
payment.publish_url is the site’s pay page. It needs the seller signed in on the site, so an app should use the order path instead.
A repeat with the same Idempotency-Key gets the first answer back. Starting orders is limited to 20 an hour per user: 429 rate_limited. A refused request is not counted. listing.created is sent to webhooks when the listing goes live.
Photos
Section titled “Photos”A photo is checked as the listing form checks it: a type the site accepts (JPEG, PNG, GIF or WebP), an image that decodes, within the pixel limit and the site’s largest photo size.
Upload first, then attach
Section titled “Upload first, then attach”curl -X POST $API/photos \ -H "Authorization: Bearer $TOKEN" \{ "data": { "token": "9f2b6c1e0a7d4e3b8c5a1d2e3f405162", "expires_at": "2026-10-03T11:12:00Z" } }Send the file one of two ways:
| Way | How |
|---|---|
| Multipart | multipart/form-data, the file in a field named photo. |
| Raw | The image itself as the body, with Content-Type: image/jpeg (or image/png, image/gif, image/webp). |
curl -X POST $API/photos -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: image/jpeg" --data-binary @books.jpg| Rule | Detail |
|---|---|
| Token life | Two hours. The cron removes older photos. |
| Whose | A token works only for the user who uploaded it. An unknown, expired or foreign token is a 422 on /photo_tokens/<n>. |
| Waiting | A user may hold 50 staged photos at once. The 51st is a 422. |
| Size | The site’s largest photo size, and never over 16 MB. Bigger is 413 too_large. |
| Wrong type | 415 unsupported_media_type when the request is neither multipart nor an image. A file that is not an allowed image, or has too many pixels, is 422. |
The token is used once. After the listing saves, it is gone. If the save fails, it stays valid until it expires.
Add to a listing you already have
Section titled “Add to a listing you already have”201 with the photo (id, thumbnail, preview, normal, original) and a Location
header. If the listing already holds as many photos as the site allows, it is a 422 on
/photo. If the site holds edited listings for review, adding a photo sends the listing back
to review.
curl -X DELETE $API/listings/412/photos/881 -H "Authorization: Bearer $TOKEN"204. A photo id that is not on this listing is a 404.
How many photos a listing holds
Section titled “How many photos a listing holds”The site sets a maximum number of photos per listing. When photo_tokens or photo_urls name more than the
listing has room for, the listing still saves with the first ones, and warnings carries
photo_skipped with the count.
Photos by address
Section titled “Photos by address”Off by default. The site owner switches it on with Let new listings name photos by web
address. With it off, photo_urls is a 422 on /photo_urls: upload to /photos instead.
{ "category_id": 12, "title": "…", "description": "…", "photo_urls": ["https://cdn.example.com/books.jpg"] }The site downloads each address itself:
- Only public
httpandhttpsaddresses on their usual ports. Private and local addresses are refused. - Redirects are not followed. A download stops after 15 seconds, when it stays below 1 KB a second for 5 seconds, and at the site’s largest photo size.
- Up to 4 downloads of one request run at the same time; the next starts as one finishes. All of them must finish within 30 seconds. A server without PHP’s cURL extension refuses every address.
- One bad address fails the whole request with a
422on/photo_urls/<n>. Nothing is saved. A download that fails says only that the photo could not be downloaded, and a refused address only that the site does not download from it. - Downloads happen before an
If-Matchcheck, so a412can still cost a download. A write that is refused after the downloads keeps none of them. - Each download counts toward 30 an hour per user on
PATCH. See Limits.
Comments
Section titled “Comments”curl -i -X POST $API/listings/412/comments \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "title": "Still available?", "body": "Can you ship to Canada?" }'201 with the comment and Location: …/comments/9.
{ "data": { "id": 9, "listing_id": 412, "title": "Still available?", "body": "Can you ship to Canada?", "author": { "name": "Jane Doe", "user_id": 23 }, "published_at": "2026-10-03T09:30:00Z" }, "warnings": [ { "code": "comment_pending", "message": "The comment shows once it is approved." } ]}| Rule | Detail |
|---|---|
body |
Required, 1 to 5000 characters. A body that is only tags or spaces is a 422. |
title |
Optional, up to 200. |
| Author | Taken from the account. You cannot set a name or e-mail. |
| Moderation | If the site approves comments first, or the spam check holds one, you get comment_pending. |
| Refusals | 403 when comments are off, or the account is banned. 404 when the listing is pending, disabled, spam or unpaid. |
| Delete | DELETE /comments/{id} removes your own approved comment. Someone else’s is 403; one still waiting for approval is 409. |
Saved searches
Section titled “Saved searches”A saved search mails the user new matches. It takes the filters of GET /listings.
curl -i -X POST $API/account/alerts \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "filters": { "q": "bicycle", "category": "for-sale", "price_max": 300, "with_photos": true } }'{ "data": { "id": 5, "filters": { "q": "bicycle", "category": [1], "price_max": 300, "with_photos": true }, "type": "daily", "active": true, "created_at": "2026-10-03T09:40:00Z" }}| Rule | Detail |
|---|---|
| Filters | q, category, country, region, city, city_area, user, locale, price_min, price_max, with_photos, premium, custom_field. At least one. |
| Same search again | Answers 200 with the saved one, not a second 201. |
| Limit | A user keeps at most 20 saved searches; the site owner can change it. One more is 422 validation_failed at /. The hourly alert limit in Spam and bots also applies: over it is 429 rate_limited. |
type |
How often it mails: daily or weekly. |
| List and stop | GET /account/alerts, DELETE /account/alerts/{id} (204). |
| Shown on the web | The same search appears on the account’s alerts page. |
Conversations
Section titled “Conversations”Messages between a buyer and a listing’s seller. Every call acts as the signed-in user, so a conversation you are not part of is a 404.
curl -i -X POST $API/account/conversations \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "listing": 412, "body": "Is the bike still for sale?" }'201 with the message and Location: …/account/conversations/31. A second message about the same listing goes into the conversation already there: still 201 with the new message, and the Location of that conversation.
{ "data": { "id": 88, "conversation_id": 31, "sender_id": 23, "body": "Is the bike still for sale?", "held": false, "created_at": "2026-10-23T09:30:00Z" }}Reply in a conversation:
curl -i -X POST $API/account/conversations/31/messages \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "body": "Yes, it is." }'List and poll. GET /account/conversations carries meta.unread and meta.latest_at, so limit=1 is a cheap check for news. Messages come newest first and page with cursor. To poll, pass after with the last message id you hold: you get the newer ones, oldest first, and no cursor.
curl "$API/account/conversations/31/messages?after=88" -H "Authorization: Bearer $TOKEN"Reading never marks anything read, so an app syncing in the background does not clear the badge. Call POST …/read when the person has seen the thread.
curl -X POST $API/account/conversations/31/read -H "Authorization: Bearer $TOKEN"curl -X POST $API/account/conversations/31/block -H "Authorization: Bearer $TOKEN"curl -X DELETE $API/account/conversations/31/block -H "Authorization: Bearer $TOKEN"curl -X POST $API/account/conversations/31/report \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{ "reason": "spam" }'read, block, DELETE block and report answer 200 with the conversation as it is now.
| Rule | Detail |
|---|---|
listing |
Required, a live listing. |
body |
Required, up to the site’s Longest message setting (5000 characters by default). |
reason |
One of spam, scam, offensive, other. Reporting also blocks the conversation for you. |
| State | A conversation is open, held, closed or reported. blocked_by_me and blocked_by_them say who blocked. |
| Moderation | If the site holds the message for review, you get 201 with held: true and the warning message_held. The other person sees it once an admin releases it. |
| Webhooks | message.created and conversation.reported carry ids only, never the text. See Webhooks. |
What a refusal means:
| Status | Meaning |
|---|---|
403 feature_disabled |
Messages are switched off on this site, or the account is not live. |
403 wrong_credential |
The call needs a signed-in user. |
403 forbidden |
The other person blocked the conversation. |
403 not_owner |
DELETE .../block on a block the other person set. Only they can lift it. |
403 banned |
The e-mail address or IP address is banned. |
404 not_found |
No such live listing, or a conversation you are not part of. A seller gets 404 for a thread still held for review. |
409 conflict |
Your own listing, a listing with no seller, a closed, held or reported conversation, or a report on one already reported. detail says which. |
422 validation_failed |
Empty or too long body, or an unknown reason. A body the site’s spam, link or word rules refuse has the field code rejected. |
429 rate_limited |
Over the hourly limit. See Limits. |
Wake an app for new messages
Section titled “Wake an app for new messages”An app that gets its own push endpoint can register it. The push carries no text; the app asks GET /account/conversations what is new.
curl -X PUT $API/account/push-subscriptions \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "endpoint": "https://fcm.googleapis.com/fcm/send/abc" }'200 with { "data": { "browser": "…", "expires_at": "…" } }. Register it again before expires_at (90 days) to keep it. Stop with DELETE /account/push-subscriptions/{browser} (204). There is no list. Needs the conversations:write scope.
| Status | Meaning |
|---|---|
404 not_found |
Browser notifications are switched off on this site, or on DELETE the browser is not yours. |
422 validation_failed |
endpoint is empty, longer than 1024 bytes, not https://, or not a public address. |
Cart and orders
Section titled “Cart and orders”The request names variants and quantities. Prices, delivery, fees, totals and stock are worked out on the server; an amount in the body is ignored.
curl "$API/cart/quote?lines=12:2,15:1&gateway=offline"lines is variant:qty pairs, at most 50, a quantity of 1 to 20. A variant that is gone, hidden or out of stock does not fail the call: it is listed in problems (not_buyable, out_of_stock, qty) with its position in lines, and an out-of-stock line stays out of the totals. shipping picks a delivery method from shipping_methods; gateway adds that payment method’s fee. Every amount is {amount, currency, formatted}. A quote is never cached.
Before checkout a shopper may add country (two letters), state (up to 100 characters), postcode (up to 20) and coupon (letters, digits and -, up to 40, read in upper case). Delivery, tax and discount plugins use them; a value that does not fit is left out, not refused.
| Field | Detail |
|---|---|
needs_shipping |
false when every line is a download: no address, delivery or cash on delivery. |
free_over |
{amount, remaining}: free delivery from this subtotal and what is left to reach it, or null. A delivery plugin may set its own. |
taxes[].included |
true for tax already in the prices: shown, not added to total. |
totals.tax / totals.tax_included |
Tax added on top of the prices, and tax already in them. |
problems |
Each has line, code, pointer and message. A line problem has a line and a null pointer. A plugin’s problem has line null, a pointer (/coupon, /address/postcode, /address/state, /address/country or /shipping_method) and a message to show the shopper. |
curl -i -X POST $API/account/orders \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{ "lines": [{ "variant": 12, "qty": 2 }], "shipping_method": "standard", "gateway": "offline", "coupon": "WELCOME10", "address": { "name": "Tom Lee", "phone": "5550100", "line1": "1 Main St", "city": "Austin", "state": "TX", "postcode": "73301", "country": "US" } }'The 201 has the order in data and where to pay in next: { "kind": "redirect", "payload": "<url>" } to send the buyer to, or { "kind": "html", "payload": "<markup>" }, the gateway’s own page to print as it is.
| Rule | Detail |
|---|---|
| Who | A signed-in user. A guest page token can price a cart but not place one. |
| Refused | 422 with a pointer: /lines/n (out_of_stock, not_buyable), /shipping_method (unknown), /gateway (unavailable, too_many_open), /address/<field> (required, maxLength). A plugin’s problem (a bad coupon, a postcode outside every delivery zone) is rejected at its pointer, with its message. Nothing is written. |
coupon |
Optional, the same rule as on the quote. |
| Included tax | A tax line already in the prices is stored with amount 0, unit the tax and included true, so the lines still add up to total. |
| Retry | Send an Idempotency-Key: a repeat gets the first order back and does not ask the gateway again. |
| Limit | 20 orders an hour per user: 429 rate_limited. A refused request (422) is not counted. |
status |
pending, paid, shipped, delivered, failed, refunded, cancelled. |
| Lines | kind is item, shipping, tax, discount or fee; a discount’s amount is negative. listing links the live listing and is null once it is gone. |
| Cancel | POST /account/orders/{id}/cancel, only while can_cancel is true: a pending bank transfer or cash on delivery order. 200 with the order, else 409 conflict. |
| Pay again | A pending card order has can_pay true instead. POST /account/orders/{id}/pay asks the gateway again and answers 200 with the order and a new next; any other order is 409 conflict. |
| Other users | Another user’s order is 404. |
Sign up
Section titled “Sign up”Off by default. The site owner switches on Allow sign-up through the API, and the site must have registration on. The password needs at least 5 characters.
curl -i -X POST $API/users -H "Content-Type: application/json" \{ "data": { "confirmed": false } }confirmed is false until the link in the activation e-mail is opened. The user cannot sign in
before that: the sign-in answers as for a wrong password. An e-mail that already has an account
gets the same 201 and body, and no account is made, so sign-up does not tell which addresses
are taken. A username sent with a taken
e-mail is held as a new account would hold it, so a later sign-up meets it taken either way.
Optional: username, phone_land, phone_mobile. A site that is off answers
403 feature_disabled. A banned e-mail or address answers 403 banned.
The sign-up e-mails are sent from the job queue, so they arrive when cron next runs. This keeps a new address from answering slower than a taken one. The queue holds only the account id for the activation e-mail; its link is made when the job runs.
The hiding needs the activation e-mail. If the site switches off Users need to validate their
account, a new account could sign in at once and a taken e-mail could not, so the hiding is off:
a taken e-mail is 422 with “The specified e-mail is already in use”, and the new account’s
e-mails go out at once.
There is no captcha to show, so sign-up is limited: 5 per address and 100 for the whole site an hour (the site’s number is a setting). Only a sign-up that passed its checks counts for the site. If the site cannot count, it refuses.
Editing safely with If-Match
Section titled “Editing safely with If-Match”To avoid overwriting a change made meanwhile, send the ETag of your last GET in
If-Match on a PUT, PATCH or DELETE. If the resource changed you get 412 precondition_failed.
If-Match: *only checks that the resource exists. If it is gone, the write answers as itsGETdoes (404) and runs nothing.- A tag a proxy made weak (
W/"…") still matches. - A caller who can
PATCHorDELETEa listing, comment, photo, the account, a user, key, saved search, category, field, currency, location, webhook or the settings gets anETagthat holds the stored version ("<version>.<hash>"). AnyGETof the path works, whatever itsfields,includeorlocale. For a listing or comment that means its owner, or an admin key. A listing’s version covers its texts, location, custom fields and photos. - Other callers get a plain hash of the body. Use the tag of the plain
GET, with nofieldsorinclude. - The check and the write run as one, so no other write can land between them.
- A
PATCHsent withIf-Matchanswers with the newETag, ready for the next edit. - A credential that cannot read the path’s
GETis refused, as the check cannot be made. A resource it cannot see answers404, never412. - Without
If-Matchthe write is never refused.
Retrying safely with Idempotency-Key
Section titled “Retrying safely with Idempotency-Key”A network drop after a POST leaves you not knowing if it worked. Send an Idempotency-Key
and retry the same call: the server runs it once.
curl -X POST $API/listings -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: 6f1c2a80-4d1e-4a53-9c6b-0d9a8f3e51aa" … # same call, any number of times| Rule | Detail |
|---|---|
| Where | POST, PUT, PATCH, DELETE with any credential. Anonymous calls ignore it. |
| Key | 1 to 255 visible ASCII characters. A random UUID is fine. Otherwise 400 invalid_header. |
| Kept | 24 hours. Keys belong to one sign-in (across its refreshed tokens) or one key. Another user cannot use yours. |
| Replay | The same status, headers and body come back, plus Idempotency-Replayed: true. |
| Different request | The same key with another method, path, query, If-Match or body is 422 idempotency_key_reused. |
| Still running | The same key while the first call runs is 409 idempotency_in_flight, with Retry-After: 1. A call that died is retried only after the longest a PHP request may run. |
| Errors | A 5xx or 429 is not kept: retry runs again. Any other 4xx is kept and replays. Fix the request and use a new key. |
| Not covered | POST /auth/token, POST /account/password and POST /account/keys. They carry secrets, so they never replay. |
Limits
Section titled “Limits”| What | Limit | Over it |
|---|---|---|
| Requests | The usual limits, and 30 writes a minute | 429 rate_limited |
| New listings | Per user an hour: the site’s setting, or 30 when it is 0 (10 while the listing form asks for a captcha). Per address: three times that. |
429 rate_limited |
| Comments | 20 an hour per user, counted with the ones they post on the site’s comment form | 429 rate_limited |
| Photos fetched by address | 30 an hour per user by default (a setting), counted on PATCH |
429 rate_limited |
| E-mail changes on the account | 5 an hour per user | 429 rate_limited |
| Saved searches | The site’s hourly alert limit, counted with the web form | 429 rate_limited |
| Staged photos waiting | 50 per user | 422 on /photo |
photo_tokens / photo_urls in one body |
50 / 20 | 422 validation_failed |
| Payments started for a listing | 20 an hour per user, with the orders above | 429 rate_limited |
| Messages | 30 an hour per user for replies (throttle_message_send), 10 an hour for new conversations (throttle_message_start). Both are in Spam and bots and count with the site’s own message forms. |
429 rate_limited |
| Sign-ups | 5 an hour per address; 100 for the site by default (a setting), counting only those that passed their checks | 429 rate_limited |
Each 429 has Retry-After. The site owner sets the listing number: Set up the REST API.
Account
Section titled “Account”| Endpoint | Scope | Does |
|---|---|---|
GET /account |
account:read |
Your profile, in the owner’s view. |
PATCH /account |
account:write |
Edit the profile: name, website, phone_land, phone_mobile, country, region_id, city_id, city_area, address, zip, lat, lng, is_company, email (with current_password). A website that is not a web address is a 422. |
GET /account/listings |
account:read |
Your listings in any status, newest first, as your listings page shows them. Filters: status (active, pending, unpaid, disabled, expired, spam), limit, cursor, count. |
POST /account/password |
account:write |
Change the password. The new one needs at least 5 characters, as on the site’s forms; shorter is a 422 on /new_password. |
GET /account/sessions |
account:read |
Your live sign-ins. Keys are at /account/keys. |
DELETE /account/sessions/{id} |
account:write |
End one sign-in. 204. |
GET, POST, DELETE /account/keys |
account:write |
Personal keys. |
A new email needs current_password in the same body, or the answer is a 422 on
/current_password. It is not applied at once. A confirmation link goes to the new address, and
the answer carries the warning email_confirmation_sent. The e-mail changes when the link is opened.
Warnings
Section titled “Warnings”warnings is a list of { "code", "message" }. A warning is not an error: the write worked.
| Code | On | Meaning |
|---|---|---|
listing_pending |
POST, PATCH /listings |
The listing is not live yet. It needs approval or activation. |
photo_skipped |
POST, PATCH /listings |
Some photos were not added: the listing holds as many as it may. |
comment_pending |
POST …/comments |
The comment shows once it is approved. |
email_confirmation_sent |
PATCH /account |
A link went to the new address. |
listing_unpaid |
POST, PATCH /listings |
The listing is saved but not live: it waits for payment. payment says what it costs and where to pay. |
message_held |
POST /account/conversations, POST …/messages |
The message waits for review before the other side sees it. |
Errors are in API errors.