/v1/auth/token/two-factorThe second half of a sign-in. Send the challenge from
the first step with one of three answers: a code from
an authenticator app, one of the account's recovery codes, or a passkey
assertion. Which of them the account can use is in methods on the challenge.
Everything else about the sign-in, including who is signing in, which app and which scopes, comes from the challenge. This request cannot change any of it.
Request
curl https://api.proppertrading.com/v1/auth/token/two-factor \
-H "Content-Type: application/json" \
-d '{
"challenge": "a14ea608e89261062f4e90792701ef2a7c5ad1188c8537108efd788d1390ce03",
"code": "123456"
}'
| Field | Type | Description |
|---|---|---|
challenge | string | From the sign-in response. Required |
code | string | Six digits from the authenticator. Required without recovery_code |
recovery_code | string | One of the codes handed out when two-factor was set up. Required without code |
passkey | object | A WebAuthn assertion, exactly as the authenticator returned it. Required without the other two |
A recovery code is spent when it is used, exactly as on the web. Nine remain after the first one.
Send one of the three, not several. The passkey object is the assertion with
id, rawId, type and a response holding clientDataJSON,
authenticatorData, signature and optionally userHandle. Ask
for its options first: the
assertion has to answer a challenge this API issued.
Response
Identical to a successful sign-in, with
session.two_factor set to true.
{
"data": {
"object": "access_token",
"status": "signed_in",
"token": "ptat_9f2c1a44_3b8e7d2f5c9a1b4e6d8f0a2c4e6b8d1f",
"session": { "id": "9f2c1a44", "object": "session", "two_factor": true },
"workspace": { "object": "workspace", "id": "acmefunded", "name": "Acme Funded", "platform": false, "url": "https://acmefunded.com", "branding": null, "maintenance": false }
},
"meta": { "request_id": "req_01kz4bkr1aj1gv63d8fv3133ey" }
}
The challenge is used up either way
It survives exactly one correct code, and at most five wrong ones. After that it
is gone and the sign-in starts again from the password. A client that hits
expired_two_factor_challenge should return to its email and password screen
rather than asking for another code.
Challenges also expire on their own after five minutes.
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_two_factor_code | Wrong code, or a passkey that did not verify. The challenge is still good |
| 401 | expired_two_factor_challenge | Expired, already used, or burned by five wrong codes |
| 401 | account_deactivated | The account was switched off between the two steps |
| 403 | admin_access_required | Signing in to the Propper app with an account that does not help run the environment |
| 403 | block_anonymizer | The firm blocks VPN, proxy and Tor connections |
| 403 | block_country | The firm blocks connections from this country |
| 422 | validation_failed | None of a code, a recovery code or a passkey was sent |
| 429 | rate_limit_exceeded | Twenty attempts per ten minutes per address |
| 503 | tenant_in_maintenance | The firm closed between the two steps |
Every check runs again
Minutes can pass between the password and the code, and an account can be deactivated or a firm closed in between. The second step is not a formality.