/v1/public with no user authentication. They are reachable by anyone on the internet, so each one is small, validates strictly and has its own throttle on top of the global /v1 limiter (120 requests per 60 seconds per IP by default).
/v1/public/sign and /v1/public/onboarding are registered before /v1/public in modules/index.ts, so the more specific routers win.
Waitlist
POST /v1/public/waitlist-leads
Stores a lead from the marketing site.
Auth: none.
string
required
Trimmed, 1 to 120 characters.
string
required
Trimmed, 7 to 30 characters, with at least 7 digits.
string
Trimmed, at most 160 characters.
string
default:"landing"
Trimmed, at most 60 characters. Where the lead came from.
- Creates a
WaitlistLeadrow. - Answers the caller.
- After the response is sent, posts the lead as JSON (
id,name,businessName,phone,source,createdAt) to the URL inWAITLIST_WEBHOOK_URL. This forward is best effort. A failure is logged and the stored lead is unaffected.
requestId.
Response: 201.
Signup verification
These four endpoints back the self-serve studio signup. The coach proves a phone number over WhatsApp, and optionally an email, before any account exists. Each successful verification returns a short-lived signed token that the signup later hands toPOST /v1/web/onboarding/bootstrap on the web gateway lane.
Common behaviour (onboarding.service.ts):
- Codes are 6 digits, stored in Redis for 300 seconds.
- A body that fails the schema returns 400 with
{ "ok": false, "code": "VALIDATION" }. Other errors use the standard envelope. - The client IP for throttling is the
x-client-ipheader when present, elsereq.ip. The web app’s proxy sets that header because it terminates the browser connection. - With
TRAINEE_OTP_DEV_MODEon, nothing is sent and the code is returned asdevCode.
POST /v1/public/onboarding/request-otp
Sends a verification code to a phone over WhatsApp.
Auth: none.
string
required
Trimmed, 7 to 30 characters.
registercode in Hebrew, sent from the organization in SMARTSEND_OTP_API_KEY, falling back to SMARTSEND_API_KEY. Unlike the trainee sign-in, this endpoint does not look the phone up first, so it does not reveal whether a number is registered.
Response: 200.
POST /v1/public/onboarding/verify-otp
Checks the code and returns a phone token.
Auth: none.
string
required
The same phone.
string
required
4 to 8 digits.
BETTER_AUTH_SECRET and is valid for 15 minutes (PHONE_TOKEN_TTL_SECONDS).
Response: 200.
POST /v1/public/onboarding/request-email-otp
Sends a verification code to an email address.
Auth: none.
string
required
Trimmed, a valid email, at most 254 characters.
emailOtp mail template in Hebrew.
Response: 200 with { "sent": true } in data.
Errors: VALIDATION (400), RATE_LIMITED (429), SERVICE_UNAVAILABLE (503) when the mail provider refuses or no sender is wired.
POST /v1/public/onboarding/verify-email-otp
Checks the email code and returns an email token.
Auth: none.
string
required
The same email.
string
required
4 to 8 digits.
VALIDATION (400), BAD_REQUEST (400) invalid or expired code, RATE_LIMITED (429).
Document signing
A coach can send a trainee a PDF to sign. The trainee opens a link of the formAPP_WEB_URL/sign/<token> in a browser. That page is served by the web app, which proxies these four endpoints. There is no sign-in: the token in the path is the whole credential.
How the lane differs from the rest of the API:
- Auth. A request without
x-service-idis a direct caller and is counted under its own IP by the global limiter. A request withx-service-idmust carry a valid service signature (see Authentication). In return it skips the global limiter and itsx-client-ipheader is believed as the visitor address. - Body limit. JSON up to 4 MB on this path.
- Shapes. Answers are plain JSON or PDF bytes, not the standard envelope, because the proxy relays them to the page as they are. Every response sets
Cache-Control: no-store(PDFs:private, no-store) andX-Robots-Tag: noindex. - Errors.
{ "error": "<Hebrew message>", "code": "<reason>" }, withdetailswhen the error carries them.codeisdetails.reasonwhen there is one, else the error code. A miss is always 404 with onlyerror, the same body whatever the reason, so the endpoint does not reveal which tokens exist. - Throttle. Per link and per visitor IP, in 600 second windows: view 60 and 600, document 40 and 400, submit 8 and 60. Over the limit is 429 with code
RATE_LIMITED. The throttle is skipped if Redis is unreachable. - Misses. A malformed token, an unknown token, an archived studio and a deleted trainee are all the same 404.
statusOf): pending, signed once completed, and canceled when the assignment is no longer pending, the form was deactivated or the form lost its document.
GET /v1/public/sign/:token
Returns everything the signing page needs to render.
Auth: the link token.
string
required
The signing token from the link.
object
A strict allow-list of branding: name, logo URL and two hex colours. Nothing else from the studio is exposed.
array
Page sizes in PDF points with rotation applied.
string
text or signature.object
page is the zero-based page. x, y, w and h are fractions of that page from its top-left corner.string
Present only on fields the coach filled when sending.
string | null
Set once
status is signed.RATE_LIMITED.
GET /v1/public/sign/:token/document
Streams the source PDF, so the page can render it with pdf.js from its own origin.
Auth: the link token.
string
required
The signing token.
Content-Type: application/pdf and the PDF bytes.
Errors: 404 for a miss or a canceled link. 429. 500 with code INTERNAL when the stored document cannot be read or is not a PDF.
POST /v1/public/sign/:token
Submits the trainee’s values and signatures and produces the signed PDF.
Auth: the link token.
string
required
The signing token.
object
default:"{}"
Text field key to value. Keys at most 100 characters, values at most 5000. At most 200 keys.
object
default:"{}"
Signature field key to a PNG data URL from the signature pad. Each value at most 600,000 characters. The decoded PNG is limited to 400 KB, 4000 pixels per side and 4,000,000 pixels in total. At most 200 keys.
- Refuses a link that is already signed or canceled.
- Rejects any key that is not one of the document’s trainee-filled fields.
- Decodes and checks each signature image.
- Validates the answers with the same form validation as the trainee app, including required fields.
- Renders the signed PDF from the source document, the coach’s values and the trainee’s values and signatures, and stores it.
- Completes the assignment through
FormsService.submitSigning: the same pipeline as an app submit, including theFORM_FILLEDwebhook, which already carriesdocumentUrl. The signing audit (time, IP, user agent, phone, document hash) is saved on the response.
GET /v1/public/sign/:token/signed
Streams the signed PDF once the link is signed.
Auth: the link token.
string
required
The signing token.
Content-Type: application/pdf.
Errors: 404 when the link is not signed yet or for any miss. 429. This endpoint shares the document throttle bucket.
Inbound webhooks
Two more endpoints take no user credential. They are called by SmartSend and verified by atoken query parameter compared with SMARTSEND_WEBHOOK_SECRET. A missing or wrong token returns 401 invalid webhook token.
POST /v1/agent/webhook
Receives one inbound message for the coach assistant.
Auth: token query parameter.
string
required
The webhook token.
string
required
The sender’s phone.
string
required
The SmartSend conversation id.
string
default:""
The message text.
string
The contact name.
AGENT_REPLY queue. The agent is disabled when AGENT_SMARTSEND_API_KEY is empty and can be limited to pilot phones with AGENT_PILOT_PHONES.
Response: 200 in the standard envelope. The contents of data come from the agent service and are not restated here.
Errors: 401 UNAUTHORIZED invalid webhook token. 422 VALIDATION when the body fails the schema.