Skip to main content

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.

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

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

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)"
}'
  • 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 construct the message and signature using Sign-In with Ethereum (SIWE).

Submit Confirmation Using OTP

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

POST /auth/submit

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

Get active registration

Fetch the registration of the protected smart account

GET /auth/registrations

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)"

See example guide how to construct the message and signature using Sign in With Ethereum (SIWE)

Delete

Deletes a registration

POST /auth/delete

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)"
}'

See example guide how to construct the message and signature using Sign in With Ethereum (SIWE)

Auth Recovery

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

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
}'

Confirm recovery with OTP challenge

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

POST /auth/signature/submit

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"
}'

How to Sign Messages (SIWE EIP-4361)

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];
}

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

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

Common Error Messages

MessageDescription
Registration not foundThe requested registration ID does not exist
Invalid signatureThe SIWE signature verification failed
Challenge expiredThe OTP challenge has expired
Invalid challengeThe OTP code provided is incorrect
Rate limit exceededToo many requests, please try again later