This section as Markdown: View Download

Meta Ads Integration

Connect your agent to Meta's advertising platform to manage campaigns, ad sets, creatives, and performance insights across Facebook and Instagram ads.

Overview

The Meta Ads integration powers the int-metaads-manage skill — creating and managing campaigns, pulling insights, managing creatives, and analyzing ad account data through direct Meta Graph / Marketing API v26 calls.

Skills that use this integration:

  • int-metaads-manage — Campaign / ad set / creative CRUD, insights reporting, direct Graph API v26 operations
  • util-ads-manager-common — Shared ads helpers (works alongside int-metaads-manage and int-googleads-manage)

Agents that typically enable this integration:

  • Meta Ads Manager
  • Any custom agent that needs paid-media capabilities on Meta

Availability

Path Status Notes
Wiro-owned approved-app OAuth Pending provider approval The registry carries this mode, but the dashboard keeps it unavailable while Wiro's Meta app is awaiting the required App Review, Advanced Access, and rollout checks.
"own" Available now You create your own Meta Developer App and connect it to Wiro. No App Review required when Development Mode + App Roles is used.
"api_key" Available now — recommended Paste a Business Manager System User token. No browser OAuth redirect and no App ID or App Secret is submitted to Wiro. Choose Never for token expiration when Meta offers it.

There is no app-less Meta “machine key.” A System User token is the closest machine-to-machine option: it removes the browser OAuth flow and recurring human sign-in, but Meta generates it for a System User through a Business Portfolio and a Meta Business app. The app, System User, ad accounts, Pages, and permissions remain customer-owned.

Today the two enabled modes are the recommended token-only System User path and customer-owned OAuth. Once Wiro's approved app has Advanced Access, a Wiro-owned OAuth option can be enabled without removing either customer-owned path.

Why Wiro remains on direct Graph API v26

Meta's official Ads MCP is not a drop-in replacement yet. Wiro has not verified live parity for:

  • targeting interest search and related raw targeting queries;
  • reach/delivery estimates;
  • lead-form and lead retrieval workflows;
  • raw Graph flexibility needed for account-specific fields, edges, and versioned fallbacks;
  • some financial, media upload, and creative-management operations.

Meta does not document a separate MCP “machine key”; MCP authentication still rests on Meta app/user or System User authorization. Replacing the direct integration is therefore deferred until Wiro completes live parity tests, permission/App Review checks, rate-limit and latency validation, error-shape regression tests, and a controlled rollout with rollback. Until those checks pass, Graph API v26 remains the authoritative runtime path.

Prerequisites

  • A Wiro API key — see Authentication for how to issue keys and sign requests.
  • A deployed agent — see Agent Overview and call POST /UserAgent/Deploy first. You need the returned useragents[0].guid for every step below.
  • A Meta Business account — business.facebook.com.
  • For the currently enabled customer-owned paths, a Meta Developer account and Business app — developers.facebook.com. The future Wiro-owned OAuth path will not require each customer to create an app.
  • For OAuth mode: an HTTPS return URL that your backend controls. Meta's callback is the fixed Wiro URL shown below; redirecturl is where Wiro sends the browser after processing that callback. http://localhost and http://127.0.0.1 are accepted for local development only.

Use this path for a stable server-to-server connection owned by the customer's Business Portfolio.

  1. Open Meta Business Settings → Users → System Users, then create or select a System User.
  2. Assign each required ad account with both ADVERTISE and ANALYZE tasks. For Page-backed creatives, also assign the relevant Facebook Pages.
  3. Generate a token for the Business app used by that System User. Choose Never under Set expiration when Meta offers it; some businesses are required to use an expiring token.
  4. Grant ads_management, ads_read, and business_management. For Page discovery and Page-backed creatives, also grant pages_show_list, pages_read_engagement, and pages_manage_ads.
  5. Save only the connection mode and token to Wiro:
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "useragentguid": "your-useragent-guid",
    "fields": [
      { "credentialkey": "meta-ads", "fieldname": "authmethod", "fieldvalue": "api_key" },
      { "credentialkey": "meta-ads", "fieldname": "systemusertoken", "fieldvalue": "YOUR_SYSTEM_USER_ACCESS_TOKEN" }
    ]
  }'
  1. Ask Wiro to validate the token and discover assigned ad accounts:
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/OAuthConnect" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "useragentguid": "your-useragent-guid",
    "credentialkey": "meta-ads",
    "authmethod": "api_key"
  }'

Wiro validates the token against Meta and returns only active ad accounts whose effective user_tasks contain both ADVERTISE and ANALYZE. The token is encrypted at rest and never reflected to the browser or API caller. Wiro does not require or store an App ID/App Secret for this direct mode.

  1. Persist one or more returned ad accounts with POST /UserAgentOAuth/SetPickerAccounts as shown in Step 10. API clients must perform this request even when only one account is returned.
  2. Optionally discover and select Page mappings with POST /UserAgentOAuth/DiscoverPickerItems, then save them through the chained SetPickerAccounts shape shown in Step 10b.

Customer-owned app OAuth

Every curl example and response shape below matches Wiro's production behavior verified against the source code. Nothing is invented.

Step 1: Create a Meta Developer App

  1. Go to developers.facebook.com/apps and click Create app.
  2. Choose "Other" as the use case, then "Business" as the app type.
  3. Set an App display name (this is what your end users see on the consent screen — use your company or product name, not "Wiro").
  4. Enter an App contact email.
  5. Select the Meta Business Account that owns the ad accounts you plan to manage, then click Create app.

You're now on the app dashboard. The app is in Development Mode by default — leave it there. Development Mode is exactly what lets you skip App Review.

Step 2: Add the Marketing API product

  1. From the app dashboard, click Add product.
  2. Find "Marketing API" and click Set up.
  3. No further configuration is required inside Marketing API itself — adding the product unlocks the ads_* permissions.

Step 3: Add "Facebook Login for Business" and register the redirect URI

Meta Ads OAuth uses Facebook Login under the hood.

  1. Click Add product again.
  2. Find "Facebook Login for Business" and click Set up.
  3. Left sidebar: Facebook Login for Business → Settings.
  4. Scroll to Valid OAuth Redirect URIs and add exactly:

https://api.wiro.ai/v1/UserAgentOAuth/MetaAdsCallback

  1. Save changes at the bottom.

This is the single most common place where own-mode setups fail. The redirect URI must be exact — HTTPS, no trailing slash, same capitalization.

Step 4: Note the required permissions

Wiro requests these exact scopes during OAuth:

ads_management,ads_read,business_management,pages_show_list,pages_read_engagement,pages_manage_ads
Permission Why Wiro requests it
ads_management Create, update, and pause campaigns, ad sets, and ads.
ads_read Read insights, performance metrics, and account metadata.
business_management Discover and validate Business-managed assets used by the full management workflow.
pages_show_list Discover Pages available to the connected principal for each selected ad account.
pages_read_engagement Read page-level engagement metrics that some creative types reference.
pages_manage_ads Use an assigned Page in Page-backed ad creatives.

These permissions normally require App Review/Advanced Access for a multi-tenant Live Mode app. In a customer-owned app's Development Mode they work without App Review for Facebook users listed under that app's Roles. This is a testing/customer-owned fallback and does not make Wiro's future one-click shared-app path live; that path waits for Wiro's app to receive Advanced Access.

Step 5: Copy your App ID and App Secret

App settings → Basic → copy the App ID, click Show next to App Secret and copy that too.

Step 6: Add the users who will connect as Testers (only if they're not you)

  • Connecting your own Facebook account? You're already the app Admin; skip this step.
  • Connecting a different Facebook account (typical for SaaS customers)? Go to App Roles → Roles → Add People and invite them as Testers or Developers. They accept at facebook.com/settings → Business Integrations.

Users not listed in App Roles will be blocked at the consent screen in Development Mode.

Step 7: Save your Meta App credentials to Wiro

Push the appid and appsecret into the agent's meta-ads credential group. Wiro merges credential updates per group — fields you don't send are preserved, and credentials from other groups are untouched.

curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "useragentguid": "your-useragent-guid",
    "fields": [
      { "credentialkey": "meta-ads", "fieldname": "appid", "fieldvalue": "YOUR_META_APP_ID" },
      { "credentialkey": "meta-ads", "fieldname": "appsecret", "fieldvalue": "YOUR_META_APP_SECRET" },
      { "credentialkey": "meta-ads", "fieldname": "authmethod", "fieldvalue": "own" }
    ]
  }'

Successful response:

{
  "result": true,
  "errors": [],
  "applied": 3
}

Credentials sent at Deploy: Deploy saves appid, appsecret and authmethod sent in the meta-ads group of its credentials body when they pass validation, but it doesn't report a result for each field. Call this step anyway and check its response before initiating OAuth.

Only user-writable fields are accepted. appid and appsecret are user-writable in the meta-ads credential. Fields the connection flow fills in (the ad account and the selected Facebook Pages) are rejected with <Field label> is managed by the connection flow in errors[], and nothing in that request is saved. appid and appsecret are not returned by POST /UserAgent/Detail, so check result and errors[] on the CredentialUpsert response to confirm the write.

Step 8: Initiate OAuth

Start the flow with authmethod: "own" so Wiro uses your customer-owned appid and appsecret.

curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/OAuthConnect" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "useragentguid": "your-useragent-guid",
    "credentialkey": "meta-ads",
    "redirecturl": "https://your-app.com/settings/integrations",
    "authmethod": "own"
  }'

Response:

{
  "result": true,
  "authorizeUrl": "https://www.facebook.com/v26.0/dialog/oauth?client_id=...&redirect_uri=https%3A%2F%2Fapi.wiro.ai%2Fv1%2FUserAgentOAuth%2FMetaAdsCallback&state=...&scope=ads_management%2Cads_read%2Cbusiness_management%2Cpages_show_list%2Cpages_read_engagement%2Cpages_manage_ads",
  "errors": []
}

Redirect the user's browser to authorizeUrl. Full-page redirect is recommended over a popup — some browsers block third-party cookies in popups, breaking the OAuth session.

State TTL: Wiro caches the OAuth state for 15 minutes. If the user takes longer to complete consent, the callback returns metaads_error=session_expired and you must call OAuthConnect again.

Step 9: Handle the callback

After the user consents, Meta sends them back to Wiro's callback URL. Wiro exchanges the code for a long-lived token, verifies its App ID, expiry, data-access expiry, and required scopes through debug_token, then fetches the user's eligible ad accounts. It writes the token into the agent's config and redirects the user to your redirecturl with query parameters.

Success URL looks like:

https://your-app.com/settings/integrations?metaads_connected=true&metaads_accounts=%5B%7B%22id%22%3A%22123456789%22%2C%22name%22%3A%22My%20Ad%20Account%22%7D%5D
  • metaads_accounts is encodeURIComponent(JSON.stringify([...])).
  • Each array element: { id, name }.
  • The id has the act_ prefix stripped by Wiro.
  • Only ad accounts with account_status === 1 and effective user_tasks containing both ADVERTISE and ANALYZE are included. Read-only ANALYZE accounts are excluded because this skill supports mutations.

Parse in the browser:

const params = new URLSearchParams(window.location.search);

if (params.get("metaads_connected") === "true") {
  const accounts = JSON.parse(decodeURIComponent(params.get("metaads_accounts") || "[]"));
  if (accounts.length === 0) {
    showError("No active ad accounts found on this Meta user.");
  } else if (accounts.length === 1) {
    await setAdAccount(accounts[0]);
  } else {
    presentAccountPicker(accounts);
  }
} else if (params.get("metaads_error")) {
  handleError(params.get("metaads_error"));
}

Step 10: Persist the ad account selection

Persist one or more discovered ad accounts. This is required for the connection to become active. The Wiro dashboard auto-selects a sole result and opens a multi-select picker when several accounts are returned; API clients must always call this endpoint explicitly.

curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/SetPickerAccounts" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "useragentguid": "your-useragent-guid",
    "credentialkey": "meta-ads",
    "accounts": [
      { "adaccountid": "123456789", "adaccountname": "My Ad Account" }
    ]
  }'

Response:

{
  "result": true,
  "accounts": [
    { "id": "123456789", "name": "My Ad Account" }
  ],
  "errors": []
}

Behavior:

  • Pass the ad account ID without the act_ prefix. If you include it, Wiro strips it automatically.
  • adaccountname is optional but recommended — it surfaces in OAuthStatus responses and dashboards.
  • Pass multiple { adaccountid, adaccountname } entries to authorize the agent against several ad accounts at once.
  • If the agent was running (status 3 or 4), Wiro marks it status: 1 and restarts it after the stop cycle, so the daemon picks up the new ad account. No manual Start needed.

Step 10b: Discover and select Facebook Pages (optional)

After the ad-account selection is saved, discover Pages that Meta reports as promotable for each selected account:

curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/DiscoverPickerItems" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "useragentguid": "your-useragent-guid",
    "credentialkey": "meta-ads",
    "pickerkey": "pages"
  }'

Response:

{
  "result": true,
  "pickerkey": "pages",
  "groups": [
    {
      "parent": {
        "adaccountid": "123456789",
        "adaccountname": "My Ad Account"
      },
      "items": [
        { "pageid": "111222333", "pagename": "My Facebook Page" }
      ]
    }
  ],
  "errors": []
}

Save the selected Pages within five minutes. Include every successfully returned parent exactly once; an empty items array means no Page is selected for that ad account:

curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/SetPickerAccounts" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "useragentguid": "your-useragent-guid",
    "credentialkey": "meta-ads",
    "pickerkey": "pages",
    "selections": [
      {
        "parentvalue": "123456789",
        "items": [
          { "pageid": "111222333", "pagename": "My Facebook Page" }
        ]
      }
    ]
  }'

The dashboard runs this chained flow automatically after ad-account selection. It auto-selects the only Page when there is exactly one candidate; otherwise it shows Pages grouped by ad account and permits multiple selections. The Page step is optional, so reporting and non-Page operations remain available when no Page is selected.

Step 11: Verify the connection

curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/OAuthStatus" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "useragentguid": "your-useragent-guid",
    "credentialkey": "meta-ads"
  }'

Response:

{
  "result": true,
  "connected": true,
  "accounts": [
    { "id": "123456789", "name": "My Ad Account" }
  ],
  "pagemappings": [
    {
      "adaccountid": "123456789",
      "adaccountname": "My Ad Account",
      "pageid": "111222333",
      "pagename": "My Facebook Page"
    }
  ],
  "pickersteps": {
    "pages": {
      "pickerkey": "pages",
      "optional": true,
      "configured": true,
      "groups": [
        {
          "parent": {
            "adaccountid": "123456789",
            "adaccountname": "My Ad Account"
          },
          "items": [
            { "pageid": "111222333", "pagename": "My Facebook Page" }
          ]
        }
      ]
    }
  },
  "connectedat": "2026-04-17T12:00:00.000Z",
  "tokenexpiresat": "",
  "errors": []
}

Field notes:

  • OAuth mode requires authmethod: "own" and an access token. Direct mode requires authmethod: "api_key", a validated systemusertoken, and a successful probe marker; it does not require App ID or App Secret. Both modes require at least one selected ad account before connected becomes true.
  • accounts[] carries one entry per selected ad account (id = adaccountid without act_ prefix, name = adaccountname). Empty name strings appear when no adaccountname was supplied in Step 10.
  • pickersteps.pages.groups[] and pagemappings[] expose the saved Page selections without any access token.
  • tokenexpiresat comes from Meta's OAuth token response. It is empty for the token-only System User mode because Wiro stores the supplied token unchanged. If an expiring System User token is used, the operator must replace it before Meta invalidates it.
  • Meta User OAuth has no generic refresh-token contract. When a wiro or own User token expires or is invalidated, reconnect through the browser.

Step 12: Start the agent if it's not running

curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{ "guid": "your-useragent-guid" }'

Check POST /UserAgent/Detail first: if setuprequired is still true, some other credential the agent requires is missing — Start will refuse. See Agent Credentials — Setup Required.

Agents already running when you connected Meta Ads restart automatically.

How Meta Ads uses selected Pages

Meta Ads has no scalar/default pageid. Step 10b persists account-scoped pagemappings, and the runtime exposes each selected ad account with its own pages[] list.

  • Zero Pages: only Page-backed creative creation is unavailable; reporting and other account operations continue.
  • One Page under the target ad account: the agent uses it automatically.
  • Multiple Pages: the agent requires an explicit Page ID/name in the request or asks the operator which Page to use. It never picks the first Page or borrows a Page from another ad account.

Before creating the creative, the skill verifies the chosen Page through Meta. The connected person or System User must still have the required Page task and the Page must be usable with the resolved ad account.

If you need organic posting (writing posts directly to a Facebook Page rather than running ads), that's a separate integration — see the Facebook Page integration. The int-facebookpage-post skill uses the facebook-pages credential group, not meta-ads.

API Reference

All endpoints require Wiro authentication — see Authentication for x-api-key + optional signature headers.

POST /UserAgentOAuth/OAuthConnect

Parameter Type Required Description
useragentguid string Yes Agent instance GUID.
credentialkey string Yes "meta-ads".
redirecturl string OAuth only HTTPS URL (or http://localhost / http://127.0.0.1 for dev) where users return after consent. Direct System User token validation does not use this field.
authmethod string No "api_key" for the recommended System User token mode (registry default) or "own" for advanced customer-owned app OAuth.

OAuth response: { result, authorizeUrl, errors }. Direct System User token response: { result, accounts: [{id, name}], errors } with no authorizeUrl. If result: false, inspect errors[0].message — common messages: Missing useragentguid, Missing credentialkey, Missing redirecturl (OAuth only), Invalid redirect URL, User agent not found or unauthorized, and Meta Ads credentials not configured (own mode without prior Update).

GET /UserAgentOAuth/MetaAdsCallback

Server-side endpoint invoked by Meta. You don't call it — you only handle the final redirect back to your redirecturl. The callback path is per-provider — Meta Ads's stays MetaAdsCallback.

Query param Meaning
metaads_connected=true OAuth completed successfully.
metaads_accounts URL-encoded JSON array of { id, name } for active ad accounts.
metaads_error=<code> OAuth failed. See Troubleshooting.

POST /UserAgentOAuth/SetPickerAccounts

The same endpoint persists both picker stages.

Root ad-account selection:

Parameter Type Required Description
useragentguid string Yes Agent instance GUID.
credentialkey string Yes "meta-ads".
accounts array Yes One or more { adaccountid, adaccountname } entries. adaccountid must be without the act_ prefix (prefix stripped automatically if sent); adaccountname is the display name shown in dashboards and OAuthStatus responses.

Response: { result, accounts: [{id, name}, ...], errors }. Triggers an automatic agent restart if the agent was running.

Chained Page selection, after DiscoverPickerItems:

Parameter Type Required Description
useragentguid string Yes Agent instance GUID.
credentialkey string Yes "meta-ads".
pickerkey string Yes "pages".
selections array Yes One { parentvalue, items } entry for every successful discovery group. parentvalue is the ad account ID; each item carries pageid and pagename. Empty items is allowed because Pages are optional.

Response: { result, pickerkey: "pages", pagemappings: [{adaccountid, adaccountname, pageid, pagename}], errors }.

POST /UserAgentOAuth/DiscoverPickerItems

Discovers eligible Facebook Pages grouped under the already-selected ad accounts. This call must follow root ad-account SetPickerAccounts.

Parameter Type Required Description
useragentguid string Yes Agent instance GUID.
credentialkey string Yes "meta-ads".
pickerkey string Yes "pages".

Response: { result, pickerkey: "pages", groups: [{parent: {adaccountid, adaccountname}, items: [{pageid, pagename}], errorcode?}], errors }. Wiro keeps the active token server-side and caches only secret-free candidates for five minutes. Call the chained SetPickerAccounts shape before they expire.

POST /UserAgentOAuth/OAuthStatus

Parameter Type Required Description
useragentguid string Yes Agent instance GUID.
credentialkey string Yes "meta-ads".

Response fields:

Field Type Description
connected boolean true when the selected customer-owned mode has a validated credential and at least one selected ad account. Note: this reflects connection completion, not readiness of unrelated required credentials. Use setupcomplete from POST /UserAgent/Detail for whole-agent readiness.
accounts array One { id, name } per selected ad account (id = adaccountid without act_ prefix, name = adaccountname).
pagemappings array Flat, account-scoped Page mappings. Each entry contains adaccountid, optional adaccountname, pageid, and pagename.
pickersteps.pages object Grouped Page status for UI rendering: { pickerkey, optional, configured, groups[] }.
connectedat string ISO timestamp of connection.
tokenexpiresat string ISO expiry for OAuth tokens. Empty for direct System User mode because Wiro stores the supplied token unchanged.

POST /UserAgentOAuth/OAuthDisconnect

Body: { useragentguid, credentialkey: "meta-ads" }. Clears the active token, probe marker, ad-account selection, and Page mappings. Customer-owned OAuth App ID/App Secret values are preserved for reconnect; direct mode's systemusertoken is cleared. This endpoint clears Wiro's copy, so administrators should also revoke the app or token in Meta Business Settings when immediate provider-side invalidation is required.

curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/OAuthDisconnect" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "useragentguid": "your-useragent-guid",
    "credentialkey": "meta-ads"
  }'

Response: { "result": true, "errors": [] }. Running agents restart automatically.

Token lifecycle

Wiro does not exchange or renew a direct System User token. Prefer Never expiration when Meta offers it; otherwise generate a replacement and reconnect before the token expires. User OAuth connections must be reconnected after Meta expires or invalidates them. Raw tokens and App Secrets are never exposed to the agent model.

Using the Skill

Enable int-metaads-manage on the agent via POST /UserAgent/SkillsApply (see Agent Skills → Toggling Integration Skills). Adjust the cron of the built-in cs-cron-metaads-performance-reporter task (Meta Ads Manager) with enabled and interval only — the task body (value) is owned by the bundled integration skill and silently dropped on writes:

curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "useragentguid": "your-useragent-guid",
    "skillkey": "cs-cron-metaads-performance-reporter",
    "enabled": true,
    "interval": "0 9 * * *"
  }'

To change what the reporter includes (thresholds, reporting preferences, holiday markets), edit the paired preference skill ad-strategy instead — see Agent Skills → Updating Preferences.

Troubleshooting

Error code Meaning What to do
missing_params Callback was hit without state or code. Don't hit the callback URL directly. Start a new flow from Step 8.
session_expired More than 15 minutes elapsed between OAuthConnect and the consent return. Call OAuthConnect again to refresh the state.
authorization_denied User clicked Cancel, or Facebook returned error=access_denied. In Development Mode this also happens when the user isn't listed under App Roles. Add the user as a Tester (Step 6), have them accept, retry.
token_exchange_failed Facebook rejected the token exchange. Usually wrong App Secret, revoked app, or redirect URI mismatch. Re-copy the App Secret from Settings → Basic, verify the redirect URI exactly matches, retry.
useragent_not_found Wrong useragentguid or agent doesn't belong to your API key's user. Fetch the correct guid with POST /UserAgent/MyAgents.
Meta Ads credentials not configured Returned in OAuthConnect's errors[] when authmethod: "own" but appid / appsecret are missing. Call POST /UserAgent/CredentialUpsert to add meta-ads.appid and meta-ads.appsecret, then retry OAuthConnect.
required_meta_scopes_missing debug_token did not report every scope required by the full management skill. Grant the listed scopes to the app/token and reconnect.
meta_app_mismatch The token was issued to a different Meta App ID. Generate or authorize the token with the same app whose App ID and Secret were saved.
meta_token_expired The OAuth token or its data-access window has expired. Reconnect customer-owned OAuth. Direct mode does not use debug_token; replace a rejected/expired System User token in Business Settings.
no_accounts No active account has both ADVERTISE and ANALYZE tasks. Assign the connected principal those tasks on an active ad account in Business Settings, then reconnect.
Picker candidates expired. Discover them again. More than five minutes elapsed after Page discovery, or the parent account connection changed. Call DiscoverPickerItems again, then resubmit the Page selection.
selections must include every available parent exactly once The Page selection omitted or duplicated a successful ad-account group. Include one selections[] entry per successful groups[] entry; use items: [] to select no Page for a parent.
internal_error Unexpected server error during callback processing. Retry once. If it persists, contact Wiro support with the timestamp and your useragentguid.

Facebook shows a yellow banner in Development Mode. This is expected and not a blocker — users listed under App Roles can click Continue and finish authorization. Users outside App Roles are hard-blocked.

connected: false after completing OAuth

OAuthStatus returns connected: true only when the selected mode has a validated credential and at least one ad account was saved. If you skipped Step 10 (SetPickerAccounts), connected remains false. If account selection is complete but the status is still false, check the callback or direct-probe error and reconnect.

Token keeps expiring

User OAuth and System User tokens do not have the same lifecycle:

  • User OAuth (wiro / own) — reconnect through the browser after expiry or invalidation.
  • System User (api_key) — choose Never expiration when Meta permits it. If Meta requires an expiring token, generate a replacement and reconnect before expiry. Wiro does not renew or exchange it.

Multi-Tenant Architecture

For SaaS products connecting many customers' Meta Ads accounts through a single Wiro-powered backend:

  1. Recommended today: use one System User token per customer-owned Business Portfolio when that operating model fits. Customer-owned OAuth remains available for interactive user authorization; Wiro-owned OAuth stays disabled until Advanced Access and rollout approval.
  2. One Wiro agent instance per customer. Call POST /UserAgent/Deploy during onboarding, then complete the recommended direct steps or the advanced OAuth flow for that customer's useragentguid.
  3. Tokens are isolated per agent instance. Customer A's Meta token is never visible to Customer B — they live under different useragentguid values.
  4. Consent branding follows the selected path. Customer-owned OAuth shows the customer's app; the future simple path shows Wiro's reviewed app.
  5. Customer-owned Development Mode: add each connecting user to that app's Roles. Multi-tenant use outside app roles requires the appropriate App Review/Advanced Access; do not treat Development Mode as production approval.
  6. Rate limits are per app, not per customer. The Marketing API tier (Development → Standard → Advanced) governs aggregate call volume. See Meta's Rate Limiting docs.