curl --request POST \
--url https://api.paigeme.dev/v1/deploy \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"tables": [
"<string>"
],
"flows": [
"<string>"
]
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({tables: ['<string>'], flows: ['<string>']})
};
fetch('https://api.paigeme.dev/v1/deploy', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.paigeme.dev/v1/deploy"
payload = {
"tables": ["<string>"],
"flows": ["<string>"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"success": true,
"data": {
"deployed_sha": "<string>",
"files_count": 123,
"tables_created": [
"<string>"
],
"tables_synced": 123,
"sequences_repaired": [
{}
],
"rows_copied": 123,
"table_errors": [
{}
],
"table_sync_failed": true,
"flows_published": 123,
"flow_errors": [
{
"name": "<string>",
"message": "<string>",
"code": "BUSINESS_UNVERIFIED",
"retryable": true,
"state": "draft"
}
],
"flows_failed": true,
"require_warnings": [
{}
]
}
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Invalid request body",
"details": {}
},
"request_id": "<string>"
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Invalid request body",
"details": {}
},
"request_id": "<string>"
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Invalid request body",
"details": {}
},
"request_id": "<string>"
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Invalid request body",
"details": {}
},
"request_id": "<string>"
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Invalid request body",
"details": {}
},
"request_id": "<string>"
}Promote dev to production
Copies the dev working copy to production (dev → production) and syncs table structure. Table DATA and flow publishing are opt-in via tables / flows.
No field is required — an empty body {} is a valid, complete request: it promotes the code and syncs table structure without copying any rows or publishing any flows. This one genuinely takes an empty body, unlike the other all-optional-looking bodies on this API.
A deploy can partially succeed: table and flow failures are collected rather than thrown, so check table_sync_failed / flows_failed and the matching error arrays on a 200 rather than assuming success.
Structure sync is additive here, deliberately. It creates missing tables and adds missing columns; it never drops anything. The dashboard deploy dialog has a separate, unticked opt-in for removing a table or column that no longer exists in staging (GRE-772) — that opt-in is intentionally NOT exposed on this endpoint or the MCP deploy tool, because dropping a production table is irreversible and needs a human tick rather than an API key. Do not treat the asymmetry as an inconsistency to fix.
require_warnings is advisory, never fatal. It reports relative require() calls in the promoted code that resolve to no file in the project. The deploy still succeeded and the code was promoted exactly as sent — but a warning with load_time_reachable: true means the bot worker will throw at init and answer no inbound message at all.
Required scope: code:write
curl --request POST \
--url https://api.paigeme.dev/v1/deploy \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"tables": [
"<string>"
],
"flows": [
"<string>"
]
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({tables: ['<string>'], flows: ['<string>']})
};
fetch('https://api.paigeme.dev/v1/deploy', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.paigeme.dev/v1/deploy"
payload = {
"tables": ["<string>"],
"flows": ["<string>"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"success": true,
"data": {
"deployed_sha": "<string>",
"files_count": 123,
"tables_created": [
"<string>"
],
"tables_synced": 123,
"sequences_repaired": [
{}
],
"rows_copied": 123,
"table_errors": [
{}
],
"table_sync_failed": true,
"flows_published": 123,
"flow_errors": [
{
"name": "<string>",
"message": "<string>",
"code": "BUSINESS_UNVERIFIED",
"retryable": true,
"state": "draft"
}
],
"flows_failed": true,
"require_warnings": [
{}
]
}
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Invalid request body",
"details": {}
},
"request_id": "<string>"
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Invalid request body",
"details": {}
},
"request_id": "<string>"
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Invalid request body",
"details": {}
},
"request_id": "<string>"
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Invalid request body",
"details": {}
},
"request_id": "<string>"
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Invalid request body",
"details": {}
},
"request_id": "<string>"
}Authorizations
Project API key issued in Settings → API keys. Send it as Authorization: Bearer pk_live_….
Headers
Target project id, for an MCP OAuth bearer (mcp_at_…) attached to more than one project — get ids from GET /v1/projects. Matched case-insensitively.
Omit it and a READ falls back to the connection's default project; a mutation (any non-GET) on a connection with 2+ projects is rejected with 400 project_required — a write is never defaulted to a guessed project. A connection with exactly one project never needs the header.
Scopes are checked against the SELECTED project only, never a union across the connection.
For a pk_ API key the header selects nothing — one key is one project's context — but it IS validated: omit it and the key's own project is used, send it and it must name that project, otherwise the call is rejected with 403 project_not_attached (a blank value is 400 invalid_project_header, as above).
Body
Response
Success.
true Hide child attributes
Hide child attributes
Commit sha of the deployed snapshot, or null when no commit was recorded.
Number of dev files promoted.
Custom tables created in production during this deploy.
Count of tables whose structure was synced.
Report-only (GRE-779). Each entry (table, column, sequence) names a production custom-table primary key whose auto-increment default was missing or pointed at a sequence outside public, and which this deploy repaired so inserts that omit the id succeed again. A non-empty list NEVER means the deploy failed — it is the opposite, and these are deliberately kept OUT of table_errors. The pass is always-on, needs no request field, and skips base tables and wp_* synced tables.
Rows copied for the tables named in tables. 0 when data copy was not requested.
Per-table failures. Non-empty means the deploy partially succeeded.
True when table_errors is non-empty.
Count of flows published on the connected number.
Per-flow publish failures, one entry per affected flow.
Hide child attributes
Hide child attributes
Name of the flow that was not published.
Human-readable reason. Safe to show a user.
Machine-readable reason, when one is known (GRE-592). BUSINESS_UNVERIFIED means Meta will not publish flows until the WhatsApp Business Account’s business is verified in Meta Business Manager: the publish was skipped (or refused), and code and tables deployed normally. What the flow is on the connected number afterwards is in state — read it rather than assuming: a refused update can take an already-live flow OFFLINE. Absent for any other failure. More codes may be added — treat an unrecognised code like an absent one.
BUSINESS_UNVERIFIED Present alongside code. false means redeploying will fail identically until the account state changes — do not retry in a loop.
Present alongside code: BUSINESS_UNVERIFIED: what the flow is on the connected number after this deploy. draft — a flow that was not live yet; it exists as a DRAFT and customers cannot open it. live_unchanged — an already-live flow whose update was skipped BEFORE anything was sent to Meta; the previous live version is still serving customers. live_offline — an already-live flow whose update reached Meta and was then refused at publish: sending the update reverted it to DRAFT, so it is NO LONGER available to customers until the business is verified and the project is deployed again. Only live_unchanged means customers can still open the flow. More states may be added — treat an unrecognised one as not available.
draft, live_unchanged, live_offline True when flow_errors is non-empty.
Report-only (GRE-667). Each entry names a bot file whose relative require() does not resolve to any file in the project — file, line, specifier, target, reason (unresolved | empty-module), load_time_reachable, platform_owned, platform_restorable and a human-readable message. A non-empty list NEVER means the deploy failed: the code was promoted exactly as sent. load_time_reachable: true means the bot worker will throw at init and answer no inbound message at all. platform_owned: true means the file is Paige-managed and not yours to write; platform_restorable is the narrower question of whether an injector can put it back — false there means no automatic repair exists and the message escalates instead of naming one.
Was this page helpful?
