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

# 2FA

> Email-based two-factor authentication for sensitive operations like autoramp destination changes, fiat address registration, and customer email changes.

## Overview

Iron requires email two-factor authentication (2FA) for sensitive actions. This checks that actions cannot be performed on behalf of a customer without their explicit approval.

2FA is required in the following cases:

* Editing the destination address of an Autoramp (fiat or wallet).
* Registering a fiat address that needs authorization.
* Reusing an existing vIBAN or account number (for example, cancelling and re-creating an Autoramp that reuses the same vIBAN).
* Changing a customer's email address.

During customer onboarding, Iron collects verified email addresses from individuals legally associated with the customer. 2FA emails go to these addresses.

When an action triggers 2FA, the affected entity (Autoramp, fiat address, or email change) enters the `AuthorizationRequired` state. It stays there until the customer confirms the action.

## How it works

<Steps>
  <Step title="Customer triggers a sensitive action">
    Your app calls an endpoint that requires 2FA, such as editing an Autoramp destination. The entity moves to the `AuthorizationRequired` state and Iron emails the customer a 2FA code with a confirmation link.
  </Step>

  <Step title="Customer confirms via link or code">
    If the customer clicks the confirmation link in the email, the action completes and no further work is needed. If they don't, they can enter the code manually in your app.
  </Step>

  <Step title="Fetch the pending authentication code">
    Look up the pending authentication code by the entity's ID to get the authentication code ID you need for submission.

    <Accordion title="Request">
      <CodeGroup>
        ```bash bash theme={null}
        curl -X GET "https://api.sandbox.iron.xyz/api/authentication-codes/entity/{entity_id}" \
        -H "X-API-Key: $API_KEY"
        ```
      </CodeGroup>
    </Accordion>
  </Step>

  <Step title="Submit the code">
    Submit the code the customer entered against the authentication code ID from the previous step.

    <Accordion title="Request">
      <CodeGroup>
        ```bash bash theme={null}
        curl -X PUT "https://api.sandbox.iron.xyz/api/authentication-codes/{auth_code_id}" \
        -H "Content-Type: application/json" \
        -H "X-API-Key: $API_KEY" \
        -d '{ "code": 123456 }'
        ```
      </CodeGroup>
    </Accordion>
  </Step>

  <Step title="Check the result">
    A successful submission returns the authentication code with `"status": "Confirmed"` and the entity leaves the `AuthorizationRequired` state.
  </Step>
</Steps>

## Get the pending authentication code

`GET /api/authentication-codes/entity/{id}`

Returns the pending authentication code for an entity in the `AuthorizationRequired` state. The `{id}` is the ID of the entity itself (the Autoramp, fiat address, or email change), not the authentication code.

<CodeGroup>
  ```bash bash theme={null}
  curl -X GET "https://api.sandbox.iron.xyz/api/authentication-codes/entity/4b85d15e-f343-41c0-809c-85314cae2fa6" \
  -H "X-API-Key: $API_KEY"
  ```
</CodeGroup>

### Response

<CodeGroup>
  ```json JSON theme={null}
  {
    "id": "9f1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
    "status": "Pending",
    "attempts": 0,
    "entity_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
    "expires_at": "2025-01-15T10:45:00Z"
  }
  ```
</CodeGroup>

| Field        | Type      | Description                                            |
| ------------ | --------- | ------------------------------------------------------ |
| `id`         | UUID      | The authentication code ID. Use it to submit the code. |
| `status`     | string    | One of `Pending`, `Expired`, `Confirmed`, `Rejected`   |
| `attempts`   | integer   | Number of submission attempts so far                   |
| `entity_id`  | UUID      | The entity this authentication relates to              |
| `expires_at` | timestamp | When the code expires                                  |

<Note>
  If no pending code exists for the entity, this endpoint returns `404 Not Found`. This happens when the code expired or was already confirmed.
</Note>

## Submit an authentication code

`PUT /api/authentication-codes/{id}`

Submits the code the customer received by email. The `{id}` is the authentication code ID from the previous endpoint.

<CodeGroup>
  ```bash bash theme={null}
  curl -X PUT "https://api.sandbox.iron.xyz/api/authentication-codes/9f1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -d '{ "code": 123456 }'
  ```
</CodeGroup>

### Request body

| Field  | Type    | Required | Description                            |
| ------ | ------- | -------- | -------------------------------------- |
| `code` | integer | Yes      | The 2FA code from the customer's email |

<Warning>
  `code` is an integer, not a string. Send `123456`, not `"123456"`. A string value fails validation.
</Warning>

### Response

Returns the updated authentication code. Check `status` to confirm the result:

<CodeGroup>
  ```json JSON theme={null}
  {
    "id": "9f1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
    "status": "Confirmed",
    "attempts": 1,
    "entity_id": "4b85d15e-f343-41c0-809c-85314cae2fa6",
    "expires_at": "2025-01-15T10:45:00Z"
  }
  ```
</CodeGroup>

## Authentication code statuses

| Status      | Description                                                                   |
| ----------- | ----------------------------------------------------------------------------- |
| `Pending`   | The code was sent and is waiting for confirmation.                            |
| `Confirmed` | The customer confirmed the action. The entity leaves `AuthorizationRequired`. |
| `Expired`   | The code passed its `expires_at` time without confirmation.                   |
| `Rejected`  | The submitted code was wrong too many times.                                  |

## Sub-partner scoping

Both endpoints accept an optional `X-SUB-PARTNER-ID` header. When provided, the authentication code lookup and submission are scoped to that sub-partner.

## Common errors

| Error                | Cause                                                                                              |
| -------------------- | -------------------------------------------------------------------------------------------------- |
| `401 Unauthorized`   | Missing or invalid `X-API-Key`                                                                     |
| `404 Not Found`      | No pending authentication code for this entity, or the authentication code ID doesn't exist        |
| `500 Internal Error` | Returns `{ "message": "...", "trace_id": "..." }`. Reference the `trace_id` when reporting issues. |

<Note>
  The endpoints `POST /api/autoramps/{autoramp_id}/retry-auth` and `POST /api/addresses/fiat/{id}/retry-auth` are deprecated. Don't build on them. If no pending code exists for an entity, re-trigger the underlying action instead.
</Note>

## Sandbox testing

In sandbox, the static code `123456` confirms any pending authentication. Submit it as an integer:

<CodeGroup>
  ```json JSON theme={null}
  { "code": 123456 }
  ```
</CodeGroup>

## Related guides

<CardGroup>
  <Card title="Autoramps" href="/autoramp" icon="repeat" horizontal />

  <Card title="Fiat Addresses" href="/fiat-addresses" icon="building-columns" horizontal />
</CardGroup>
