/v1/automation is the lane automation platforms talk to. It was built for Make first, and the same surface works for any HTTP client. The MCP server and the WhatsApp AI assistant also execute their tools through it.
Source: apps/core-api/src/modules/automation-api. Webhook subscriptions under /hooks are in apps/core-api/src/modules/automation-hooks and are documented in Outbound webhooks.
Base URL in production: https://perform-api.otherwise.co.il/v1/automation.
Authentication
A studio API key as a bearer token:requirePartnerKey middleware as the partner lane. Every request resolves to exactly one studio, and all reads and writes are scoped to it. See Partner API and API keys for how keys are created and verified.
Envelope
This lane does not use the standardok and data envelope, and it does not share the global error middleware. Automation clients show a failed step’s message straight to a coach, so every error has to read as a sentence that says what to fix.
Success, always HTTP 200:
Error mapping
automationErrorMiddleware in automation-api.envelope.ts is registered on this router only.
backend/docs/MAKE_MODULE_AGENT_PROMPT.md says every response is HTTP 200. That is not what the code does. Failures carry real status codes. A client should treat a response as failed when success is false, whatever the status.Input coercion
Automation platforms serialize everything as strings and send empty strings for “not filled in”. The schemas inautomation-api.schema.ts coerce far more than the web lane so a coach does not need formula steps to satisfy the validator.
Pagination on list endpoints:
limit (default 50, max 200) and offset (default 0).
Endpoints
Connection
Trainees
GET /trainees/search query: q, status (a ClientStatus), coachId, tag, createdAfter, updatedAfter, limit, offset.
POST /trainees
string
required
string
required
string
required
Normalized to Perform’s canonical form before anything else.
string
string
Your own id for this trainee. The strongest key the API has.
date
string
MALE, FEMALE or OTHER.number
number
string
integer
integer
string[] or comma-separated string
string[]
Up to 20.
string
A plan to assign at creation.
date
integer
Required when the plan does not fix its own price.
integer
Required when the plan does not fix its own duration. The error names the plan and the field.
string
DAYS or MONTHS.string
string
string
WEEKLY, BIWEEKLY or MONTHLY.boolean
default:"false"
Send the WhatsApp invite. Off by default, so a bulk import never texts hundreds of people by surprise.
string
default:"error"
error, return_existing or update.notify field. Creating a trainee reaches them in exactly one way, the WhatsApp invite, which has its own flag.
How duplicates are resolved, in createTrainee:
externalIdis checked first. If a trainee with that external id exists, it is returned withcreated: false. This is what makes a retried scenario safe.- Then the phone, matched through phone variants. What happens next depends on
onDuplicatePhone:errorfails withCONFLICTand a message naming the existing trainee,return_existingreturns it,updateapplies the body to it. - Otherwise a new trainee is created inside
withTraineeRoom, so the plan’s trainee limit applies.
{ trainee, created }.
External ids are stored namespaced in Client.externalUserId as automation:<studioId>:<externalId>. Responses and webhook payloads hand back only the part the caller supplied.
DELETE /trainees/:id
Body: confirm, default false. Without confirm: true nothing is deleted and the response is a success envelope explaining that deleting a trainee erases their whole history and cannot be undone. That is deliberate. A scenario wired without reading the warning gets an explanation, not a validation error that hides why nothing happened.
Subscriptions
Note the singular key on assign and the plural on the three lifecycle calls. That is the API.
When
startedOn is omitted, the new plan starts when the trainee’s last live plan ends, not today. Today would land inside the running plan and be refused by the overlap guard, which is what every renewal of an active trainee was hitting.
Programs
Forms
POST /trainees/:id/forms/assign body:
notify deliberately has no default:
For a document to sign (
PDF_SIGNATURE) nothing is sent to the trainee app. The response carries signingUrl, the trainee’s personal signing link, and the message is “Signing link created”. The caller delivers that link itself.
Weights
Messages
POST /trainees/:id/message with message (1 to 1000 characters) sends a push notification to the trainee app. It returns { sent, selected, unreachable, disabled }. sent counts devices that actually accepted. sent: 0 is not an error. It means no reachable device.
Tasks
Plans
GET /products lists the studio’s plans. Query search, limit, offset. priceAgorot and durationValue can be null, which means the plan fixes no value and it is set per trainee. 0 means free. They are different facts.
Dropdown sources
These return shortid and label lists for a client’s dropdowns:
Webhooks
POST /hooks, GET /hooks, DELETE /hooks/:id. See Outbound webhooks.
Pagination shapes
Five endpoints return{ items, total, limit, offset, hasMore }: /trainees/search, /tasks, /products, /program-templates and /forms.
Four return only { items } and cannot page: /trainees/:id/programs, /trainees/:id/form-responses, /trainees/:id/weights and /trainees/:id/subscriptions.
Notifications from this lane
Bulk automation must not spray push notifications at real trainees. The notifier is injected at construction and not per call, so the router keeps two service instances, one loud and one silent, and picks per request from thenotify flag.
“Silent” only means no instant push from this process. A form assigned silently is still picked up by the hourly pending-form notifier, unless the request said notify: false outright.
Audit
Writes are recorded inAuditLog with the API key id as the actor. Actions written by this lane: automation.trainee.create, automation.trainee.delete, automation.subscription.assign, automation.program.assign, automation.task.create, and from the hooks module automation.hook.register and automation.hook.remove.
Plan limits
Creating a trainee goes through the studio’s plan guard. A studio at its trainee limit gets HTTP 403 with a message saying so.Existing documentation
backend/docs/AUTOMATION_API.md is a prose guide to this lane and backend/docs/AUTOMATION_API_PLAN.md is the original plan. Where they disagree with the Zod schemas and the routes, the code is right.