> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pacifica.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Two-Factor Authentication

> This section covers two-factor authentication (2FA) for withdrawals and factor changes.

Two-factor authentication (2FA) adds a second check to withdrawals. When 2FA is on, every withdrawal and every factor change needs a proof from a second factor, in addition to the request signature. Renaming a factor is the only change that needs no proof.

A factor is one of these:

* An authenticator app (TOTP) that shows a 6-digit code.
* A passkey or a security key (WebAuthn). These work only in the Pacifica app. See [Passkeys and security keys](#passkeys-and-security-keys).

API clients should use an authenticator app.

* [Get two-factor status](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/get-two-factor-status.md): This endpoint allows users to get an account's two-factor status, factors, and remaining recovery keys.
* [Begin TOTP enrollment](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/begin-totp-enrollment.md): This endpoint allows users to start adding an authenticator app (TOTP) factor.
* [Finish TOTP enrollment](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/finish-totp-enrollment.md): This endpoint allows users to confirm an authenticator app (TOTP) factor with a code.
* [Begin WebAuthn registration](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/begin-webauthn-registration.md): This endpoint allows users to start registering a passkey or security key.
* [Finish WebAuthn registration](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/finish-webauthn-registration.md): This endpoint allows users to finish registering a passkey or security key.
* [Begin WebAuthn authentication](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/begin-webauthn-authentication.md): This endpoint allows users to get a challenge for a passkey or security key proof.
* [Disable two-factor authentication](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/disable-two-factor.md): This endpoint allows users to turn off two-factor authentication and delete every factor and recovery key.
* [Remove factor](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/remove-factor.md): This endpoint allows users to remove one factor.
* [Rename factor](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/rename-factor.md): This endpoint allows users to change a factor's label.
* [Regenerate recovery keys](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/regenerate-recovery-keys.md): This endpoint allows users to replace their recovery keys with a new set.

### Turn on 2FA with an authenticator app

1. Call [Begin TOTP enrollment](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/begin-totp-enrollment.md) with a `label`. The response has an `enrollment_id`, a base32 `secret`, and an `otpauth_uri`.
2. Add the secret to an authenticator app. Show `otpauth_uri` as a QR code, or type `secret` in by hand. Codes use SHA-1, 6 digits, and a 30-second period.
3. Within 10 minutes, call [Finish TOTP enrollment](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/finish-totp-enrollment.md) with the `enrollment_id` and the app's current `code`.
4. Save the `recovery_keys` from the response. Pacifica shows them only once.

2FA is now on. [Get two-factor status](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/get-two-factor-status.md) returns `"protected_actions": ["withdrawal"]`.

The code that finishes enrollment is used up. Wait for the next code before you send your first proof.

### Proofs

A proof is a JSON object. Its `type` field selects the factor.

| `type` | Shape | Content |
| - | - | - |
| `"totp"` | `{"type": "totp", "code": "123456"}` | The authenticator app's current code, as a string of exactly 6 digits |
| `"recovery_key"` | `{"type": "recovery_key", "key": "3f9a-1c0b-77de-a4e2-0b19-5c6d-e8f0-2a7b"}` | One of your [recovery keys](#recovery-keys) |
| `"webauthn"` | `{"type": "webauthn", "challenge_id": "45c48cce-...", "credential": {...}}` | A passkey or security key assertion. See [Begin WebAuthn authentication](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/begin-webauthn-authentication.md) |

Put the proof in the request body:

* In `two_factor` on withdrawals, [Begin TOTP enrollment](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/begin-totp-enrollment.md), and [Begin WebAuthn registration](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/begin-webauthn-registration.md).
* In `proof` on [Disable two-factor authentication](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/disable-two-factor.md), [Remove factor](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/remove-factor.md), and [Regenerate recovery keys](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/regenerate-recovery-keys.md).

Do not sign the proof. Sign the request without it, then add the proof to the body.

Each proof works once:

* A TOTP code works once. After a proof passes, wait for the next code.
* A recovery key works once.
* A WebAuthn challenge works once.

Each action accepts these proofs:

| Action | `totp` | `webauthn` | `recovery_key` |
| - | - | - | - |
| Withdrawal | Yes | Yes | No |
| Add a factor while 2FA is on | Yes | Yes | Yes |
| Remove a factor | Yes | Yes | Yes |
| Regenerate recovery keys | Yes | Yes | Yes |
| Disable 2FA | Yes | Yes | Yes |

Renaming a factor needs no proof. A recovery key on a withdrawal returns status `422`.

### Who can sign

| Endpoint | Signer | Account |
| - | - | - |
| [Get two-factor status](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/get-two-factor-status.md) | Account key or [agent wallet](https://docs.pacifica.fi/api-documentation/api/signing/api-agent-keys.md) | Main account only |
| [Begin WebAuthn authentication](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/begin-webauthn-authentication.md) | Account key or [agent wallet](https://docs.pacifica.fi/api-documentation/api/signing/api-agent-keys.md) | Account with 2FA on |
| Every other 2FA endpoint | Account key only | Main account only |

An agent wallet cannot add, remove, or rename factors. It cannot regenerate recovery keys or turn 2FA off. A subaccount gets status `403` from the main-account-only endpoints.

These endpoints use the normal [signing scheme](https://docs.pacifica.fi/api-documentation/api/signing/implementation.md). Each page shows the `data` object to sign. Some request fields are not signed: `code` on Finish TOTP enrollment, `credential` on Finish WebAuthn registration, and every proof. Sign UUID values in lowercase, as the API returns them.

### Limits

* **Proof attempts.** Each signer can make 5 proof attempts per 5 minutes. A proof that passes resets this count. The account key and each agent wallet have separate counts, so an agent wallet that sends wrong proofs locks out only itself.
* **Wrong proofs.** Each signer can send 30 wrong proofs per 24 hours. After that, Pacifica refuses every proof from that signer with status `429` until 24 hours after its first wrong proof. A proof that passes does not reset this count.
* **WebAuthn challenges.** Each signer can request up to 60 challenges per 10 minutes for each action. A WebAuthn proof with an unknown, expired, or used challenge does not count as a proof attempt.
* **Factors.** An account can have up to 8 factors. It can have only one authenticator app. A new authenticator app replaces the old one when you finish its enrollment.
* **Enrollments.** You can start up to 20 enrollments per 10 minutes. An enrollment lasts 10 minutes. A TOTP enrollment allows 5 wrong codes. After that, start again.
* **Retrying a begin call.** While 2FA is on, the begin enrollment calls need a proof. After the proof passes, you can repeat the same begin request, with the same `label` (and `kind` for WebAuthn), for 5 minutes without a new proof. This helps when a user cancels an authenticator prompt.
* **Factor changes.** Adding a factor, removing a factor, regenerating recovery keys, and disabling 2FA end every enrollment in progress and every 5-minute retry window. A finish call then returns status `422` with `two-factor settings changed. Start again.` Renaming a factor does not end them.

Pacifica checks the factor limit, the enrollment limit, and whether a factor can be removed before it checks the proof. A request that fails these checks does not use up your code.

### Recovery keys

* Your first factor comes with 10 recovery keys. Later factors do not add keys. Their finish response has an empty `recovery_keys` list.
* Pacifica shows recovery keys only once. Store them somewhere safe.
* A key is 8 groups of 4 hex characters, for example `3f9a-1c0b-77de-a4e2-0b19-5c6d-e8f0-2a7b`. Case and hyphens do not matter.
* Each key works once. [Get two-factor status](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/get-two-factor-status.md) returns `recovery_keys_remaining`.
* A recovery key authorizes factor changes only. It never authorizes a withdrawal.
* If you lose your authenticator app, send a recovery key as the proof to [Begin TOTP enrollment](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/begin-totp-enrollment.md) or to [Disable two-factor authentication](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/disable-two-factor.md).
* [Regenerate recovery keys](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/regenerate-recovery-keys.md) replaces the whole set. Disabling 2FA deletes it.

### Withdrawals

When 2FA is on, every withdrawal needs a proof, including withdrawals that an agent wallet signs. Send an `idempotency_key`, and send the proof in `two_factor`. For details, see [Request withdrawal](https://docs.pacifica.fi/api-documentation/api/rest-api/account/request-withdrawal.md) and [Withdraw spot asset](https://docs.pacifica.fi/api-documentation/api/rest-api/account/withdraw-spot-asset.md).

### Passkeys and security keys

Passkeys and security keys use WebAuthn. Pacifica binds them to its own domain, and it accepts them only from the Pacifica app's origin. Pacifica also rejects an assertion that another site requested through an embedded frame, that is, an assertion with `crossOrigin` or `topOrigin` set in its client data.

So in practice, you can register and use a passkey only in the Pacifica app. A script or bot cannot make a passkey proof. API clients should use an authenticator app.

A WebAuthn proof needs a challenge from [Begin WebAuthn authentication](https://docs.pacifica.fi/api-documentation/api/rest-api/two-factor/begin-webauthn-authentication.md). The challenge is bound to the exact request that the proof authorizes.

### Errors

2FA errors carry an `error_id`. Match on `error_id`, not on `error`. See [Error codes](https://docs.pacifica.fi/api-documentation/api/error-codes.md).

| Status | `error_id` | Meaning |
| - | - | - |
| `400` | | Invalid request fields or signature. A withdrawal without `idempotency_key` while 2FA is on |
| `401` | `second_factor_invalid` | The proof is wrong, expired, or already used. Or the WebAuthn challenge is expired, used, or for another request |
| `403` | | The account is a subaccount, or it does not exist |
| `409` | `operation_conflict` | The withdrawal `idempotency_key` belongs to a different request |
| `422` | `second_factor_setup_incomplete` | The request does not fit the account's 2FA setup. `error` says why. See below |
| `428` | `second_factor_required` | 2FA is on, and the request has no proof |
| `429` | `second_factor_rate_limited` | Too many proof attempts, enrollments, or WebAuthn challenges |
| `503` | `second_factor_unavailable` | The two-factor service is unavailable. Retry later |

```json theme={null}
{
  "success": false,
  "data": null,
  "error": "two-factor proof is required",
  "code": 428,
  "error_id": "second_factor_required"
}
```

`error` values with status `422`:

| `error` | Meaning |
| - | - |
| `set up two-factor authentication before continuing` | The account has never started an enrollment |
| `two-factor is not enabled` | 2FA is off, and the call needs it on |
| `two-factor settings changed. Start again.` | A factor change ended this enrollment. Start a new one |
| `a user can have at most 8 factors` | The account already has 8 factors |
| `the last factor cannot be removed. Disable two-factor instead.` | Remove factor cannot remove the only factor |
| `unknown factor` | No factor on the account has this `factor_id` |
| `a recovery key only authorizes factor changes` | A recovery key was sent for a withdrawal |
| `credential is already registered` | This passkey or security key is already a factor |
| `the authenticator must verify the user with a PIN or biometric` | The authenticator did not verify the user |
| `no matching WebAuthn credential` | The account has no factor of the requested `kind` |

### Code Example (Python)

This example turns on 2FA with an authenticator app, then withdraws with a TOTP proof. The signing steps follow [Signing](https://docs.pacifica.fi/api-documentation/api/signing/implementation.md).

```python theme={null}
import json
import time
import uuid

import base58
import requests
from solders.keypair import Keypair

API_URL = "https://api.pacifica.fi/api/v1"
keypair = Keypair.from_bytes(base58.b58decode("your_private_key_here"))
account = str(keypair.pubkey())


def sort_json_keys(value):
    if isinstance(value, dict):
        return {key: sort_json_keys(value[key]) for key in sorted(value)}
    if isinstance(value, list):
        return [sort_json_keys(item) for item in value]
    return value


def signed_post(path, operation_type, data, unsigned=None):
    """Signs `data`, then adds the `unsigned` fields, such as a proof, to the body."""
    timestamp = int(time.time() * 1_000)
    message = {
        "timestamp": timestamp,
        "expiry_window": 30_000,
        "type": operation_type,
        "data": data,
    }
    compact = json.dumps(sort_json_keys(message), separators=(",", ":"))
    signature = keypair.sign_message(compact.encode("utf-8"))
    body = {
        "account": account,
        "signature": base58.b58encode(bytes(signature)).decode("ascii"),
        "timestamp": timestamp,
        "expiry_window": 30_000,
        **data,
        **(unsigned or {}),
    }
    return requests.post(f"{API_URL}{path}", json=body).json()


# 1. Start the enrollment. Add the URI to an authenticator app.
begin = signed_post(
    "/account/2fa/totp/begin", "begin_totp_enrollment", {"label": "Authenticator app"}
)
print(begin["data"]["otpauth_uri"])

# 2. Finish with the app's current code. The code is not signed.
finish = signed_post(
    "/account/2fa/totp/finish",
    "finish_totp_enrollment",
    {"enrollment_id": begin["data"]["enrollment_id"]},
    unsigned={"code": input("Code: ")},
)
recovery_keys = finish["data"]["recovery_keys"]  # Pacifica shows these only once

# 3. Withdraw with a TOTP proof. Use a new code, not the one from step 2.
withdrawal = signed_post(
    "/account/withdraw",
    "withdraw",
    {"amount": "100.50", "idempotency_key": str(uuid.uuid4())},
    unsigned={"two_factor": {"type": "totp", "code": input("Next code: ")}},
)
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.