QuickBooks, Meta Ads, Gmail and MCP Connections
Four integrations share one pattern: an admin asks the API for a connect URL, a person approves access on the provider's own consent screen, the provider redirects back to a DayZero callback, and the connection is then managed with status and disconnect calls.
| Family | Base path | Scope of the connection | What it does |
|---|---|---|---|
| QuickBooks Online | /api/v1/quickbooks |
One business | One-time migration of a QuickBooks company's chart of accounts, contacts, documents and history into DayZero |
| Meta Ads | /api/v1/meta-ads |
One business | Pulls campaign-level daily ad spend from Facebook / Instagram ad accounts (read-only) |
| Gmail | /api/v1/gmail |
One user (no business) | Lets Regent search and read the signed-in user's own mailbox (read-only) |
| External MCP servers | /api/v1/mcp-connections |
One business | Outbound connections to other people's MCP servers so Regent can call their tools |
These four families are in the public API Reference. Xero is not: its routes, like Plaid, Ramp, Shopify, Square, Stripe, Finch and HubSpot, are left out of the public spec. See Stripe, Plaid, Shopify, Square and Ramp for the integrations that have their own guides.
Not the same thing as the MCP Server guide. MCP Server covers the inbound direction, where Claude, ChatGPT or Cursor connect to DayZero. This guide covers the outbound direction, where Regent connects to other MCP servers.
Conventions
Every call needs Authorization: Bearer dz_your_token_here. The business-scoped families (QuickBooks, Meta Ads, MCP connections) also need x-business-id: YOUR_BUSINESS_ID. Gmail is tied to the authenticated user: it takes no business header and needs no admin role.
Successful responses use the standard envelope, { "success": true, "data": ... }; the examples below show the data payload. Errors follow Error Handling: branch on code, not message. A body or query value that fails schema validation is a 400 with code GEN_002.
Permissions. Routes marked admin need the admin permission on the business: direct owners and admins, and advisory-firm staff on their clients. Direct members have read and write and viewers read only, so neither can connect, change or disconnect anything. A read_only API token can only call GET routes. read routes (status, lists, reports that read stored data) need only read access. No plan or add-on check applies; a provider is available only if the environment holds that provider's app credentials (see the *_NOT_CONFIGURED errors).
The OAuth connect flow
GET .../connect-url (and POST /mcp-connections/oauth/start) returns connect_url and state. A person must then open connect_url in a browser, so this step cannot be completed by a headless client.
- Request the connect URL from the browser that will approve it (the DayZero app does this when an admin clicks Connect).
- Open
connect_url; the person approves access on the provider's page. - The provider redirects to the DayZero callback, which stores the connection and answers with a
302to the app. - Confirm with the family's
statuscall.
- For QuickBooks, Meta Ads and external MCP servers the connect response also sets an HttpOnly
dz_oauth_<service>cookie that binds the flow to the browser that asked. The callback rejects the redirect if the cookie is missing or does not match (reason=oauth_state_mismatchon the redirect). Gmail has no such cookie. - The
statenonce is single use and expires after 10 minutes. - Callbacks (
/oauth/callback) are called by the provider, never by your code. They are unauthenticated and always answer with a302, never JSON. A user who cancels on the consent screen is redirected witherror; there is no 4xx to handle.
| Family | Redirect (outcome is connected or error) |
|---|---|
| QuickBooks | /books/integrations?quickbooks=<outcome>; failures add message and reason (the error code). disconnected is also used after the Intuit Disconnect URL |
| Meta Ads | /books/integrations?meta_ads=<outcome> |
| External MCP | /books/integrations?mcp=<outcome> |
| Gmail | /settings?section=connected-accounts&gmail=<outcome>; a withheld permission adds reason=scope_not_granted and message |
QuickBooks Online
The QuickBooks integration is a one-time migration, not an ongoing sync. It reads from QuickBooks (scope com.intuit.quickbooks.accounting) and writes real DayZero records. There is no scheduled QuickBooks sync, and nothing is ever written back to QuickBooks.
| Method and path | Permission | Purpose |
|---|---|---|
GET /quickbooks/connect-url |
admin | Intuit consent URL (also how to reconnect) |
GET /quickbooks/oauth/callback |
none (Intuit) | Stores the connection |
GET /quickbooks/disconnected |
none (Intuit) | Landing for Intuit's Disconnect URL |
GET /quickbooks/status |
read | Connection and import status |
POST /quickbooks/import |
admin | Start the migration import |
POST /quickbooks/import/cancel |
admin | Stop a running import |
GET /quickbooks/migration-report |
admin | Trial balance comparison, QuickBooks vs DayZero |
GET /quickbooks/coverage-report |
admin | Per-object-type import counts |
DELETE /quickbooks/disconnect |
admin | Revoke the grant |
Connect
curl "https://api.ondayzero.com/api/v1/quickbooks/connect-url" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"{ "success": true, "data": { "connect_url": "https://appcenter.intuit.com/connect/oauth2?...", "state": "..." } }It always returns a URL, so calling it again is also how you reconnect after the grant is rejected. In the callback DayZero exchanges the code, then confirms with QuickBooks that the grant can read the company's CompanyInfo before storing anything; if that check fails nothing is connected and the fresh grant is revoked. A QuickBooks company can be live on one DayZero business at a time: linking one that is already connected elsewhere fails with the "already linked to another DayZero business" message (INTEGRATION_077, shown on the error redirect). Reconnecting to a different company than before resets the import state and idempotency links for the business but keeps the DayZero records already imported.
Status
curl "https://api.ondayzero.com/api/v1/quickbooks/status" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"{
"connected": true,
"healthy": true,
"reauth_required": false,
"realm_id": "9341453000000000",
"company_name": "Acme Co",
"last_synced_at": "2026-10-01T14:22:05",
"import_status": "completed",
"import_summary": {
"start_date": "2026-01-01",
"selected_phases": ["bills", "invoices", "payments"],
"payments": { "skipped": true, "reason": "invoices failed", "blocked_by": "invoices" }
},
"last_error_message": null,
"disconnected_at": null
}import_status is running, completed, failed or cancelled (null before the first import). import_summary holds one entry per import phase, plus start_date and, when you chose phases, selected_phases; while a run is live it also carries a progress object. A phase blocked by a failed prerequisite shows skipped: true, a reason and blocked_by. reauth_required is true when Intuit rejected the grant (expired, revoked, or the app was removed inside QuickBooks); run the connect flow again. Not connected is connected: false (not an error). After a disconnect, connected is false but company_name, disconnected_at, import_status and last_synced_at remain so you can tell which company was linked.
Import
curl -X POST "https://api.ondayzero.com/api/v1/quickbooks/import?start_date=2026-01-01&phases=invoices&phases=bills&phases=payments" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Both parameters are optional query parameters (there is no request body):
| Parameter | Description |
|---|---|
start_date |
YYYY-MM-DD. Everything dated on or after it is imported; a single opening-balance journal entry is posted as of the day before. Omitted, it defaults to today. A malformed date is a 400 |
phases |
Repeat to import only some optional sections. Omit to import everything. Unknown names are ignored |
Valid phases values include invoices, bills, credit_memos, vendor_credits, purchase_orders, journal_entries, cash_transactions, payments, bill_payments, fixed_assets, budgets and attachments. Master data always imports regardless of phases: chart of accounts, customers, vendors, employees, items and tax rates, plus the opening balance. payments needs invoices, bill_payments needs bills, and attachments needs both; if a prerequisite fails or is not selected, the dependent phase is skipped.
The call queues a background job and returns right away (HTTP 201):
{ "workflow_id": "quickbooks-import-YOUR_BUSINESS_ID", "business_id": "YOUR_BUSINESS_ID", "status": "queued", "executor": "temporal" }Poll GET /quickbooks/status until import_status leaves running. Only one import runs per business: a second start while one is live returns 409 (INTEGRATION_084). Imports are idempotent against earlier runs, so re-running resumes past what is already in DayZero instead of duplicating it. A run that finishes with gaps still ends completed: a phase QuickBooks denied access to (403), a section that stopped early, a dependent phase that was skipped, or documents skipped because the same number already existed are all listed in last_error_message, and re-running retries them.
curl -X POST "https://api.ondayzero.com/api/v1/quickbooks/import/cancel" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Cancel returns { "cancelled": true, "workflow_cancelled": true } (HTTP 201). The page being written is rolled back, records already committed stay, and a later import continues past them. workflow_cancelled is false when no job was actually in flight; a row that was not running is left unchanged.
Verify the migration
curl "https://api.ondayzero.com/api/v1/quickbooks/migration-report?as_of_date=2026-09-30" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"This compares the QuickBooks Trial Balance (a live call to Intuit) with the DayZero ledger balances linked to each QuickBooks account. as_of_date (YYYY-MM-DD) defaults to today. If Intuit rejects the grant, the call returns 400 INTEGRATION_073; run the connect flow again. Amounts are integer cents, debit-positive on both sides (debits positive, credits negative):
{
"as_of_date": "2026-09-30",
"rows": [
{ "qbo_account_id": "35", "qbo_account_name": "Checking", "linked": true,
"local_ledger_id": "LEDGER_UUID", "local_ledger_name": "Checking",
"qbo_cents": 1250000, "local_cents": 1250000, "delta_cents": 0, "severity": "ok" }
],
"totals": { "qbo_cents": 1250000, "local_cents": 1250000, "delta_cents": 0, "mismatched_rows": 0, "abs_delta_cents": 0 }
}severity is ok (delta of zero), warn (under 100 cents) or error (unlinked account, or 100 cents or more). Rows are sorted by severity (error, warn, ok), then by absolute delta. mismatched_rows counts rows that are not ok.
GET /quickbooks/coverage-report lists every supported QuickBooks object type: integration, object_types (the count) and rows, each with source_type, dayzero_target, imported, skipped and errors (the last two come from the latest import summary). It works without a live connection and returns counts of what was imported.
Disconnect
curl -X DELETE "https://api.ondayzero.com/api/v1/quickbooks/disconnect" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"This revokes the grant with Intuit and clears the stored tokens. The response is { "disconnected": true, "revoked": true }; revoked: false means the grant was cleared locally but Intuit did not acknowledge the revoke (the owner can also remove the app inside QuickBooks under Apps > My apps). Imported DayZero records are never deleted. A disconnect on a business that is not connected succeeds with revoked: false. A running import is marked failed.
If a user removes DayZero from inside QuickBooks, Intuit sends the browser to GET /quickbooks/disconnected?realmId=... (configured as the app's Disconnect URL). DayZero verifies the stored grant with Intuit and only marks the connection disconnected if Intuit rejects it. The response is always the same redirect, whether or not the company is known.
Meta Ads
Meta Ads connects a Facebook user with the ads_read permission and pulls campaign-level daily spend for every ad account that user can read. The data is operational reporting (spend, impressions and clicks per campaign per day) so a business can compare what Meta reports with card charges already in the ledger. No journal entry or transaction is created.
| Method and path | Permission | Purpose |
|---|---|---|
GET /meta-ads/connect-url |
admin | Facebook Login URL (connect or reconnect) |
GET /meta-ads/oauth/callback |
none (Meta) | Stores the connection and queues a first sync |
GET /meta-ads/status |
read | Connection, accounts and 30-day totals |
PATCH /meta-ads/settings |
admin | Auto-sync and window settings |
GET /meta-ads/accounts |
read | Ad accounts with sync settings |
POST /meta-ads/accounts/refresh |
admin | Re-list ad accounts from Meta |
PATCH /meta-ads/accounts/{account_id} |
admin | Include or exclude one account |
POST /meta-ads/sync |
admin | Pull spend now |
GET /meta-ads/spend |
read | Aggregated spend |
DELETE /meta-ads/disconnect |
admin | Remove the connection and pulled spend |
Connect and reconnect
GET /meta-ads/connect-url returns { "connect_url", "state" } and works even when already connected. Meta's long-lived user token (about 60 days) has no refresh token, so reconnecting is how an expiring token is renewed; it keeps the ad accounts, their settings and every spend row already pulled. The callback exchanges the code for a long-lived token, lists the ad accounts, and queues a first sync automatically. If the server has no Meta app credentials the call returns 400 (INTEGRATION_025) and status reports configured: false.
Status
curl "https://api.ondayzero.com/api/v1/meta-ads/status" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"{
"connected": true,
"healthy": true,
"needs_reauth": false,
"meta_user_name": "Jane Doe",
"token_expires_at": "2026-12-05T10:00:00",
"token_expires_in_days": 55,
"last_synced_at": "2026-10-11T06:31:12",
"last_sync_error": null,
"auto_sync_enabled": true,
"lookback_days": 7,
"initial_backfill_days": 90,
"account_count": 2,
"accounts_enabled": 2,
"accounts": [
{ "id": "AD_ACCOUNT_UUID", "meta_account_id": "act_1234567890", "name": "Acme Ads",
"currency": "USD", "timezone_name": "America/Los_Angeles", "account_status": 1,
"meta_business_name": "Acme", "sync_enabled": true,
"synced_through": "2026-10-11", "last_error": null }
],
"spend_rows": 1840,
"earliest_spend_date": "2026-07-14",
"spend_last_30d": [ { "currency": "USD", "spend_minor": 482310 } ],
"configured": true
}needs_reauth is true when the token has expired or Meta rejected it during a sync; run the connect flow again. Not connected returns connected: false (not an error). account_status is Meta's code (1 is active). spend_minor values are integers in the currency's minor unit (cents for USD) and are reported per currency, never summed across currencies.
Settings and accounts
curl -X PATCH "https://api.ondayzero.com/api/v1/meta-ads/settings" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{ "auto_sync_enabled": true, "lookback_days": 14, "initial_backfill_days": 180 }'| Field | Description |
|---|---|
auto_sync_enabled |
Daily automatic pull on or off (default on) |
lookback_days |
1-90, default 7. Trailing window re-pulled on each sync to pick up Meta's late revisions |
initial_backfill_days |
1-1095, default 90. How far back a never-synced account is pulled |
All fields are optional and the response is the full status object. Settings, GET /meta-ads/accounts, account updates, sync and disconnect all return 400 (code GEN_002, message "Meta Ads is not connected to this business.") when Meta Ads is not connected.
GET /meta-ads/accounts returns the account list on its own. POST /meta-ads/accounts/refresh asks Meta again and returns { "accounts": [...], "added": 1, "updated": 1 } (HTTP 201); it needs a live token, so reconnect first if needs_reauth is true. An expired or revoked Meta token on refresh or sync returns 400 INTEGRATION_091; reconnect via /meta-ads/connect-url. To skip an account:
curl -X PATCH "https://api.ondayzero.com/api/v1/meta-ads/accounts/AD_ACCOUNT_UUID" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{ "sync_enabled": false }'account_id is DayZero's id for the account (not the act_... Meta id); sync_enabled is required. An unknown id returns 404 (NOT_FOUND_001). Newly discovered accounts start with sync enabled. The response is the updated account.
Sync
curl -X POST "https://api.ondayzero.com/api/v1/meta-ads/sync" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{ "lookback_days": 14, "full_backfill": false }'The body is optional. lookback_days (1-1095) overrides the connection's window for this run; full_backfill: true re-pulls the whole initial_backfill_days history for every enabled account. Normally the job is queued and returns HTTP 201 with { "status": "queued", "workflow_id": "...", "business_id": "...", "executor": "temporal" }; poll GET /meta-ads/status for last_synced_at and last_sync_error. If the job queue is unreachable the pull runs inline and the response carries the result instead: status (completed, or partial when an account failed), executor: "inline", accounts_synced, accounts_failed, rows_written and error.
A failure on one account is recorded on that account (last_error) and the other accounts continue. A rejected or expired token aborts the run and sets needs_reauth. DayZero also pulls automatically once a day at 06:30 UTC for connections with auto_sync_enabled, skipping ones that need reconnecting.
Read spend
curl "https://api.ondayzero.com/api/v1/meta-ads/spend?since=2026-09-01&until=2026-09-30&group_by=campaign&account_id=AD_ACCOUNT_UUID" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"| Parameter | Description |
|---|---|
since, until |
YYYY-MM-DD, inclusive. Default: the last 30 days (29 days ago through today) |
group_by |
account (default), day or campaign |
account_id |
Repeatable; restrict to these DayZero ad account ids |
{
"since": "2026-09-01",
"until": "2026-09-30",
"group_by": "campaign",
"rows": [
{ "ad_account_id": "AD_ACCOUNT_UUID", "meta_account_id": "act_1234567890", "account_name": "Acme Ads",
"campaign_id": "120210000000001", "campaign_name": "Fall launch",
"currency": "USD", "spend_minor": 182500, "impressions": 94000, "clicks": 1820 }
],
"totals": [ { "currency": "USD", "spend_minor": 182500 } ]
}Each row has currency, spend_minor, impressions and clicks, plus the grouping fields: date for day; ad_account_id, meta_account_id and account_name for account and campaign; and campaign_id and campaign_name for campaign. totals has one entry per currency. until before since, a range over 1,095 days, or a group_by outside the three values is a 400.
Disconnect
DELETE /meta-ads/disconnect removes the token, the ad accounts and all pulled spend rows and returns { "status": "disconnected" }. It does not revoke the grant on Meta's side.
Gmail
Gmail is a personal connection: it links the authenticated user's own mailbox so Regent can search and read their email when they ask. Access is read-only (gmail.readonly, plus openid email to label the connection) and DayZero stores the tokens, not email content. There is one connection per user, and it applies in every business that user works in. These routes take no x-business-id header and no admin role; Gmail is the only family here that does not need admin.
| Method and path | Purpose |
|---|---|
GET /gmail/connect-url |
Google consent URL |
GET /gmail/oauth/callback |
Called by Google; stores the connection |
GET /gmail/status |
Connection state |
DELETE /gmail/disconnect |
Revoke and remove |
curl "https://api.ondayzero.com/api/v1/gmail/connect-url" \
-H "Authorization: Bearer dz_your_token_here"Returns { "connect_url", "state" }. If the user already has a connection, no URL is generated and the response is { "status": "already_connected", "google_email": "jane@example.com" }; disconnect first to link a different mailbox. If the environment has no Google credentials it returns 400 (INTEGRATION_081). The consent screen lets users untick individual permissions; if "Read your email" is unticked the callback stores nothing and redirects with reason=scope_not_granted.
curl "https://api.ondayzero.com/api/v1/gmail/status" \
-H "Authorization: Bearer dz_your_token_here"{
"connected": true,
"healthy": true,
"google_email": "jane@example.com",
"scopes": "openid email https://www.googleapis.com/auth/gmail.readonly",
"connected_at": "2026-10-01T09:12:44",
"last_used_at": "2026-10-10T15:03:10",
"last_error_message": null
}Regent gets two read-only chat tools over this connection: search_gmail (a Gmail query; limit defaults to 10 with a maximum of 25; include_spam_trash defaults to false) and read_gmail_message (a message_id from a search). Results are marked as untrusted third-party text. The DayZero MCP server has no Gmail tools, so an assistant connected over MCP such as Claude, ChatGPT or Cursor cannot read this mailbox.
Not connected is { "connected": false, "healthy": false }. Access tokens refresh automatically. If Google rejects the refresh grant the connection is marked healthy: false with a last_error_message, Regent's Gmail tools report that Gmail authorization expired (INTEGRATION_083), and the user must disconnect and connect again.
curl -X DELETE "https://api.ondayzero.com/api/v1/gmail/disconnect" \
-H "Authorization: Bearer dz_your_token_here"Disconnect revokes the Google grant (best effort), deletes the stored connection even if Google is unreachable, and returns { "status": "disconnected" }. With nothing connected it returns 400 (INTEGRATION_082, "No Gmail account is linked to your user.").
External MCP Servers
A business admin can connect other MCP servers (payroll, payments, CRM and so on) so that Regent can use their tools. Connections live under /mcp-connections/connections; note the repeated segment.
| Method and path | Permission | Purpose |
|---|---|---|
GET /mcp-connections/connections |
read | List connections with their tools |
POST /mcp-connections/connections |
admin | Connect a server with no sign-in or a static header |
GET /mcp-connections/connections/{connection_id} |
read | One connection |
PATCH /mcp-connections/connections/{connection_id} |
admin | Rename, switch off, change trust, replace key |
POST /mcp-connections/connections/{connection_id}/refresh-tools |
admin | Re-read the server's tool list |
DELETE /mcp-connections/connections/{connection_id} |
admin | Disconnect |
POST /mcp-connections/oauth/start |
admin | Begin OAuth for a server that supports it |
GET /mcp-connections/oauth/callback |
none (the server) | Finishes the OAuth connect |
The list returns { "items": [ ...connections ] } with no pagination (a business has at most 20).
Connect with no auth or an API key
curl -X POST "https://api.ondayzero.com/api/v1/mcp-connections/connections" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"name": "Payroll",
"server_url": "https://mcp.payroll.example.com/mcp",
"auth_type": "header",
"headers": { "Authorization": "Bearer YOUR_SERVER_KEY" },
"trust_read_only_hints": false
}'| Field | Description |
|---|---|
name |
Required, 1-120 characters. Its lowercase, underscore-separated form (up to 48 characters) becomes the connection's slug, the handle Regent uses, so it must be unique in the business |
server_url |
Required, https:// endpoint, 8-2048 characters |
auth_type |
none (default) or header. oauth is rejected here; use the OAuth flow |
headers |
Required for header (1 to 10). Names are 1-64 token characters; values 1-4096 characters. Framing and routing headers such as Host, Content-Type, Content-Length and Mcp-Session-Id cannot be set |
trust_read_only_hints |
Default false. See Tool trust |
DayZero contacts the server once to verify it and read its tool list; the connection is stored only if that works. The response (HTTP 201) is the connection:
{
"id": "CONNECTION_UUID",
"slug": "payroll",
"name": "Payroll",
"server_host": "mcp.payroll.example.com",
"auth_type": "header",
"enabled": true,
"trust_read_only_hints": false,
"blocked_tools": [],
"healthy": true,
"last_error_message": null,
"last_used_at": null,
"tools_refreshed_at": "2026-10-11T12:00:00",
"server_info": "Payroll MCP 1.4.0",
"created_at": "2026-10-11T12:00:00",
"tools": [
{ "name": "list_employees", "description": "List employees", "read_only_hint": true, "destructive_hint": false, "policy": "write" }
]
}Responses never echo secrets: the URL is reduced to server_host, and headers, tokens and client secrets are write-only. policy is read, write or blocked.
Connect with OAuth
For servers that support OAuth (with PKCE), send name, server_url and trust_read_only_hints to POST /mcp-connections/oauth/start (HTTP 200):
curl -X POST "https://api.ondayzero.com/api/v1/mcp-connections/oauth/start" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{ "name": "CRM", "server_url": "https://mcp.crm.example.com/mcp", "trust_read_only_hints": false }'DayZero discovers the server's authorization endpoints and registers itself as a client, then returns { "connect_url", "state" } for an admin to open in the same browser (see the connect flow). For servers without dynamic client registration, add oauth_client_id (up to 512 characters) and optionally oauth_client_secret (up to 1024). The redirect URI to register is /api/v1/mcp-connections/oauth/callback on the API host. After approval the callback exchanges the code, reads the tool list and creates the connection; if an OAuth connection with that name and host already exists it replaces its tokens instead, which is how you reconnect. If the name belongs to a non-OAuth connection or a different host, the start call returns 409 (INTEGRATION_089). Tokens refresh automatically before expiry; if the refresh grant is gone the connection is marked healthy: false and needs reconnecting.
Update, refresh and disconnect
curl -X PATCH "https://api.ondayzero.com/api/v1/mcp-connections/connections/CONNECTION_UUID" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{ "enabled": true, "trust_read_only_hints": true, "blocked_tools": ["delete_employee"] }'All fields are optional: name (1-120 characters), enabled (false switches the connection off), trust_read_only_hints, blocked_tools (up to 500 tool names; replaces the list) and headers (replaces the stored headers; an empty object clears them and the connection becomes none; not allowed on OAuth connections). Returns the updated connection. Renaming does not change the slug.
curl -X POST "https://api.ondayzero.com/api/v1/mcp-connections/connections/CONNECTION_UUID/refresh-tools" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"refresh-tools (HTTP 200, no body) re-reads the server's tool list and returns the connection; a rejected credential marks it healthy: false. DELETE /mcp-connections/connections/{connection_id} deletes the connection and its stored credentials and returns { "status": "disconnected" }.
Tool trust
Trust is decided by DayZero, not by the remote server. A tool's policy is:
blockedif its name is inblocked_tools. It can never be called.readonly iftrust_read_only_hintsis on and the server marked the tool read-only and not destructive. Regent runs read tools without asking.writefor everything else, including tools the catalog has not seen. Regent must get a person to approve the exact call first.
Regent can only call tools on enabled connections, with arguments up to 100,000 characters of JSON. Tool results are treated as untrusted third-party text. Each call is recorded in the audit log with a hash of the arguments, not the arguments themselves.
Limits and safety
- At most 20 connections per business (
400,GEN_002). - Server URLs must be
https://, contain no credentials, and resolve only to public addresses; private, loopback, carrier-grade NAT and other reserved ranges are refused, and connections are pinned to the validated addresses. Violations return400(INTEGRATION_086).
Errors
| Status | Code | When |
|---|---|---|
| 400 | GEN_002 |
Body or query failed validation; Meta Ads call while not connected; Meta spend range invalid; MCP connection limit reached or bad headers |
| 400 | INTEGRATION_025 |
Meta app credentials not configured on this environment |
| 400 | INTEGRATION_071 |
QuickBooks not configured on this environment |
| 400 | INTEGRATION_073 |
QuickBooks authorization expired or rejected by Intuit (also on GET /quickbooks/migration-report); connect again |
| 400 | INTEGRATION_091 |
Meta token expired or revoked on POST /meta-ads/accounts/refresh or POST /meta-ads/sync; the connection is flagged needs_reauth, reconnect via /meta-ads/connect-url |
| 404 | INTEGRATION_072 |
QuickBooks not connected (import, cancel, migration report) |
| 409 | INTEGRATION_084 |
A QuickBooks import is already running |
| 400 | INTEGRATION_081 |
Gmail not configured on this environment |
| 400 | INTEGRATION_082 |
No Gmail account linked (disconnect) |
| 400 | INTEGRATION_083 |
Gmail authorization expired; disconnect and connect again |
| 400 | INTEGRATION_086 |
MCP server address not allowed (not https, private network, credentials in URL) |
| 400 | INTEGRATION_087 |
Could not reach or talk to the MCP server |
| 400 | INTEGRATION_088 |
MCP server rejected the credentials, or needs sign-in |
| 409 | INTEGRATION_089 |
An MCP connection with that name already exists |
| 404 | INTEGRATION_090 |
MCP connection not found in this business |
| 404 | NOT_FOUND_001 |
Meta ad account id not found |
| 401 / 403 | AUTH_* |
Missing or invalid token; token scope, role or business access does not allow the call (see Error Handling) |
Not covered
- Per-record detail of what each QuickBooks phase creates; use
GET /quickbooks/coverage-reportandimport_summaryto see counts per object type, then read the results through the public Customers, Vendors, Invoices, Bills, Ledgers and Journal Entries endpoints. - Regent's own tools for these connections (Gmail search and read, calling external MCP tools); they run inside DayZero's chat and are not API endpoints.