AI Agent
Drive the AI agent that builds and edits your project, in single- or multi-prompt mode.
The AI agent builds and modifies your project from natural-language prompts. Agent runs are asynchronous — you start a run, then poll for status. All endpoints require the api-key header.
#Run AI Agent
/api/v1/vcaas/projects/:projectId/agent/startUses creditsStart the AI agent with a prompt to build or modify your project. Supports two modes:
Single-prompt (default, recommended): omit
multiPrompt. One prompt, typically 5 to 40 minutes and 6 to 46 credits.5 to 40 minutes is the full envelope a run can take, from a one-line tweak to a large build, and it is also what the API's own
messagefield quotes. Most prompts land well inside it; use the upper figure if you are writing a timeout.Multi-prompt (rare, opt-in): include
multiPrompt. A sequential batch that runs unsupervised. Each prompt still takes 5 to 40 minutes and 6 to 46 credits, so a 10-prompt batch can run for several hours and burn hundreds of credits.
Budget 6 to 46 credits per prompt. That is the range to show in your own UI, and it is what a normal run lands in.
The exact price is metered, not fixed: 6 credits are taken when the run starts, and the rest is charged when it finishes, from how much work the run actually turned out to be. Two things sit outside the typical range and are worth knowing about before they surprise you:
- A run that uses its entire time budget (the agent works until it is stopped at its ceiling) is billed a flat 50 credits, or 60 on a healthy balance, for the whole prompt.
- A failed run is refunded, so a prompt that produced nothing does not cost you the full price.
The same numbers apply whether the run was started by POST /projects/launch or POST /agent/start — it is the same run.
The API only refuses a run when your balance is below its hard minimum, which is far below the price of a prompt. That is not the number to budget against. A run started with less than it ends up costing will drain the balance to zero and the shortfall is absorbed, but the next one is refused — so keep a working balance comfortably above the 6-to-46 a prompt costs rather than topping up to the floor.
Only use multi-prompt when a single job genuinely requires several sequential AI runs AND the user has explicitly accepted the cost and time. For everything else, send one prompt at a time — it is faster, cheaper, and lets the user steer between steps. Multi-prompt is unsupervised: no human approval step, no pause between prompts. If unsure, do NOT pass multiPrompt.
Returns immediately with status "init". Poll GET /agent/status every 10–15s and show realtimeConversation messages live. Single-prompt: complete when status is "done". Multi-prompt: poll multiPrompt.status until "done"/"cancelled" — the per-prompt status flips between "init" and "done" repeatedly. After every prompt completes you MUST refresh the preview URL via GET /projects/:projectId → developmentUrlFieldToUse (default temporalDevelopmentProjectUrl if null/undefined).
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project to run the agent on |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
prompt | string | Yes¹ | Single-prompt: the user instruction. Multi-prompt + letTotalumDecide: the high-level goal Totalum breaks down. Multi-prompt + prompts[]: ignored. |
inputFiles | array | No | Reference images/files for the agent |
inputFiles[].name | string | Yes | File name |
inputFiles[].imageDescription | string | Yes | Description of the image content |
inputFiles[].url | string | Yes | Public URL or uploaded file URL |
multiPrompt | object | No | Opt into the unsupervised multi-prompt batch mode. Omit for normal use. |
multiPrompt.prompts | string[] | No² | Up to 50 prompts to run sequentially. Each item must be a non-empty string. |
multiPrompt.letTotalumDecide | boolean | No² | When true, Totalum plans the prompt list from the top-level prompt. |
model | string | No | AI model for the run. Totalum already routes each prompt to the best model automatically — set this only if you are sure of what you are doing. "opus" (default, the standard behaviour) or "sonnet" (faster and cheaper per token, less capable on hard tasks). A value that is not available is ignored, the default applies, and the reason is returned in data.warnings — the run still starts. |
effort | string | No | How hard the agent thinks and how thoroughly it works: "low", "medium", "high" or "xhigh". Totalum already picks the effort for each prompt — set this only if you are sure. Omit to use the automatic choice. An invalid value is ignored and reported in data.warnings. |
fastMode | boolean | No | Fast mode: same Opus quality, up to 2.5× faster, higher cost per token (billed through the metered part of the price). Opus only — ignored (with a data.warnings entry) when model is "sonnet" or when the value is not a boolean. |
¹ prompt is required unless multiPrompt.prompts is provided (then it is ignored).
² When multiPrompt is present, exactly one of prompts (non-empty array, ≤ 50 items) or letTotalumDecide=true must be set.
model, effort and fastMode apply to every run the request starts, including each prompt of a multi-prompt batch. They only change how the agent runs — the prompt, the project and the results are otherwise identical. When you send none of them, Totalum's built-in routing already picks the best model and effort for each prompt (for example a lighter, cheaper model for questions and small changes), so most integrations should leave them out. Override only when you are sure: sonnet to force the cheaper model, fastMode when you are iterating live and want the answer sooner rather than cheaper. These options can never make a request fail: anything unavailable or invalid is dropped, the defaults apply, and data.warnings tells you what was ignored and why.
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | The project ID |
data.status | string | Always "init" on success — for both single and multi-prompt mode |
data.message | string | Instructions on how to poll for status (multi-prompt includes the cost/time warning) |
data.warnings | string[] | undefined | Present only when a run option was ignored (unknown model, invalid effort, fastMode with a non-opus model or a non-boolean). One human-readable sentence per ignored value. The run started normally with the defaults. |
data.multiPrompt | object | undefined | Present only when the request had multiPrompt. Detailed batch status is on GET /agent/status under multiPrompt. |
data.multiPrompt.totalPrompts | number | undefined | Known when the caller provided prompts; absent while Totalum is still planning a letTotalumDecide batch. |
Example request
# Single-prompt (recommended)
curl -X POST -H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"prompt":"Create a landing page with a contact form","inputFiles":[]}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/start
# Single-prompt on Sonnet with a lower effort level (quick question / small change)
curl -X POST -H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"prompt":"Change the primary button colour to blue","model":"sonnet","effort":"medium"}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/start
# Single-prompt on Opus with fast mode (same quality, faster, pricier)
curl -X POST -H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"prompt":"Add a contact form that emails me","fastMode":true}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/start
# Multi-prompt with explicit list
curl -X POST -H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"multiPrompt":{"prompts":["Set up auth","Add a Stripe checkout","Wire up the dashboard"]}}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/start
# Multi-prompt with Totalum planning
curl -X POST -H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"prompt":"Build a full SaaS billing system with auth, Stripe, and a customer portal","multiPrompt":{"letTotalumDecide":true}}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/start{
"errors": null,
"data": {
"projectId": "my-app",
"status": "init",
"message": "Process started, can take from 5 to 40 minutes. Fetch GET .../agent/status every 10-15 seconds to track progress."
}
}Multi-prompt response also includes multiPrompt:
{
"errors": null,
"data": {
"projectId": "my-app",
"status": "init",
"message": "Multi-prompt run started with 3 prompts. ⚠️ Multi-prompt mode is expensive and slow — only use it when a job genuinely requires multiple sequential AI runs. Each prompt typically takes 5 to 40 minutes and costs 6 to 46 credits...",
"multiPrompt": { "totalPrompts": 3 }
}
}{
"errors": { "errorCode": "AGENT_RUNNING", "errorMessage": "An agent is already running on this project" },
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
MISSING_PROMPT | 400 | prompt is required (single-prompt mode, or letTotalumDecide=true — it is the planner goal) |
INVALID_MULTI_PROMPT | 400 | multiPrompt was set but neither (or both) of prompts[] and letTotalumDecide=true were provided |
TOO_MANY_PROMPTS | 400 | multiPrompt.prompts exceeds the 50-item limit |
INVALID_PROMPT_ITEM | 400 | A multiPrompt.prompts entry is not a non-empty string |
AGENT_RUNNING | 409 | An agent is already running on this project |
AUTO_EXECUTION_ALREADY_ACTIVE | 409 | A previous multi-prompt batch is still active (executing/paused/awaiting approval). Cancel it first. |
DEPLOYMENT_RUNNING | 409 | Cannot start agent while a deployment is in progress |
RECOVERY_RUNNING | 409 | Cannot start agent while a version recovery is in progress — poll versionRecovery until null, then retry |
REBUILD_RUNNING | 409 | Cannot start agent while a rebuild is in progress — poll rebuild/status until it is no longer "rebuilding", then retry |
GITHUB_PULL_RUNNING | 409 | Cannot start agent while a GitHub pull is rebuilding the project — poll github/pull-status, then retry |
IMPORT_IN_PROGRESS | 409 | Cannot start agent while a project import is in progress — poll importInProgress until null, then retry |
INSUFFICIENT_CREDITS | 402 | Balance is below the minimum needed to start a run. A typical prompt costs 6 to 46 credits, so keep a working balance well above the floor — multi-prompt needs many times more |
PROMPT_SECURITY_VIOLATION | 400 | Prompt failed security validation |
#Get Agent Status
/api/v1/vcaas/projects/:projectId/agent/statusFreePoll to track agent progress and get real-time messages. Poll every 10–15 seconds.
When status becomes "done", you MUST refresh the preview URL by calling GET /projects/:projectId and reading developmentUrlFieldToUse ("temporalDevelopmentProjectUrl" for the live server or "cachedDevelopmentUrl" for a cached snapshot). If developmentUrlFieldToUse is null or undefined, default to temporalDevelopmentProjectUrl. The URL can change after each agent run.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project to poll |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | The project ID |
data.status | string | "init" (a prompt is currently running) | "done" (last prompt finished) | "idle" (never run). For multi-prompt runs this flips between "init" and "done" repeatedly as the batch advances — poll multiPrompt.status to track the whole batch. |
data.startedAt | string | null | ISO 8601 start time of the current prompt (not the whole batch) |
data.realtimeConversation | array | Single-prompt: messages from the current run. Multi-prompt: messages from the whole batch (scoped to multiPrompt.startedAt). |
data.realtimeConversation[].author | string | "user" | "agent" |
data.realtimeConversation[].message | string | The message text |
data.realtimeConversation[].messageType | string | "regular" | "starting" | "building" | "finished" | "error" | "limit-reached" |
data.realtimeConversation[].createdAt | string | ISO 8601 message date |
data.realtimeConversation[].versionId | string | undefined | Version created at this step |
data.realtimeConversation[].secretKeysNeeded | object | undefined | Secrets the agent needs (key: { isProvided, description }) |
data.realtimeConversation[].gitDiffUrl | string | undefined | Diff URL for this step |
data.creditsSpent | number | undefined | Credits spent on the current prompt (present when status is "done") |
data.multiPrompt | object | null | Present only when a multi-prompt batch exists. The canonical signal for "is a multi-prompt run in flight" — poll its status until "done"/"cancelled". |
data.multiPrompt.status | string | "planning" | "executing" | "paused" | "done" | "cancelled" |
data.multiPrompt.totalPrompts | number | Number of prompts in the batch (0 during planning) |
data.multiPrompt.currentPromptIndex | number | Index of the in-flight prompt; -1 before the first one starts |
data.multiPrompt.prompts | array | Ordered list of prompts in the batch |
data.multiPrompt.prompts[].order | number | Position in the batch |
data.multiPrompt.prompts[].prompt | string | The prompt text |
data.multiPrompt.prompts[].status | string | "pending" | "executing" | "done" | "failed" |
data.multiPrompt.startedAt | string | ISO 8601 start time of the whole batch |
data.multiPrompt.updatedAt | string | ISO 8601 last change to the batch state |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/status{
"errors": null,
"data": {
"projectId": "my-app",
"status": "init",
"startedAt": "2026-03-11T10:35:00.000Z",
"realtimeConversation": [
{
"author": "agent",
"message": "Creating the project structure...",
"messageType": "building",
"createdAt": "2026-03-11T10:36:00.000Z"
},
{
"author": "agent",
"message": "Project built successfully!",
"messageType": "finished",
"versionId": "v_abc123",
"gitDiffUrl": "https://...",
"secretKeysNeeded": {
"STRIPE_SECRET_KEY": { "isProvided": false, "description": "Required for processing payments" }
},
"createdAt": "2026-03-11T10:50:00.000Z"
}
],
"creditsSpent": 3.2,
"multiPrompt": null
}
}Multi-prompt example (running prompt 2 of 3):
{
"errors": null,
"data": {
"projectId": "my-app",
"status": "init",
"startedAt": "2026-03-11T11:05:00.000Z",
"realtimeConversation": [ /* messages from the whole batch */ ],
"multiPrompt": {
"status": "executing",
"totalPrompts": 3,
"currentPromptIndex": 1,
"prompts": [
{ "order": 0, "prompt": "Set up auth", "status": "done" },
{ "order": 1, "prompt": "Add Stripe checkout", "status": "executing" },
{ "order": 2, "prompt": "Wire up dashboard", "status": "pending" }
],
"startedAt": "2026-03-11T10:35:00.000Z",
"updatedAt": "2026-03-11T11:05:00.000Z"
}
}
}{
"errors": { "errorCode": "PROJECT_NOT_FOUND", "errorMessage": "Project does not exist or you don't own it" },
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
#Get Full Conversation
/api/v1/vcaas/projects/:projectId/agent/full-conversationFreeRetrieve the complete conversation history across all agent runs (not just the current one).
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project whose conversation to fetch |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | The project ID |
data.conversation | array | All messages from all agent runs |
data.conversation[].author | string | "user" | "agent" |
data.conversation[].message | string | The message text |
data.conversation[].messageType | string | "regular" | "starting" | "building" | "finished" | "error" | "limit-reached" |
data.conversation[].createdAt | string | ISO 8601 message date |
data.conversation[].versionId | string | undefined | Version created at this step |
data.conversation[].secretKeysNeeded | object | undefined | API keys the agent needs (key: { isProvided, description }). Add missing keys as project secrets. |
data.conversation[].gitDiffUrl | string | undefined | Diff URL for this step |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/full-conversation{
"errors": null,
"data": {
"projectId": "my-app",
"conversation": [
{ "author": "user", "message": "Create a landing page", "messageType": "regular", "createdAt": "..." },
{ "author": "agent", "message": "Building project structure...", "messageType": "building", "createdAt": "..." },
{
"author": "agent",
"message": "Project built successfully!",
"messageType": "finished",
"versionId": "v_abc123",
"gitDiffUrl": "https://...",
"secretKeysNeeded": {
"STRIPE_SECRET_KEY": { "isProvided": false, "description": "Required for processing payments" }
},
"createdAt": "..."
}
]
}
}{
"errors": { "errorCode": "PROJECT_NOT_FOUND", "errorMessage": "Project does not exist or you don't own it" },
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
#Stop Agent
/api/v1/vcaas/projects/:projectId/agent/stopFreeSend a stop signal to a running agent.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project whose agent to stop |
Response fields
| Field | Type | Description |
|---|---|---|
data.message | string | Confirmation message |
Example request
curl -X POST -H "api-key: tlm_sk_your_key" \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/stop{ "errors": null, "data": { "message": "Agent stop signal sent" } }{
"errors": { "errorCode": "NO_PROCESS_RUNNING", "errorMessage": "No agent process is currently running" },
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
NO_PROCESS_RUNNING | 400 | No agent process is currently running |
#Concurrency rules
- Only one heavy operation at a time per project. Six things count: an agent run, a deployment, a version recovery, a rebuild (started by
POST /rebuildor by the visual editor), a GitHub pull, and a project import. Starting another while one runs answers409with the code that names it —AGENT_RUNNING,DEPLOYMENT_RUNNING,RECOVERY_RUNNING,REBUILD_RUNNING,GITHUB_PULL_RUNNINGorIMPORT_IN_PROGRESS— and the message says what to poll. - Two exceptions, on purpose: a server restart is allowed during a rebuild, a pull or an import (it is the way out of one that died), and a file write is allowed during a rebuild (the write is simply not in that build — rebuild again).
- If deploy or recover is called and the server is not active, it auto-starts (charges
START_SERVERcredits) and returnsSERVER_NOT_READY. PollGET /projects/:projectIduntilagentServerStatusis"Active", then retry. - Agent start IS allowed during server operations (the backend waits internally).
#Complete integration flow
- Create the project and start building it —
POST /projects/launch, body{ "projectId": "my-app", "prompt": "Build a SaaS landing page" }. One call. Async, takes 5 to 40 minutes. Use theprojectIdit returns (a taken name is auto-suffixed), and checkagent.startedandwarnings.- Already have an empty project? Then start at step 2 instead:
POST /projects/my-app/agent/start, body{ "prompt": "..." }. That is the only case where the two calls are separate — see Launch Project.
- Already have an empty project? Then start at step 2 instead:
- Send follow-up prompts —
POST /projects/my-app/agent/start, body{ "prompt": "Add a pricing page" }. Same endpoint, same polling, for every change after the first. - Poll agent status (every 10–15s) —
GET /projects/my-app/agent/status."init"= working (showrealtimeConversation),"done"= finished (creditsSpent= total). On"done", immediately callGET /projects/:projectIdand refresh the preview URL.- 3.1 Refresh preview URL (on page load, page refresh, and after every prompt finishes) —
GET /projects/my-app, readdevelopmentUrlFieldToUse: usecachedDevelopmentUrl(static snapshot while the server is archived) ortemporalDevelopmentProjectUrl(live dev server); defaulttemporalDevelopmentProjectUrlifnull/undefined. Reload the iframe when the URL changes.
- 3.1 Refresh preview URL (on page load, page refresh, and after every prompt finishes) —
- Publish —
POST /projects/my-app/deployments/deploy. Builds, deploys, and assigns a public URL. Takes 2 to 5 minutes. - Poll deployment status (every 10–15s) —
GET /projects/my-app/deployments/status; on"success", getproductionProjectUrlfromGET /projects/my-app. - (Optional) Custom domain —
PUT /projects/my-app/domain, body{ "hostname": "app.yourdomain.com" }; configure the returned DNS records; pollcustomDomain.statusuntil"active".
- Agent and deployment are ASYNCHRONOUS — always poll for status.
- Show real-time agent conversation messages during generation.
- NEVER call the Totalum API from frontend code — always go through your backend server.
- ALWAYS refresh the dev preview URL (
GET /projects/:projectId) on page load, page refresh, and after each prompt; usedevelopmentUrlFieldToUse; defaulttemporalDevelopmentProjectUrlifnull/undefined. - On the dev preview, don't show the production URL; display an open-in-blank link so the user always sees the realtime dev preview (production may be outdated until published).
- Keep the API key secret — it must never leave your backend server.
#Global error codes
All errors are shaped as { "errors": { "errorCode": "CODE", "errorMessage": "..." }, "data": null }.
| Code | HTTP | Meaning |
|---|---|---|
INSUFFICIENT_CREDITS | 402 | Not enough credits for this operation |
PROJECT_CREDIT_LIMIT_REACHED | 403 | Project monthly credit limit reached for that category |
PROJECT_NOT_ALLOWED | 403 | API key doesn't have access to this project |
PROJECT_NOT_FOUND | 404 | Project doesn't exist or you don't own it |
AGENT_RUNNING | 409 | Agent already running on this project |
DEPLOYMENT_RUNNING | 409 | A deployment is in progress |
RECOVERY_RUNNING | 409 | A version recovery is in progress |
REBUILD_RUNNING | 409 | A rebuild is in progress — poll GET /projects/:projectId/rebuild/status until it is no longer "rebuilding" |
GITHUB_PULL_RUNNING | 409 | A GitHub pull is rebuilding the project — poll GET /projects/:projectId/github/pull-status until it is no longer "pulling" |
IMPORT_IN_PROGRESS | 409 | A project import is in progress — poll GET /projects/:projectId until importInProgress is null |
SERVER_NOT_READY | 409 | Server auto-starting, poll until Active |
NO_DEPLOYMENT | 404 | No deployment found. Deploy first and wait until status is "success" |
MISSING_PROMPT | 400 | prompt field is required |
MISSING_PROJECT_ID | 400 | projectId field is required |
INVALID_PROJECT_NAME | 400 | Invalid projectId format |
INVALID_PROJECT_NAME_LENGTH | 400 | Project name must be 4-35 characters |
PROJECT_ALREADY_EXISTS | 409 | A project with this ID already exists |
PLAN_NOT_API | 400 | Only API plan projects can be deleted |
PROMPT_SECURITY_VIOLATION | 400 | Prompt failed security validation |
NO_PROCESS_RUNNING | 400 | No agent process is currently running |
MISSING_FILE | 400 | file field is required (multipart) |
UPLOAD_FAILED | 500 | File upload failed |
MISSING_VERSION_ID | 400 | versionId is required |
MISSING_SECRET_FIELDS | 400 | secretName and secretValue are required |
INVALID_SECRET_KEY_NAME | 400 | Invalid secret key name format |
MISSING_SECRET_ID | 400 | secretId is required |
MISSING_HOSTNAME | 400 | hostname is required |
MISSING_TABLE_NAME | 400 | tableName is required |
GET_ACCOUNT_ERROR | 400 | Internal error fetching account info |
LIST_PROJECTS_ERROR | 400 | Internal error listing projects |
RATE_LIMIT_EXCEEDED | 429 | Too many requests — project creation is rate-limited per plan (see Projects) |
MISSING_WEBHOOK_FIELDS | 400 | url and event are required |
INVALID_WEBHOOK_URL | 400 | Webhook URL must use HTTPS |
INVALID_WEBHOOK_EVENT | 400 | Event not in allowed list |
WEBHOOK_EVENT_ALREADY_EXISTS | 409 | A webhook for this event already exists |
WEBHOOK_NOT_FOUND | 404 | Webhook not found |
MISSING_GITHUB_FIELDS | 400 | token and repositoryFullName are required |
GITHUB_VALIDATION_FAILED | 400 | Token invalid, missing permissions, or repo not found |
GITHUB_SECRET_STORE_ERROR | 400 | Failed to store GitHub credentials |
GITHUB_SYNC_FAILED | 400 | Initial sync with GitHub failed |
GITHUB_NOT_CONNECTED | 400 | GitHub is not connected to this project |
GITHUB_PULL_ERROR | 400 | Failed to pull changes from GitHub |
MISSING_LIMIT_FIELDS | 400 | At least one credit limit field is required |
INVALID_LIMIT | 400 | Credit limit must be a positive number |
INVALID_SYNC_DIRECTION | 400 | syncDirection must be "totalum_to_github" or "github_to_totalum" |
