POST
/
api
/
v1
/
auth
/
register
/
email
Register (email + password)
curl --request POST \
  --url https://api.example.com/api/v1/auth/register/email
import requests

url = "https://api.example.com/api/v1/auth/register/email"

response = requests.post(url)

print(response.text)
const options = {method: 'POST'};

fetch('https://api.example.com/api/v1/auth/register/email', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/auth/register/email",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
package main

import (
"fmt"
"net/http"
"io"
)

func main() {

url := "https://api.example.com/api/v1/auth/register/email"

req, _ := http.NewRequest("POST", url, nil)

res, _ := http.DefaultClient.Do(req)

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.post("https://api.example.com/api/v1/auth/register/email")
.asString();
require 'uri'
require 'net/http'

url = URI("https://api.example.com/api/v1/auth/register/email")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)

response = http.request(request)
puts response.read_body

Overview

Starts a new registration from the Create account screen. The client collects the user’s firstName, lastName, email, password, an optional dateOfBirth, and explicit termsAccepted + privacyAccepted consent (both must be true). Names should be entered exactly as they appear on the tenancy agreement, since they are later matched against the uploaded tenancy document. On success the account is created with emailVerified: false, the profile + consent fields are persisted (consent timestamps recorded), and a 6-digit one-time verification code is sent to the supplied email via Resend. The client must then call POST /verify-email with the code to log the user in — this endpoint does NOT return tokens. If the email is already registered with the same password, this is treated as an idempotent retry: a fresh verification code is dispatched and userId returns null. The response is intentionally identical to the new-account case so a caller cannot tell the email already existed. If the email is registered with a different password the request is rejected with 409 EMAIL_ALREADY_REGISTERED. If the email is registered via OAuth (Google / Apple) the request is rejected with 409 ACCOUNT_EXISTS_VIA_OAUTH so the client can redirect to the appropriate sign-in flow.

Authentication

None required.

Path parameters

None.

Query parameters

None.

Request body

FieldTypeRequiredAllowed valuesExample
firstNamestringyes1..100 chars after trim. Use the name as it appears on the tenancy agreement.Alice
lastNamestringyes1..100 chars after trim.Smith
emailstringyesRFC 5322 valid, 5..254 chars, lowercased server-sidealice@example.com
passwordstringyes8..200 chars; must contain at least one letter AND one digitp4ssword1
dateOfBirthstringnoISO calendar date YYYY-MM-DD; applicant must be 18 or older. Omit to skip.1990-06-15
termsAcceptedbooleanyesMust be true — the user ticked “I agree to the Terms & Conditions”.true
privacyAcceptedbooleanyesMust be true — the user ticked “I agree to the Privacy Policy”.true

Example payload

{
  "firstName": "Alice",
  "lastName": "Smith",
  "email": "alice@example.com",
  "password": "p4ssword1",
  "dateOfBirth": "1990-06-15",
  "termsAccepted": true,
  "privacyAccepted": true
}

Response — 201 Created

FieldTypeNotesExample
userIdstring | nullThe new user’s ID, or null on an idempotent retry. Clients must treat both as success.usr_01HZQ7K3M4N5P6Q7R8S9T0V1W2
emailVerificationRequiredbooleanAlways true for this endpoint. Hint to the client to prompt for the OTP.true

Example response

{
  "userId": "usr_01HZQ7K3M4N5P6Q7R8S9T0V1W2",
  "emailVerificationRequired": true
}

Error responses

StatusCodeMeaning
400VALIDATION_FAILEDMissing/blank firstName or lastName; email malformed; password too short or missing a letter/digit; dateOfBirth not YYYY-MM-DD or under 18; termsAccepted or privacyAccepted not true.
409EMAIL_ALREADY_REGISTEREDThe email exists with a different password. Use /login or /forgot-password.
409ACCOUNT_EXISTS_VIA_OAUTHThe email is bound to a Google / Apple OAuth identity. Use the corresponding OAuth flow.
503MAIL_NOT_CONFIGUREDResend API key is missing in this environment.

Example error — 409

{
  "type": "https://api.swappr.co.uk/errors/email-already-registered",
  "title": "Email already registered",
  "status": 409,
  "code": "EMAIL_ALREADY_REGISTERED",
  "detail": "An account with this email already exists",
  "instance": "/api/v1/auth/register/email",
  "requestId": "01HZQ7K3M4N5P6Q7R8S9T0V1W2"
}

Side effects

  • Inserts a new row into the users collection with emailVerified: false, the supplied firstName / lastName / dateOfBirth, and termsAccepted / privacyAccepted set to true with their acceptance timestamps (idempotent retry skips insert).
  • Inserts a row into the otp_codes collection (purpose: 'email_verify', 5-min TTL). Any prior outstanding code for this email is invalidated.
  • Sends an email via Resend with the 6-digit code.

See also

curl

curl -X POST https://api.swappr.co.uk/api/v1/auth/register/email \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Alice",
    "lastName": "Smith",
    "email": "alice@example.com",
    "password": "p4ssword1",
    "dateOfBirth": "1990-06-15",
    "termsAccepted": true,
    "privacyAccepted": true
  }'