curl --request POST \
--url https://{host}/v1/me/deletion \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/vnd.api+json' \
--data '
{
"data": {
"type": "users"
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/vnd.api+json'},
body: JSON.stringify({data: {type: 'users'}})
};
fetch('https://{host}/v1/me/deletion', 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/me/deletion",
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' => 'users'
]
]),
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/me/deletion"
payload = { "data": { "type": "users" } }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/vnd.api+json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"jsonapi": {
"version": "1.0"
},
"meta": {
"sentTo": "jeffrey@example.test",
"expiresInMinutes": 15,
"reasons": [
{
"id": "no_longer_needed",
"label": "I no longer need an account",
"takesNote": false
},
{
"id": "bought_elsewhere",
"label": "I bought a watch elsewhere",
"takesNote": false
},
{
"id": "too_many_emails",
"label": "I get too many emails or notifications",
"takesNote": false
},
{
"id": "privacy",
"label": "Privacy concerns",
"takesNote": false
},
{
"id": "bad_experience",
"label": "I had a bad experience",
"takesNote": false
},
{
"id": "other",
"label": "Other",
"takesNote": true
}
]
}
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}{
"jsonapi": {
"version": "1.0"
},
"errors": [
{
"status": "409",
"code": "account_has_open_orders",
"title": "Conflict",
"detail": "You have an order that is not finished yet. We cannot delete your account while somebody is waiting to send you a watch, or waiting to be paid.",
"meta": {
"orders": [
"202600123"
]
}
}
]
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}Request deletion code
Scope: account:write. The Wristler app only.
Step one of deleting an account, which App Store guideline 5.1.1(v) requires the
app to be able to do. We mail a six-digit code; DELETE /v1/me spends it.
A token is not enough on its own. A phone taken from somebody is a phone with a signed-in app on it, and the mailbox is the one thing whoever took it does not have — so the code goes to the address, and the mail also says that ignoring it is enough to stop the deletion.
The refusals below are checked here as well as at the deletion, so nobody types
a code we were never going to accept. meta.sentTo is the address it went to,
shown back so somebody watching the wrong mailbox can see why nothing arrived.
meta.reasons is the list of reasons DELETE /v1/me takes, labelled in the
Accept-Language of the request, with takesNote on the one (other) that keeps a
reasonNote. Draw the optional “why are you leaving” picker from it rather than
from a list of your own.
Five codes an hour. Past that, 429 too_many_attempts with Retry-After.
account_has_open_orders carries meta.orders: the order numbers in the way, as
strings, so a client can point somebody at them rather than saying “you have an
order”. They are the caller’s own orders, so nothing there is anything they could
not already list.
Why account:write and not account-security:write. That scope is for what
protects an account — the second factor and the recovery code — and every endpoint
under it asks for the password again on top. Closing an account is not protecting
it, and what guards this is the mailed code rather than a password. Both scopes are
our app’s alone, so nothing is widened either way; asking for the security scope
would only make the consent screen describe something this does not do.
curl --request POST \
--url https://{host}/v1/me/deletion \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/vnd.api+json' \
--data '
{
"data": {
"type": "users"
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/vnd.api+json'},
body: JSON.stringify({data: {type: 'users'}})
};
fetch('https://{host}/v1/me/deletion', 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/me/deletion",
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' => 'users'
]
]),
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/me/deletion"
payload = { "data": { "type": "users" } }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/vnd.api+json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"jsonapi": {
"version": "1.0"
},
"meta": {
"sentTo": "jeffrey@example.test",
"expiresInMinutes": 15,
"reasons": [
{
"id": "no_longer_needed",
"label": "I no longer need an account",
"takesNote": false
},
{
"id": "bought_elsewhere",
"label": "I bought a watch elsewhere",
"takesNote": false
},
{
"id": "too_many_emails",
"label": "I get too many emails or notifications",
"takesNote": false
},
{
"id": "privacy",
"label": "Privacy concerns",
"takesNote": false
},
{
"id": "bad_experience",
"label": "I had a bad experience",
"takesNote": false
},
{
"id": "other",
"label": "Other",
"takesNote": true
}
]
}
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}{
"jsonapi": {
"version": "1.0"
},
"errors": [
{
"status": "409",
"code": "account_has_open_orders",
"title": "Conflict",
"detail": "You have an order that is not finished yet. We cannot delete your account while somebody is waiting to send you a watch, or waiting to be paid.",
"meta": {
"orders": [
"202600123"
]
}
}
]
}{
"errors": [
{
"status": "404",
"code": "watch_reserved",
"title": "Not Found",
"detail": "<string>",
"meta": {},
"source": {
"pointer": "/data/attributes/reference",
"parameter": "<string>"
}
}
],
"jsonapi": {
"version": "1.0"
}
}Authorizations
Body
Response
The code is on its way.