# Authentication-Based Recovery API (Email / SMS)

A Safe guardian service that uses email and phone verification to facilitate account recovery. It can be used as a default recovery method or combined with other guardians (such as hardware wallets or trusted contacts) to create a customized recovery threshold.

info

To get started, request access [here](https://app.formbricks.com/s/brdzlw0t897cz3mxl3ausfb5).

## Authentication[​](#authentication "Direct link to Authentication")

All API requests require a Bearer token in the Authorization header:

```
curl -X POST \
  https://yourcompany.recovery.candide.dev/auth/register \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_BEARER_TOKEN' \
  -d '{...}'
```

**/auth** (Registration)

* **/auth/register**
  * **POST**: Submit a registration request
* **/auth/submit**
  * **POST**: Submits the receive OTP
* **/auth/registrations**
  * **GET**: Fetch active registration
* **/auth/delete**
  * **POST**: Delete a registration

**/auth/signature** (Recovery)

* **/auth/signature/request**
  * **POST**: Request to recover an account
* **/auth/signature/submit**
  * **POST**: Confirm recovery with OTP challenge

## Auth Registration[​](#auth-registration "Direct link to Auth Registration")

### Email / SMS[​](#email--sms "Direct link to Email / SMS")

Submit a registration request with the target smart account to protect using the choice of channel (email or SMS). The user will then receive an OTP code to later submit the challenge in `/auth/submit`.

#### `POST /auth/register`[​](#post-authregister "Direct link to post-authregister")

* Example Request
* Example Response
* Request Body
* Response Body

```
curl -X POST \
  https://yourcompany.recovery.candide.dev/auth/register \
  -H 'Content-Type: application/json' \
  -d '{
   "account":"0x...",
   "chainId": 1,
   "channel":"email",
   "target":"user@example.com",
   "message": "siwe(chainId, statement(channel, target))",
   "signature": "sign(message)"
}'
```

```
{
   "challengeId":"unique-challenge-id",
}
```

| key         | type     | description                                                 |
| :---------- | :------- | :---------------------------------------------------------- |
| `account`   | `string` | The smart account address requesting registration           |
| `chainId`   | `number` | The chain ID where the account resides                      |
| `channel`   | `string` | Authentication channel: 'email' or 'sms'                    |
| `target`    | `string` | The email or phone number for authentication                |
| `message`   | `string` | SIWE (EIP-4361) message signed by the account               |
| `signature` | `string` | Signature proving the request is initiated from the account |

| key           | type     | description                                                    |
| :------------ | :------- | :------------------------------------------------------------- |
| `challengeId` | `string` | Unique challenge ID for the registration, used in /auth/submit |

* `account`: The smart account address requesting registration.
* `chainId`: The chain id in which your account resides (for multi-chain wallets, users will need to register per chain)
* `channel`: Either `"email"` or `"sms"` (defines the authentication type).
* `target`: The email or phone number for authentication.
* `message`: SIWE (EIP-4361) message statement. Statement:

```
I authorize Safe Recovery Service to sign a recovery request for my account after I authenticate using {{target}} via {{channel}}
```

* `signature`: signature proving the request is initiated from the account

See [example guide](#how-to-sign-messages-siwe-eip-4361) how to construct the message and signature using Sign-In with Ethereum (SIWE).

### Submit Confirmation Using OTP[​](#submit-confirmation-using-otp "Direct link to Submit Confirmation Using OTP")

Submit the received OTP code to confirm ownership of the target channel.

#### `POST /auth/submit`[​](#post-authsubmit "Direct link to post-authsubmit")

* Example Request
* Example Response
* Request Body
* Response Body

```
curl -X POST \
  https://yourcompany.recovery.candide.dev/auth/submit \
  -H 'Content-Type: application/json' \
  -d '{
   "challengeId":"unique-challenge-id",
   "challenge":"123456"
}'
```

```
{
   "registrationId": "unique-registration-id",
   "guardianAddress": "0x...",
}
```

| key           | type     | description                                          |
| :------------ | :------- | :--------------------------------------------------- |
| `challengeId` | `string` | The unique challenge ID received from /auth/register |
| `challenge`   | `string` | The OTP code received via email/SMS                  |

| key               | type     | description                                          |
| :---------------- | :------- | :--------------------------------------------------- |
| `registrationId`  | `string` | Unique registration ID for the authenticated channel |
| `guardianAddress` | `string` | The guardian address added to the Safe account       |

### Get active registration[​](#get-active-registration "Direct link to Get active registration")

Fetch the registration of the protected smart account

#### `GET /auth/registrations`[​](#get-authregistrations "Direct link to get-authregistrations")

* Example Request
* Example Response
* Query Parameters
* Response Body

```
curl -G "https://yourcompany.recovery.candide.dev/auth/registrations" \
   --data-urlencode "account=0x...",
   --data-urlencode "chainId=0x1",
   --data-urlencode "message=siwe(chainId, statement)",
   --data-urlencode "signature=sign(message)"
```

```
{
  "registrations": [
    {
      "id": "unique-registration-id",
      "channel": "email",
      "target": "user@example.com",
    }
  ]
}
```

| key         | type     | description                            |
| :---------- | :------- | :------------------------------------- |
| `account`   | `string` | The Safe account address               |
| `chainId`   | `number` | The chain ID where the account resides |
| `message`   | `string` | SIWE message signed by the account     |
| `signature` | `string` | Signature proving the request          |

| key             | type    | description                  |
| :-------------- | :------ | :--------------------------- |
| `registrations` | `array` | List of active registrations |

See [example guide](#how-to-sign-messages-siwe-eip-4361) how to construct the message and signature using Sign in With Ethereum (SIWE)

### Delete[​](#delete "Direct link to Delete")

Deletes a registration

#### `POST /auth/delete`[​](#post-authdelete "Direct link to post-authdelete")

* Example Request
* Example Response
* Request Body
* Response Body

```
curl -X POST \
  https://yourcompany.recovery.candide.dev/auth/delete \
  -H 'Content-Type: application/json' \
  -d '{
   "registrationId":"unique-registration-id",
   "message": "siwe(chainId, statement(registrationId))",
   "signature": "sign(registrationId, timestamp)"
}'
```

```
{
    "success": "true"
}
```

| key              | type     | description                        |
| :--------------- | :------- | :--------------------------------- |
| `registrationId` | `string` | The registration ID to delete      |
| `message`        | `string` | SIWE message signed by the account |
| `signature`      | `string` | Signature proving the request      |

| key       | type      | description                     |
| :-------- | :-------- | :------------------------------ |
| `success` | `boolean` | True if deletion was successful |

See [example guide](#how-to-sign-messages-siwe-eip-4361) how to construct the message and signature using Sign in With Ethereum (SIWE)

## Auth Recovery[​](#auth-recovery "Direct link to Auth Recovery")

### Request to recover an account[​](#request-to-recover-an-account "Direct link to Request to recover an account")

Request a signature from the service to recover an account given the new owners and threshold

#### `POST /auth/signature/request`[​](#post-authsignaturerequest "Direct link to post-authsignaturerequest")

* Example Request
* Example Response
* Request Body
* Response Body

```
curl -X POST \
  https://yourcompany.recovery.candide.dev/auth/signature/request \
  -H 'Content-Type: application/json' \
  -d '{
    "account":"0x...",
    "newOwners": ["0x...", "0x..."],
    "newThreshold": 2,
    "chainId": 1
  }'
```

```
{
   "requestId":"unique-signature-request-id",
   "requiredVerifications": 1,
   "auths": [
      {
         "challengeId": "unique-challenge-id",
         "channel": "email",
         "target": "us**@exa****.com"
      }
   ]
}
```

| key            | type       | description                               |
| :------------- | :--------- | :---------------------------------------- |
| `account`      | `string`   | The smart account address to be recovered |
| `newOwners`    | `string[]` | The new owners for the Safe account       |
| `newThreshold` | `number`   | The new threshold for the Safe account    |
| `chainId`      | `number`   | Chain ID for the recovery request         |

| key                     | type     | description                               |
| :---------------------- | :------- | :---------------------------------------- |
| `requestId`             | `string` | Unique signature request ID               |
| `requiredVerifications` | `number` | Minimum number of OTP challenges required |
| `auths`                 | `array`  | List of authentication methods to verify  |

### Confirm recovery with OTP challenge[​](#confirm-recovery-with-otp-challenge "Direct link to Confirm recovery with OTP challenge")

Request to submits the signature with the provided OTP code challenge and id

#### `POST /auth/signature/submit`[​](#post-authsignaturesubmit "Direct link to post-authsignaturesubmit")

* Example Request
* Example Response
* Request Body
* Response Body

```
curl -X POST \
  https://yourcompany.recovery.candide.dev/auth/signature/submit \
  -H 'Content-Type: application/json' \
  -d '{
    "requestId": "unique-signature-request-id",
    "challengeId": "unique-challenge-id",
    "challenge": "123456"
  }'
```

```
{
   "success": true,
   "signer": "0x...",
   "signature": "0x..."
}
```

| key           | type     | description                                            |
| :------------ | :------- | :----------------------------------------------------- |
| `requestId`   | `string` | The unique ID from /auth/signature/request             |
| `challengeId` | `string` | The challenge ID specific to the authentication method |
| `challenge`   | `string` | The OTP code received via email/SMS                    |

| key         | type      | description                                                               |
| :---------- | :-------- | :------------------------------------------------------------------------ |
| `success`   | `boolean` | True if verification was successful                                       |
| `signer`    | `string`  | Guardian address (available only if sufficient verifications collected)   |
| `signature` | `string`  | Recovery signature (available only if sufficient verifications collected) |

## How to Sign Messages (SIWE EIP-4361)[​](#how-to-sign-messages-siwe-eip-4361 "Direct link to How to Sign Messages (SIWE EIP-4361)")

* SIWE
* safe-utils
* example of what users will see

```
import { SiweMessage } from "siwe";
import { hexlify, randomBytes } from "ethers";

import { personalSign, getMessageHashForSafe } from "./safe-utils"

function generateSIWEMessageSignaturePair(safeAccountAddress: string, statement: string, chainId: string): [string, string] {
  const siweMessage = new SiweMessage({
    version: "1",
    address: ethers.getAddress(accountAddress),
    domain: "service://safe-recovery-safeAccountAddress",
    uri: "service://safe-recovery-service",
    statement,
    chainId: Number(chainId),
    nonce: hexlify(randomBytes(24)),
  });
  const message = siweMessage.prepareMessage();
  const signature = personalSign(safeAccountAddress, message, BigInt(chainId));
  return [message, signature];
}
```

```
import { hashMessage, Wallet } from "ethers";

export function getMessageHashForSafe(safeAccountAddress: string, message: string, chainId: BigInt) {
    const SAFE_MSG_TYPEHASH = "0x60b3cbf8b4a223d68d641b3b6ddf9a298e7f33710cf3d3a9d1146b5a6150fbca";
    const DOMAIN_SEPARATOR_TYPEHASH = "0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218";
    const domainSeparator = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
        ["bytes32", "uint256", "address"],
        [DOMAIN_SEPARATOR_TYPEHASH, chainId, safeAccountAddress]
    ));
    const encodedMessage = ethers.AbiCoder.defaultAbiCoder().encode(
        ["bytes32", "bytes32"],
        [SAFE_MSG_TYPEHASH, ethers.keccak256(message)]
    );
    const messageHash = ethers.keccak256(ethers.solidityPacked(
        ["bytes1", "bytes1", "bytes32", "bytes32",],
        [Uint8Array.from([0x19]), Uint8Array.from([0x01]), domainSeparator, ethers.keccak256(encodedMessage)]
    ));
    return messageHash;
}

export function personalSign(safeAccountAddress: string, message: string, chainId: BigInt){
  const hash = hashMessage(message);
	const safeMessageHash = await getMessageHashForSafe(safeAccountAddress, message, chainId);
  const signer = new Wallet(process.env.privateKey)
  return signer.signingKey.sign(messageHash).serialized;
}
```

```
service://safe-recovery-service wants you to sign in with your Ethereum account:
0x13D6D891307758afc45EE42C90bFE7636C32088b

I request to retrieve all Social Recovery Module alert subscriptions linked to my account

URI: service://safe-recovery-service
Version: 1
Chain ID: 11155420
Nonce: 0x95e25544f0f05b90c12b92d5a0d29666b99c77a47b00e854
Issued At: 2025-03-13T15:47:08.746Z
```

## Error Handling[​](#error-handling "Direct link to Error Handling")

The API uses standard HTTP status codes to indicate the success or failure of a request. Error responses include a JSON object with the following structure:

```
{
  "error": {
    "code": 404,
    "message": "Registration not found"
  }
}
```

### HTTP Status Codes[​](#http-status-codes "Direct link to HTTP Status Codes")

| Code | Description                                                 |
| ---- | ----------------------------------------------------------- |
| 200  | Success                                                     |
| 400  | Bad Request - Invalid parameters or missing required fields |
| 401  | Unauthorized - Invalid or missing Bearer token              |
| 404  | Not Found - Resource not found                              |
| 429  | Too Many Requests - Rate limit exceeded                     |
| 500  | Internal Server Error - Something went wrong on the server  |

### Common Error Messages[​](#common-error-messages "Direct link to Common Error Messages")

| Message                | Description                                  |
| ---------------------- | -------------------------------------------- |
| Registration not found | The requested registration ID does not exist |
| Invalid signature      | The SIWE signature verification failed       |
| Challenge expired      | The OTP challenge has expired                |
| Invalid challenge      | The OTP code provided is incorrect           |
| Rate limit exceeded    | Too many requests, please try again later    |
