curl --request POST \
--url https://{host}/v1/me/two-factor \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/vnd.api+json' \
--data '
{
"data": {
"type": "two-factor",
"attributes": {
"secret": "JBSWY3DPEHPK3PXP",
"otp": "123456",
"currentPassword": "the-one-they-typed"
}
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/vnd.api+json'},
body: JSON.stringify({
data: {
type: 'two-factor',
attributes: {
secret: 'JBSWY3DPEHPK3PXP',
otp: '123456',
currentPassword: 'the-one-they-typed'
}
}
})
};
fetch('https://{host}/v1/me/two-factor', 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/two-factor",
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' => 'two-factor',
'attributes' => [
'secret' => 'JBSWY3DPEHPK3PXP',
'otp' => '123456',
'currentPassword' => 'the-one-they-typed'
]
]
]),
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/two-factor"
payload = { "data": {
"type": "two-factor",
"attributes": {
"secret": "JBSWY3DPEHPK3PXP",
"otp": "123456",
"currentPassword": "the-one-they-typed"
}
} }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/vnd.api+json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"data": {
"type": "two-factor",
"attributes": {
"enabled": true,
"recoveryCode": "7QK2MP-XR4T9D-B8WLZC-N3VH6S"
}
}
}{
"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"
}
}Enable two-factor
Scope: account-security:write
The secret from the setup, a code the phone is showing now, and the account password. The code is what proves the secret arrived; the password is what stops a token lifted off a device from changing what protects the account.
Only when there is not one already. An account that already has a second
factor answers 409 two_factor_already_enabled: replacing an authenticator means
turning the old one off first, which asks for a code from the phone being
replaced. Otherwise the password alone would swap somebody’s authenticator for
another and burn their recovery code.
The password may be asked about five times in fifteen minutes, across every
endpoint that asks for it. After that this answers 429 too_many_attempts until
the window passes, whether the password is right or wrong.
This also ends the account’s other sessions — every other access and refresh token is revoked, and the one making this call is kept.
The answer carries the recovery code, once. There is no endpoint that reads it back — a recovery code somebody can fetch with the token they already hold is not a recovery code. Show it, and say it will not be shown again.
| Refusal | Status | code |
|---|---|---|
secret, otp or currentPassword missing | 422 | |
| The password does not match | 422 | |
| Already has a second factor | 409 | two_factor_already_enabled |
| Not the secret the setup handed out, or held longer than 30 minutes | 422 | two_factor_setup_expired |
| The code does not match the secret | 422 | |
| Out of password tries | 429 | too_many_attempts |
curl --request POST \
--url https://{host}/v1/me/two-factor \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/vnd.api+json' \
--data '
{
"data": {
"type": "two-factor",
"attributes": {
"secret": "JBSWY3DPEHPK3PXP",
"otp": "123456",
"currentPassword": "the-one-they-typed"
}
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/vnd.api+json'},
body: JSON.stringify({
data: {
type: 'two-factor',
attributes: {
secret: 'JBSWY3DPEHPK3PXP',
otp: '123456',
currentPassword: 'the-one-they-typed'
}
}
})
};
fetch('https://{host}/v1/me/two-factor', 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/two-factor",
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' => 'two-factor',
'attributes' => [
'secret' => 'JBSWY3DPEHPK3PXP',
'otp' => '123456',
'currentPassword' => 'the-one-they-typed'
]
]
]),
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/two-factor"
payload = { "data": {
"type": "two-factor",
"attributes": {
"secret": "JBSWY3DPEHPK3PXP",
"otp": "123456",
"currentPassword": "the-one-they-typed"
}
} }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/vnd.api+json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"data": {
"type": "two-factor",
"attributes": {
"enabled": true,
"recoveryCode": "7QK2MP-XR4T9D-B8WLZC-N3VH6S"
}
}
}{
"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"
}
}