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.
The following API is in a pre-release phase. Request access here.
/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 a OTP code to later submits the challenge in /auth/submit
POST /auth/register
- Example Request
- Example Response
curl -X POST \
https://api.candide.dev/recoveries/$version/$network/$your_api_key/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",
}
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)
Submits confirmation using OTP
Submits the receive OTP code to confirm ownership of the target channel
POST /auth/submit
- Example Request
- Example Response
curl -X POST \
https://api.candide.dev/recoveries/$version/$network/$your_api_key/auth/submit \
-H 'Content-Type: application/json' \
-d '{
"challengeId":"unique-challenge-id",
"challenge":"123456"
}'
{
"registrationId": "unique-registration-id",
"guardianAddress": "0x...",
}
challengeId
: The unique challenge ID received in the registration response.challenge
: The code received via email/SMS.
Get active registration
Fetch the registration of the protected smart account
GET /auth/registrations
- Example Request
- Example Response
curl -G "https://api.candide.dev/recoveries/$version/$network/$your_api_key/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",
}
]
}
account
: The safe account address.chainId
: The chain id in which your account resides.message
: SIWE (EIP-4361) message statement. Statement:
I request to retrieve all authentication methods currently registered to my account with Safe Recovery Service
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)
Delete
Deletes a registration
POST /auth/delete
- Example Request
- Example Response
curl -X POST \
https://api.candide.dev/recoveries/$version/$network/$your_api_key/auth/delete \
-H 'Content-Type: application/json' \
-d '{
"registrationId":"unique-registration-id",
"message": "siwe(chainId, statement(registrationId))",
"signature": "sign(registrationId, timestamp)"
}'
{
"success": "true"
}
registrationId
: The registration id.message
: SIWE (EIP-4361) message statement. Statement:
I request to remove the authentication method with registration ID {{id}} from my account on Safe Recovery Service
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)
Auth Recovery
Request to recover an account
Request a siganture from the service to recover an account given the new owners and threshold
POST /auth/signature/request
- Example Request
- Example Response
curl -X POST \
https://api.candide.dev/recoveries/$version/$network/$your_api_key/execute \
-H 'Content-Type: application/json' \
-d '{
"account":"0x...",
"newOwners": ["0x...", "0x..."],
"newThreshold": 2,
"chainId": 1
}'
{
"requestId":"unique-signature-request-id",
"requiredVerifications": 1, // minimum number of required challenges that should be submitted for this signature request
"auths": [
{
"challengeId": "unique-challenge-id",
"channel": "email",
"target": "us**@exa****.com"
}
]
}
account
: The smart account address to be recovered.newOwners
: The new owners to the safe accountnewThreshold
: The new threshold to the safe accountchainId
: chain id for the recovery request signature
Confirm recovery with OTP challenge
Request to submits the signature with the provided OTP code challenge and id
POST /auth/signature/submit
- Example Request
- Example Response
curl -X POST \
https://api.candide.dev/recoveries/$version/$network/$your_api_key/auth/signature/submit \
-H 'Content-Type: application/json' \
-d '{
"requestId": "unique-signature-request-id",
"challengeId": "unique-challenge-id",
"challenge": "123456"
}'
{
"success": true,
"signer": "0x...", // available only if sufficient verifications were collected
"signature": "0x...", // available only if sufficient verifications were collected
}
requestId
: The unique ID received in the signature request response.challengeId
: The unique challenge ID specific to a authentication method, received in the signature request response.challenge
: The code received via email/SMS.
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