Skip to content

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.

  1. Request the connect URL from the browser that will approve it (the DayZero app does this when an admin clicks Connect).
  2. Open connect_url; the person approves access on the provider's page.
  3. The provider redirects to the DayZero callback, which stores the connection and answers with a 302 to the app.
  4. Confirm with the family's status call.
  • 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_mismatch on the redirect). Gmail has no such cookie.
  • The state nonce 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 a 302, never JSON. A user who cancels on the consent screen is redirected with error; 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

bash
curl "https://api.ondayzero.com/api/v1/quickbooks/connect-url" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
json
{ "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

bash
curl "https://api.ondayzero.com/api/v1/quickbooks/status" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
json
{
  "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

bash
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):

json
{ "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.

bash
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

bash
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):

json
{
  "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

bash
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

bash
curl "https://api.ondayzero.com/api/v1/meta-ads/status" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
json
{
  "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

bash
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:

bash
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

bash
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

bash
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
json
{
  "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
bash
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.

bash
curl "https://api.ondayzero.com/api/v1/gmail/status" \
  -H "Authorization: Bearer dz_your_token_here"
json
{
  "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.

bash
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

bash
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:

json
{
  "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):

bash
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

bash
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.

bash
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:

  • blocked if its name is in blocked_tools. It can never be called.
  • read only if trust_read_only_hints is on and the server marked the tool read-only and not destructive. Regent runs read tools without asking.
  • write for 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 return 400 (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-report and import_summary to 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.