curl --request POST \
--url https://{host}/v1/me/identities \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/vnd.api+json' \
--data '
{
"data": {
"type": "identities",
"attributes": {
"provider": "apple",
"idToken": "eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ...",
"nonce": "7c1f9e2b4d",
"authorizationCode": "c6a1b3e0f8d2…"
}
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/vnd.api+json'},
body: JSON.stringify({
data: {
type: 'identities',
attributes: {
provider: 'apple',
idToken: 'eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ...',
nonce: '7c1f9e2b4d',
authorizationCode: 'c6a1b3e0f8d2…'
}
}
})
};
fetch('https://{host}/v1/me/identities', 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/identities",
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' => 'identities',
'attributes' => [
'provider' => 'apple',
'idToken' => 'eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ...',
'nonce' => '7c1f9e2b4d',
'authorizationCode' => 'c6a1b3e0f8d2…'
]
]
]),
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/identities"
payload = { "data": {
"type": "identities",
"attributes": {
"provider": "apple",
"idToken": "eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ...",
"nonce": "7c1f9e2b4d",
"authorizationCode": "c6a1b3e0f8d2…"
}
} }
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": {
"identities": [
"apple"
]
}
}{
"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"
}
}Link Apple or Google
Scope: account-security:write
Links an Apple ID or a Google account to the account already signed in to, so it can be
signed in to with that from now on. Send what the native sign-in sheet gave you — the
ID token and the nonce you asked it for — as at sign-in. Which are linked is
identities on /v1/me.
For Apple, send authorizationCode too, as the token exchange takes it at sign-in.
We exchange it with Apple for a refresh token, which is what deleting the account revokes
(App Store guideline 5.1.1(v)). Without it the link still works, but there is nothing to
revoke. Unlinking Apple revokes it as well.
Signing in with Apple or Google already links on a matching, verified address; this is for when it does not — an Apple ID that hides the address, a Google account on another one.
| Case | Status | code |
|---|---|---|
| Linked | 201 | |
| Already linked, the same one | 200 | |
Not apple or google | 422 | unknown_provider |
| The token does not check out | 422 | identity_not_verified |
| A different one of that provider is linked here | 409 | identity_already_linked |
| That one is linked to another Wristler account | 409 | identity_in_use |
The owner is mailed every time (sign_in_method_changed, which also lands in the bell and
pushes): a linked identity is a way in without the password.
curl --request POST \
--url https://{host}/v1/me/identities \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/vnd.api+json' \
--data '
{
"data": {
"type": "identities",
"attributes": {
"provider": "apple",
"idToken": "eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ...",
"nonce": "7c1f9e2b4d",
"authorizationCode": "c6a1b3e0f8d2…"
}
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/vnd.api+json'},
body: JSON.stringify({
data: {
type: 'identities',
attributes: {
provider: 'apple',
idToken: 'eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ...',
nonce: '7c1f9e2b4d',
authorizationCode: 'c6a1b3e0f8d2…'
}
}
})
};
fetch('https://{host}/v1/me/identities', 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/identities",
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' => 'identities',
'attributes' => [
'provider' => 'apple',
'idToken' => 'eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ...',
'nonce' => '7c1f9e2b4d',
'authorizationCode' => 'c6a1b3e0f8d2…'
]
]
]),
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/identities"
payload = { "data": {
"type": "identities",
"attributes": {
"provider": "apple",
"idToken": "eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ...",
"nonce": "7c1f9e2b4d",
"authorizationCode": "c6a1b3e0f8d2…"
}
} }
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": {
"identities": [
"apple"
]
}
}{
"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
Body
Response
It was already linked.