---
title: "Verify a recovery code and upgrade the session to AAL2."
method: POST
path: "/factors/recovery-codes/verify"
tags: ["user"]
---

# Verify a recovery code and upgrade the session to AAL2.

`POST /factors/recovery-codes/verify`

## Request

- Base URL: `https://abcdefghijklmnopqrst.supabase.co/auth/v1`
- URL: `https://abcdefghijklmnopqrst.supabase.co/auth/v1/factors/recovery-codes/verify`
- Auth: API key in header `apikey` and HTTP bearer

Redeems one single-use recovery code from an AAL1 session. The submitted code is normalized before comparison: whitespace and `-` separators are removed and letters are lowercased, so `K4M9-X7QP-2AB8-HT3Z` and `k4m9x7qp2ab8ht3z` are equivalent. A matched code is consumed and cannot be used again.

## Request body

- object
  - `code` string, required: The recovery code to redeem. Separators, whitespace and letter case are ignored.

## Response `200`

The recovery code was accepted and consumed. These new credentials have an increased Authenticator Assurance Level (AAL) and an `amr` claim that includes `mfa/recovery_code`.

- AccessTokenResponseSchema
  - `access_token` string: A valid JWT that will expire in `expires_in` seconds.
  - `refresh_token` string: An opaque string that can be used once to obtain a new access and refresh token.
  - `token_type` string: What type of token this is. Only `bearer` returned, may change in the future.
  - `expires_in` integer: Number of seconds after which the `access_token` should be renewed by using the refresh token with the `refresh_token` grant type.
  - `expires_at` integer: UNIX timestamp after which the `access_token` should be renewed by using the refresh token with the `refresh_token` grant type.
  - `weak_password` object: Only returned on the `/token?grant_type=password` endpoint. When present, it indicates that the password used is weak. Inspect the `reasons` and/or `message` properties to identify why.
    - `reasons` string[]
    - `message` string
  - `user` UserSchema: Object describing the user related to the issued access and refresh tokens.
    - `id` string, uuid
    - `aud` string
    - `role` string
    - `email` string: User's primary contact email. In most cases you can uniquely identify a user by their email address, but not in all cases.
    - `email_confirmed_at` string, date-time
    - `phone` string, phone: User's primary contact phone number. In most cases you can uniquely identify a user by their phone number, but not in all cases.
    - `phone_confirmed_at` string, date-time
    - `confirmation_sent_at` string, date-time
    - `confirmed_at` string, date-time
    - `recovery_sent_at` string, date-time
    - `new_email` string, email
    - `email_change_sent_at` string, date-time
    - `new_phone` string, phone
    - `phone_change_sent_at` string, date-time
    - `reauthentication_sent_at` string, date-time
    - `last_sign_in_at` string, date-time
    - `app_metadata` object
    - `user_metadata` object
    - `factors` MFAFactorSchema[]
      - `id` string, uuid
      - `status` string: Usually one of: - verified - unverified
      - `friendly_name` string
      - `factor_type` string: Usually one of: - totp - phone - webauthn - recovery_code
      - `webauthn_credential` string
      - `phone` string, phone, nullable
      - `created_at` string, date-time
      - `updated_at` string, date-time
      - `last_challenged_at` string, date-time, nullable
    - `identities` IdentitySchema[]
      - `identity_id` string, uuid
      - `id` string, uuid
      - `user_id` string, uuid
      - `identity_data` object
      - `provider` string
      - `last_sign_in_at` string, date-time
      - `created_at` string, date-time
      - `updated_at` string, date-time
      - `email` string, email
    - `banned_until` string, date-time
    - `created_at` string, date-time
    - `updated_at` string, date-time
    - `deleted_at` string, date-time
    - `is_anonymous` boolean

## Other responses

- `400`: Bad request. The body could not be parsed or `code` is missing or empty.
- `403`: Forbidden error. The MFA verification attempt hook rejected the attempt or the user is anonymous.
- `422`: Unprocessable entity. `mfa_verification_failed` is returned for a wrong code, an already-used code and when no recovery codes are enrolled. `mfa_recovery_codes_verify_not_enabled` is returned when recovery-code verification is disabled on the server.
- `429`: Too many requests. Either the IP-based rate limit was breached (`over_request_rate_limit`), or the per-user lockout is active after too many consecutive failed attempts (`mfa_recovery_codes_locked`). Regenerating the codes from an AAL2 session clears the lock.

## Changes

- **2026-09-21** `2e2206095d36`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/supabase/apis/supabase-auth-rest-api/changes/factors/recovery-codes/verify/post.md)

---

[API](https://skmtc.dev/supabase/apis/supabase-auth-rest-api.md) - [All operations](https://skmtc.dev/supabase/apis/supabase-auth-rest-api/llms.txt) - [OpenAPI document](https://skmtc.dev/supabase/apis/supabase-auth-rest-api/revisions/c480c2703e65?raw)
