The automation lane is what automation platforms talk to. It was built for Make.com first. The same surface works for any tool that can send HTTP requests, and the MCP endpoint is a thin wrapper over it. Mount: /v1/automation, router automationApiRouter in src/modules/automation-api/automation-api.routes.ts. Webhook handlers come from src/modules/automation-hooks/. Lane: automation. Every route runs requirePartnerKey, the same studio API key check as the Partner API. A key resolves to one studio and has no scopes. Keys are created by an OWNER or HEAD_COACH through POST /v1/web/studios/current/api-keys, documented on the partner page.
Rate limit: the global /v1 limiter only, 120 requests per 60 seconds per IP by default. The module adds no limiter of its own.

Response envelope

This lane does not use the standard { ok, data } envelope. It has its own, defined in automation-api.envelope.ts, because automation tools show the response message to a coach when a step fails. Success is always HTTP 200, including creates:
Errors carry one readable sentence and no code:
The router registers its own error handler, automationErrorMiddleware, which maps errors like this:
A missing resource is 400 on this lane, not 404. Branch on success and the status, not on an error code.
A bad or missing key is also answered in this shape: 401 with missing partner api key or invalid partner api key.

Input handling

Automation tools serialize everything as strings and send empty strings for fields left blank. The schemas in automation-api.schema.ts coerce accordingly: No field on this lane uses null to clear a value. Leaving a field blank on an update leaves it unchanged. Lists use offset pagination: limit (default 50, max 200) and offset (default 0). Paginated responses carry items, total, limit, offset and hasMore.

Notifications

Writes on this lane can reach real trainees, so each endpoint says what it sends:
  • Creating a trainee sends nothing unless sendWhatsappInvite is true. An onboardingFormId does send that form with its normal push.
  • Assigning a plan or a program pushes a notification by default. Send notify: false for bulk imports.
  • Assigning a form has three states, described on that endpoint.

Auditing

Writes record an AuditLog row with the API key id as the actor. Actions: automation.trainee.create, automation.trainee.delete, automation.subscription.assign, automation.program.assign, automation.task.create, automation.hook.register and automation.hook.remove.

Connection

GET /v1/automation/validate

The connection test. Make calls it when a coach adds the connection. Auth: studio API key. Response: 200.
Errors: 400 studio not found.

Trainees

A trainee object on this lane has these fields (traineeSelect in the repository, shaped by present):
externalId is the id the caller supplied at creation, or null. Internally it is stored on Client.externalUserId as automation:<studioId>:<externalId>, and the internal column is never returned.

POST /v1/automation/trainees

Creates a trainee, optionally with a subscription, coaches and forms. Auth: studio API key.
string
required
Non-empty.
string
required
Non-empty.
string
required
Any common format. Must normalize to at least 9 characters.
string
A valid email.
string
A stable id from the source system. This is the idempotency key. See below.
string
A date.
string
MALE, FEMALE or OTHER.
number
Positive.
number
Positive.
string
Free text.
number
Non-negative integer.
number
Non-negative integer.
string | string[]
A comma-separated string or an array. At most 50 tags.
string[]
At most 20 coach ids, from GET /v1/automation/rpc/coaches.
string
A plan id. Also creates the subscription.
string
Subscription start date.
number
Non-negative integer, in agorot. Overrides the plan’s price for this trainee.
number
Positive integer. How long the plan runs for this trainee. Required when the plan does not fix a duration.
string
DAYS or MONTHS.
string
An intake form to send right after creation. A document to sign is refused here.
string
The recurring check-in form for this trainee.
string
WEEKLY, BIWEEKLY or MONTHLY.
boolean
default:"false"
Send the WhatsApp app invite to the trainee.
string
default:"error"
error, return_existing or update. What to do when a trainee with this phone already exists.
Duplicate handling, in order:
  1. externalId match. If a trainee with this externalId already exists in the studio, it is returned with created: false. Nothing is updated. This makes a retried bundle or a re-run scenario safe.
  2. Phone match. Otherwise, if a trainee with the same phone exists, onDuplicatePhone decides: error returns 409, return_existing returns the trainee, and update applies the profile fields from the body (names, email, birth date, gender, height, weight, goal, goals, tags, coaches) and returns the updated trainee. Subscription fields are not applied to an existing trainee.
  3. Create. Otherwise the trainee is created through the same clients.create the coach web app uses, with the API key id as the actor. Plan limits of the studio’s billing plan apply.
In cases 1 and 2, if onboardingFormId is given and that trainee has no pending assignment of it, the form is sent. A failure there is logged and does not fail the call. Response: 200.
trainee is the full trainee object, shortened here. When an existing trainee is returned, created is false and the message is Trainee already existed, returned the existing one. Errors:

GET /v1/automation/trainees/search

Searches trainees. Auth: studio API key.
string
A name or a phone. A value made only of digits, spaces, +, - and brackets is treated as a phone and matched on its variants. Anything else is a case-insensitive name match.
string
ACTIVE, PAUSED, CHURN_RISK, CHURNED or ARCHIVED.
string
Only trainees assigned to this coach.
string
Only trainees with this exact tag.
string
A date. Created at or after it.
string
A date. Updated at or after it. Useful for polling.
number
default:"50"
1 to 200.
number
default:"0"
Non-negative.
Deleted trainees are excluded. Ordered by creation time, newest first. Response: 200.
Errors: 400 for invalid query values.

GET /v1/automation/trainees/by-phone/:phone

Finds one trainee by phone. Auth: studio API key.
string
required
At least 3 characters. Any common format.
Response: 200 for a hit and for a miss.
A miss returns { "found": false, "trainee": null } with the message No trainee with that phone. Errors: 400 when the value does not normalize to a phone of at least 6 characters.

GET /v1/automation/trainees/:id

Returns one trainee with their live subscriptions. Auth: studio API key.
string
required
The trainee id.
Response: 200 with data: { trainee, subscriptions }. subscriptions is the list from ClientSubscriptionsService.listLive, the same rows as GET /v1/automation/trainees/:id/subscriptions. Errors: 400 when no trainee with that id exists in the studio.

PATCH /v1/automation/trainees/:id

Updates profile fields. Auth: studio API key.
string
required
The trainee id.
Body fields, all optional: firstName, lastName, phone, email, birthDate, gender, heightCm, weightKg, goal, stepsGoal, waterGoalMl, tags, coachIds, updateFormId, updateCadence. Types and limits are the same as on create. tags and coachIds replace the stored lists when sent. Blank fields are ignored. Subscription and plan fields cannot be changed here. Subscriptions are the source of truth for a trainee’s plan and status. Use the subscription endpoints. Response: 200 with data: { trainee } and the message Trainee updated. Errors: 400 for an unknown trainee or invalid values. Errors from the shared clients service pass through with their own status.

DELETE /v1/automation/trainees/:id

Deletes a trainee and their history. This cannot be undone. Auth: studio API key.
string
required
The trainee id.
boolean
default:"false"
Must be true to delete.
Without confirm: true nothing is deleted and the call still returns 200, with an explanation in message. That is deliberate: a scenario wired without reading the warning gets a clear sentence, not a validation error. Response: 200.
Without confirmation, data is { "deleted": false } and the message starts with Nothing was deleted. Errors: 400 for an unknown trainee.

Subscriptions

GET /v1/automation/products

Lists the studio’s active plans. Auth: studio API key.
Case-insensitive match on the plan name.
number
default:"50"
1 to 200.
number
default:"0"
Non-negative.
Response: 200.
A null priceAgorot or durationValue means the plan does not fix that value and it is decided per trainee. Errors: 400 for invalid query values.

GET /v1/automation/trainees/:id/subscriptions

Lists the trainee’s live subscriptions. Auth: studio API key.
string
required
The trainee id.
Response: 200 with data: { items }. Each item is a ClientSubscription row as returned by repo.liveByClient. The exact column list is defined in the client-subscriptions module and is not restated here. Errors: 400 for an unknown trainee.

POST /v1/automation/trainees/:id/subscription

Assigns or renews a plan. Auth: studio API key.
string
required
The trainee id.
string
required
The plan id.
string
Start date.
number
Non-negative integer. Overrides the plan’s price.
number
Positive integer. Required when the plan does not fix a duration.
string
DAYS or MONTHS.
boolean
default:"true"
false uses a silent notifier so the trainee gets no push.
Calls ClientSubscriptionsService.addSubscription, the same code path as the coach web app, including the subscription-assigned task hook. Response: 200 with data: { subscription } and the message Plan assigned. Errors: 400 for an unknown trainee or plan, or a plan with no duration and none given. The message names the plan.

POST /v1/automation/trainees/:id/subscription/freeze

Freezes the trainee’s running plan. Auth: studio API key.
string
required
The trainee id.
No body. Response: 200 with data: { subscriptions }, the live subscriptions after the change, and the message Plan frozen. Errors: 400, for example plan is already frozen or client has no active plan.

POST /v1/automation/trainees/:id/subscription/reactivate

Reactivates a frozen plan. Auth: studio API key.
string
required
The trainee id.
No body. Response: 200 with data: { subscriptions } and the message Plan reactivated. Errors: 400, for example plan is not frozen.

POST /v1/automation/trainees/:id/subscription/cancel

Cancels one subscription. Auth: studio API key.
string
required
The trainee id.
string
required
The subscription to cancel, from the subscriptions list.
Response: 200 with data: { subscriptions } and the message Plan canceled. Errors: 400 for an unknown trainee or a missing subscriptionId. Refusals from the subscription service pass through.

Programs

GET /v1/automation/program-templates

Lists the studio’s program templates. Auth: studio API key. Query: search, limit, offset, as on the products list. Response: 200 with paginated items of { id, name, type }. type is TRAINING or NUTRITION. Errors: 400 for invalid query values.

GET /v1/automation/trainees/:id/programs

Lists the trainee’s programs, newest first, at most 50. Auth: studio API key.
string
required
The trainee id.
Response: 200.
Errors: 400 for an unknown trainee.

POST /v1/automation/trainees/:id/programs/assign

Assigns a training or nutrition program from a template. Auth: studio API key.
string
required
The trainee id.
string
required
The program template id.
string
Start date.
boolean
default:"true"
false suppresses the push to the trainee.
boolean
default:"false"
Allow assigning the same template again while a program from it is active.
Unless replaceExisting is true, the call is refused when the trainee already has an ACTIVE program whose sourceTemplateId is this template. That keeps a re-run scenario from stacking duplicates. The assignment itself goes through ProgramTemplatesService.assign, and a first plan starts a subscription that was waiting for one. Response: 200 with data: { program } and the message Program assigned. Errors:

Forms

GET /v1/automation/forms

Lists the studio’s forms that can be sent: active and published. Auth: studio API key. Query: search, limit, offset. Response: 200 with paginated items of { id, name, type }. type is INTAKE, CHECK_IN, ONE_TIME or PDF_SIGNATURE. Errors: 400 for invalid query values.

POST /v1/automation/trainees/:id/forms/assign

Sends a form to a trainee, or creates a signing link for a document to sign. Auth: studio API key.
string
required
The trainee id.
string
required
The form id.
string
When the form becomes visible to the trainee.
boolean
Three states. true: push the notification right away. false: never announce this form. Blank or omitted: no instant push, and the hourly pending-form notifier announces it once under the studio’s settings, when the trainee can open it.
string
A note shown with the form. At most 500 characters.
object | array | string
Documents to sign only. Values for the fields the coach fills at send time, keyed by field key. Accepted as an object, as an array of { key, value }, or as a JSON string of either. Numbers and booleans are converted to strings. Ignored for other form types.
For a regular form the assignment appears in the trainee app. For a PDF_SIGNATURE form Perform sends nothing: the response carries signingUrl, and the caller delivers that link to the trainee. An inactive form is refused. Response: 200.
assignment is the FormAssignment row without its token, prefill and frozen schema, shortened here. For a regular form signingUrl is null and the message is Form sent to the trainee. Errors: 400 for an unknown trainee or form, an inactive form (form template is not active), or invalid fieldValues.

GET /v1/automation/trainees/:id/form-responses

Lists the trainee’s form responses, newest first. Auth: studio API key.
string
required
The trainee id.
Query: limit, offset. search is accepted and ignored. Response: 200.
This list has no total or hasMore. Page until a short page comes back. Answers are not included. Use a FORM_FILLED webhook to receive them. Errors: 400 for an unknown trainee.

Weights and messages

GET /v1/automation/trainees/:id/weights

Lists the trainee’s weigh-ins, newest first. Auth: studio API key.
string
required
The trainee id.
Query: limit, offset. Response: 200 with data: { items }, each { id, weightKg, recordedAt, source }. No total or hasMore. Errors: 400 for an unknown trainee.

POST /v1/automation/trainees/:id/weights

Logs a weigh-in. Auth: studio API key.
string
required
The trainee id.
number
required
Greater than 0, at most 500.
string
A date. Defaults to now.
Creates a WeightEntry with source: "automation" and sets Client.weightKg to this value in the same transaction, whatever the recordedAt. Response: 200.
Errors: 400 for an unknown trainee or an invalid weight.

POST /v1/automation/trainees/:id/message

Sends a push message to the trainee’s app, as the studio. Auth: studio API key.
string
required
The trainee id.
string
required
1 to 1000 characters. The token [שם] is replaced with the trainee’s first name.
Goes through clients.sendPushMessage as a TRAINER_MESSAGE notification, so the studio’s settings for that type apply. When a device accepts the message, a MESSAGE entry is added to the trainee’s timeline. A trainee with no registered device receives nothing and the call still succeeds. Response: 200 with the message Message sent and the send result from the clients service in data. Its exact fields are defined there and are not restated here. Errors: 400 for an unknown trainee or an empty message.

Tasks

Tasks are InboxItem rows on the coach work board.

GET /v1/automation/tasks

Lists tasks, newest first. Auth: studio API key.
string
OPEN, SNOOZED, DONE or DISMISSED.
string
Only tasks about this trainee.
string
Only tasks for this coach.
number
default:"50"
1 to 200.
number
default:"0"
Non-negative.
Response: 200.
Errors: 400 for invalid query values.

POST /v1/automation/tasks

Creates a task for a coach. Auth: studio API key.
string
required
Non-empty.
string
The trainee the task is about. Must exist in the studio.
string
At most 2000 characters.
string
A date.
number
default:"1"
Integer 0 to 5.
string
The coach to assign.
The task is created with type MANUAL and source automation. Response: 200 with data: { task } in the list item shape and the message Task created. Errors: 400 for a missing title or an unknown trainee.

PATCH /v1/automation/tasks/:id

Updates a task’s status, snooze time or priority. Auth: studio API key.
string
required
The task id.
string
OPEN, SNOOZED, DONE or DISMISSED.
string
A date.
number
Integer 0 to 5.
Response: 200 with data: { task } and the message Task updated. Errors: 400 when no task with that id exists in the studio.

Webhooks

An automation tool registers a webhook when a scenario with an instant trigger is switched on, and deletes it when the scenario is switched off. Hooks belong to the API key that created them. One event exists today: FORM_FILLED. It fires for every form submission in the studio, from the trainee app and from the public signing page.

POST /v1/automation/hooks

Registers a webhook. Auth: studio API key.
string
required
The URL to deliver to. Trimmed, at most 2048 characters. Must be https on a public host. localhost, loopback, private ranges, link-local addresses and .local or .internal hosts are refused.
string
default:"FORM_FILLED"
The only value is FORM_FILLED.
string
Narrow the hook to one form. Blank means every form.
A key can hold at most 50 hooks. The response includes a secret, 48 hex characters, used to verify deliveries. It is returned only here. Response: 200.
Errors: 400 for an invalid URL (targetUrl must be an https URL on a public host), an unknown form (form not found), or when the key already has 50 webhooks.

GET /v1/automation/hooks

Lists the webhooks registered by this API key. Auth: studio API key.
string
FORM_FILLED.
Response: 200 with data: { hooks }. Each hook has the fields above without secret. lastDeliveryAt and lastStatus show the most recent delivery attempt. Errors: 400 for an unknown event.

DELETE /v1/automation/hooks/:id

Removes a webhook. Auth: studio API key.
string
required
The hook id. Only hooks created by this key can be removed.
Response: 200.
Errors: 400 webhook not found.

Delivery

Deliveries are queued on the BullMQ queue AUTOMATION_HOOK_DELIVER and sent by hook-delivery-worker.ts, off the request path. A trainee’s submission never waits on a webhook. Each delivery is a POST with these headers: Verify by computing the same HMAC over the exact bytes received:
Retry rules:
  • The request times out after 10 seconds.
  • A 2xx response is a success.
  • A 4xx response other than 429 is final. The delivery is dropped.
  • Anything else (5xx, 429, a timeout or a network error) is retried, up to 5 attempts in total with exponential backoff starting at 10 seconds.
  • Every attempt updates the hook’s lastDeliveryAt and lastStatus. Failures increment an internal counter. The code does not deactivate a hook after repeated failures.
  • A hook whose studio is archived, or that has been deleted, receives nothing.

FORM_FILLED payload

Built by buildFormFilledPayload in automation-hooks.dispatcher.ts:
string
Lower case form_filled in the body. The header carries FORM_FILLED.
number
The ordinal of this submission among all of the trainee’s submitted forms in the studio, of any form type. Computed at send time, not stored.
string | null
The signed PDF for a document to sign. null for every other form.
string | null
The id the caller supplied when creating the trainee through this lane.
array
One entry per answerable field in the order the trainee saw them, with labels, so a scenario can map a field without a second call. value is null for an unanswered field.
These feed dynamic dropdowns in Make modules. Each returns a plain array in data, with no pagination. The first five return { id, value } items, which map onto Make’s label and value pair as item.value and item.id.

GET /v1/automation/rpc/products

Lists the studio’s active plans for a dropdown. Auth: studio API key. No parameters. Ordered by name. Response: 200. Items are { id, value }. value is the plan name followed by its price in whole shekels, or by a Hebrew “price per trainee” note for a plan with no fixed price. Errors: none beyond auth.

GET /v1/automation/rpc/coaches

Lists the studio’s active coaches for a dropdown. Auth: studio API key. No parameters. Ordered by name. Response: 200. Items are { id, value }. value is the coach name. Errors: none beyond auth.

GET /v1/automation/rpc/forms

Lists the forms that can be sent: active and published. Auth: studio API key. No parameters. Ordered by name. Response: 200. Items are { id, value }. value is the form name with its type in brackets, for example Weekly check-in (CHECK_IN). Errors: none beyond auth.

GET /v1/automation/rpc/program-templates

Lists every program template of the studio. Auth: studio API key. No parameters. Ordered by name. Response: 200. Items are { id, value }. value is the template name with its type in brackets. Errors: none beyond auth.

GET /v1/automation/rpc/tags

Lists the tags in use on the studio’s trainees. Auth: studio API key. No parameters. Tags are collected from up to 5000 trainees that are not deleted. Response: 200. Items are { id, value }, both set to the tag. Errors: none beyond auth.

GET /v1/automation/rpc/form-fields

Lists the fields a coach fills when sending one document to sign. It feeds the fieldValues input of the form assign module. Auth: studio API key.
string
A PDF_SIGNATURE form id. Without it, for an unknown id, or for any other form type, the result is an empty array.
Response: 200. Items use different keys from the other sources: value is the field key and label is the field label followed by the key in brackets.
Errors: none beyond auth.

Differences from the older notes

backend/docs/AUTOMATION_API.md is behind the code in these places:
  • It says there are no triggers. The lane now has webhook registration (/hooks) with the FORM_FILLED event.
  • It lists five dropdown sources. There is a sixth, /rpc/form-fields.
  • forms/assign also accepts fieldValues and can return a signingUrl for documents to sign.
  • It lists 401, 409 and 429. The handler also maps 403 and 503, and turns every not-found into 400.
  • form-responses and weights lists are not fully paginated: they return items only.