Three routers are mounted under /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.
What it does:
  1. Creates a WaitlistLead row.
  2. Answers the caller.
  3. After the response is sent, posts the lead as JSON (id, name, businessName, phone, source, createdAt) to the URL in WAITLIST_WEBHOOK_URL. This forward is best effort. A failure is logged and the stored lead is unaffected.
This handler does not use the shared response helpers, so its shapes differ from the standard envelope and carry no requestId. Response: 201.
Errors:

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 to POST /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-ip header when present, else req.ip. The web app’s proxy sets that header because it terminates the browser connection.
  • With TRAINEE_OTP_DEV_MODE on, nothing is sent and the code is returned as devCode.

POST /v1/public/onboarding/request-otp

Sends a verification code to a phone over WhatsApp. Auth: none.
string
required
Trimmed, 7 to 30 characters.
Throttles: 3 sends per phone and 30 per IP in 600 seconds. The message is the WhatsApp template 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.
Errors:

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.
A wrong code increments a failure counter for that phone with a 600 second expiry. On the fifth failure the stored code is deleted and the response is 429, so a 6-digit code cannot be brute-forced inside its lifetime. A correct code deletes both the code and the counter. The phone token is signed with BETTER_AUTH_SECRET and is valid for 15 minutes (PHONE_TOKEN_TTL_SECONDS). Response: 200.
Errors:

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.
Same throttles as the phone lane: 3 sends per email and 30 per IP in 600 seconds. The code is sent with the 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.
Same five-failure rule as the phone lane. Response: 200.
Errors: 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 form APP_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-id is a direct caller and is counted under its own IP by the global limiter. A request with x-service-id must carry a valid service signature (see Authentication). In return it skips the global limiter and its x-client-ip header 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) and X-Robots-Tag: noindex.
  • Errors. { "error": "<Hebrew message>", "code": "<reason>" }, with details when the error carries them. code is details.reason when there is one, else the error code. A miss is always 404 with only error, 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.
A link has one of three states (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.
Response: 200.
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.
Fields and coach values are the ones frozen when the form was sent, not the template’s current ones. Errors: 404 for a miss. 429 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.
Response: 200 with 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.
What the service does:
  1. Refuses a link that is already signed or canceled.
  2. Rejects any key that is not one of the document’s trainee-filled fields.
  3. Decodes and checks each signature image.
  4. Validates the answers with the same form validation as the trainee app, including required fields.
  5. Renders the signed PDF from the source document, the coach’s values and the trainee’s values and signatures, and stores it.
  6. Completes the assignment through FormsService.submitSigning: the same pipeline as an app submit, including the FORM_FILLED webhook, which already carries documentUrl. The signing audit (time, IP, user agent, phone, document hash) is saved on the response.
If a later step fails, uploads from this attempt are removed. Response: 200.
Errors:

GET /v1/public/sign/:token/signed

Streams the signed PDF once the link is signed. Auth: the link token.
string
required
The signing token.
Response: 200 with 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 a token 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.
The handler passes the message to the agent service, which debounces messages and queues a reply job on the 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.