curl --request POST \
--url https://{host}/v1/conversations \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/vnd.api+json' \
--data '
{
"data": {
"type": "conversations",
"attributes": {
"message": "Is this one still available, and would you consider an offer?"
},
"relationships": {
"watch": {
"data": {
"type": "watches",
"id": "91827"
}
}
}
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/vnd.api+json'},
body: JSON.stringify({
data: {
type: 'conversations',
attributes: {message: 'Is this one still available, and would you consider an offer?'},
relationships: {watch: {data: {type: 'watches', id: '91827'}}}
}
})
};
fetch('https://{host}/v1/conversations', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{host}/v1/conversations",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'data' => [
'type' => 'conversations',
'attributes' => [
'message' => 'Is this one still available, and would you consider an offer?'
],
'relationships' => [
'watch' => [
'data' => [
'type' => 'watches',
'id' => '91827'
]
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/vnd.api+json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}import requests
url = "https://{host}/v1/conversations"
payload = { "data": {
"type": "conversations",
"attributes": { "message": "Is this one still available, and would you consider an offer?" },
"relationships": { "watch": { "data": {
"type": "watches",
"id": "91827"
} } }
} }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/vnd.api+json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"data": {
"type": "conversations",
"id": "3312",
"attributes": {
"side": "seller",
"unread": true,
"archived": false,
"noReplyNeeded": false,
"buyer": {
"name": "Jens Urlbauer",
"initials": "JU",
"estimatedCountry": "DE"
},
"blocked": false,
"blockedByYou": false,
"purpose": "propose_price",
"lastMessage": {
"id": "88120",
"excerpt": "Price proposal",
"from": "seller",
"mine": false,
"createdAt": "2026-09-21T09:30:11+02:00",
"removed": false,
"priceProposal": {
"amount": 1300000,
"currency": "EUR",
"from": "seller",
"status": "pending",
"open": true,
"payable": true,
"generatedBody": true
}
},
"agreedPrice": 1300000,
"order": null,
"createdAt": "2026-09-20T10:00:00.000000Z",
"updatedAt": "2026-09-21T07:30:11.000000Z"
},
"relationships": {
"watch": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/watch"
}
},
"seller": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/seller"
}
},
"messages": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/messages"
}
}
},
"links": {
"self": "https://api.wristler.eu/v1/conversations/3312"
}
}
}{
"data": {
"type": "conversations",
"id": "3312",
"attributes": {
"side": "seller",
"unread": true,
"archived": false,
"noReplyNeeded": false,
"buyer": {
"name": "Jens Urlbauer",
"initials": "JU",
"estimatedCountry": "DE"
},
"blocked": false,
"blockedByYou": false,
"purpose": "propose_price",
"lastMessage": {
"id": "88120",
"excerpt": "Price proposal",
"from": "seller",
"mine": false,
"createdAt": "2026-09-21T09:30:11+02:00",
"removed": false,
"priceProposal": {
"amount": 1300000,
"currency": "EUR",
"from": "seller",
"status": "pending",
"open": true,
"payable": true,
"generatedBody": true
}
},
"agreedPrice": 1300000,
"order": null,
"createdAt": "2026-09-20T10:00:00.000000Z",
"updatedAt": "2026-09-21T07:30:11.000000Z"
},
"relationships": {
"watch": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/watch"
}
},
"seller": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/seller"
}
},
"messages": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/messages"
}
}
},
"links": {
"self": "https://api.wristler.eu/v1/conversations/3312"
}
}
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}Start thread
Scope: messages:write
Takes the watch it is about and the first thing to say. The seller is not named: a watch belongs to one business, so naming them would be a second way to say the same thing and a way to disagree.
message is required, at least six characters and at most 5000. It does not come back
as an attribute — it becomes the first message in the thread, which is where it is
read back from. Links to anywhere but Wristler are taken out of it before it is
stored, exactly as on the website.
Only about a watch that is for sale at a shop that can take payment, and not one listed
by a business you work at — the watch page offers a message in neither case. To write
in a thread that already exists about a watch that has since sold, use
POST /v1/conversations/{id}/messages.
priceProposal: { amount } in cents opens the thread with the buyer’s offer instead,
held to exactly the rules of a buyer’s offer sent with POST .../messages: a business
that takes offers, the seller’s floor (minimumProposal on the watch), no
shippingCosts (422, pointer /data/attributes/priceProposal/shippingCosts — shipping
is the shop’s to set) and one open offer at a time. message is then optional: send it
and those are the words beside the offer, leave it out and the website’s own sentence
is used. A refused offer writes nothing — no thread is left behind.
requestAvailability: true is the website’s “Request availability”, for a
watch with availabilityOnRequest: true. It takes no message and no
priceProposal (422 on the field): the thread opens with the website’s own
sentence asking whether the watch is available, signed with the buyer’s name. A
watch that is not on request is 422 availability_not_on_request.
attachmentHandles puts files on the opening message, exactly as on
POST .../messages: handles from POST /v1/uploads, four at most, in our own
storage. Only with a message — beside a priceProposal or requestAvailability
it is a 422 on /data/attributes/attachmentHandles. Ask for
include=messages,messages.attachments to have the opening message and its files
in the answer.
It is an upsert. There is one thread per buyer, seller and watch, so asking
twice does not make a second one: a new thread answers 201 with a Location,
one that was already open answers 200 without, and either way the message is
added and the thread is un-archived for both sides. Answering 409 instead
would push that bookkeeping onto every client, and each of them would get it
wrong differently.
| Refusal | Status | code |
|---|---|---|
| A watch that is sold, offline or withdrawn, or at a shop that cannot take payment | 422 | watch_not_for_sale |
| A watch listed by a business the caller works at | 422 | cannot_message_your_own_business |
requestAvailability on a watch that is not on request | 422 | availability_not_on_request |
An opening offer to a business that takes none, or on a watch that takes none (minimumProposal: null) | 409 | offers_not_accepted |
| An opening offer below the seller’s floor | 422 | below_minimum |
An opening offer while the buyer already has one waiting on this watch (meta.offer is its message id) | 409 | offer_already_open |
| The buyer blocked this business, the business blocked the buyer, or the account is blocked | 403 | conversation_blocked |
curl --request POST \
--url https://{host}/v1/conversations \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/vnd.api+json' \
--data '
{
"data": {
"type": "conversations",
"attributes": {
"message": "Is this one still available, and would you consider an offer?"
},
"relationships": {
"watch": {
"data": {
"type": "watches",
"id": "91827"
}
}
}
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/vnd.api+json'},
body: JSON.stringify({
data: {
type: 'conversations',
attributes: {message: 'Is this one still available, and would you consider an offer?'},
relationships: {watch: {data: {type: 'watches', id: '91827'}}}
}
})
};
fetch('https://{host}/v1/conversations', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{host}/v1/conversations",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'data' => [
'type' => 'conversations',
'attributes' => [
'message' => 'Is this one still available, and would you consider an offer?'
],
'relationships' => [
'watch' => [
'data' => [
'type' => 'watches',
'id' => '91827'
]
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/vnd.api+json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}import requests
url = "https://{host}/v1/conversations"
payload = { "data": {
"type": "conversations",
"attributes": { "message": "Is this one still available, and would you consider an offer?" },
"relationships": { "watch": { "data": {
"type": "watches",
"id": "91827"
} } }
} }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/vnd.api+json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"data": {
"type": "conversations",
"id": "3312",
"attributes": {
"side": "seller",
"unread": true,
"archived": false,
"noReplyNeeded": false,
"buyer": {
"name": "Jens Urlbauer",
"initials": "JU",
"estimatedCountry": "DE"
},
"blocked": false,
"blockedByYou": false,
"purpose": "propose_price",
"lastMessage": {
"id": "88120",
"excerpt": "Price proposal",
"from": "seller",
"mine": false,
"createdAt": "2026-09-21T09:30:11+02:00",
"removed": false,
"priceProposal": {
"amount": 1300000,
"currency": "EUR",
"from": "seller",
"status": "pending",
"open": true,
"payable": true,
"generatedBody": true
}
},
"agreedPrice": 1300000,
"order": null,
"createdAt": "2026-09-20T10:00:00.000000Z",
"updatedAt": "2026-09-21T07:30:11.000000Z"
},
"relationships": {
"watch": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/watch"
}
},
"seller": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/seller"
}
},
"messages": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/messages"
}
}
},
"links": {
"self": "https://api.wristler.eu/v1/conversations/3312"
}
}
}{
"data": {
"type": "conversations",
"id": "3312",
"attributes": {
"side": "seller",
"unread": true,
"archived": false,
"noReplyNeeded": false,
"buyer": {
"name": "Jens Urlbauer",
"initials": "JU",
"estimatedCountry": "DE"
},
"blocked": false,
"blockedByYou": false,
"purpose": "propose_price",
"lastMessage": {
"id": "88120",
"excerpt": "Price proposal",
"from": "seller",
"mine": false,
"createdAt": "2026-09-21T09:30:11+02:00",
"removed": false,
"priceProposal": {
"amount": 1300000,
"currency": "EUR",
"from": "seller",
"status": "pending",
"open": true,
"payable": true,
"generatedBody": true
}
},
"agreedPrice": 1300000,
"order": null,
"createdAt": "2026-09-20T10:00:00.000000Z",
"updatedAt": "2026-09-21T07:30:11.000000Z"
},
"relationships": {
"watch": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/watch"
}
},
"seller": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/seller"
}
},
"messages": {
"links": {
"related": "https://api.wristler.eu/v1/conversations/3312/messages"
}
}
},
"links": {
"self": "https://api.wristler.eu/v1/conversations/3312"
}
}
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}Authorizations
Headers
Optional. A UUID per logical operation, not per attempt: a retry carrying the same key gets the first attempt's answer back, headers and all, rather than performing the write twice. Honoured for 24 hours.
A refusal is an answer. A 422 is remembered like any other, so sending a
corrected body under the same key answers 422 idempotency_key_reuse rather
than accepting the correction — the point of a key is that one key means one
decision. Use a new key for a new attempt at the request, and keep the old one
only for retrying the same bytes after a timeout.
A 5xx is the exception: nothing was decided, so the key is released and the
same one may be retried.
| Refusal | Status | code |
|---|---|---|
| The same key with a different body | 422 | idempotency_key_reuse |
| The first request with this key is still running | 409 | idempotency_in_progress |
| The key's record vanished between two requests: retry with a new key | 409 | idempotency_conflict |
| A key longer than 191 characters | 400 | idempotency_key_invalid |
A replayed answer carries Idempotent-Replay: true.
191