Skip to main content
Use this guide when an integration call or connect operation does not succeed. Each case lists the signal you receive, whether the provider request ran, and the next step. The examples use Gmail; every OAuth connector behaves the same way. Two rules apply to every case:
  • Wait with a deadline. Never poll without a limit. Stop at the deadline and start a new connect operation instead of extending the wait.
  • Never replay a write automatically. Call again only when the failure says no provider request was sent. When the outcome is unknown, inspect the provider first.
Signal: The provider returns the person to Veryfront with an error. The OAuth callback redirects to your return address with oauth_error=access_denied and integration=gmail. Other provider errors arrive as oauth_error=provider_error. When the person closes the consent window instead, nothing arrives. Effect: No connection is created or changed. Connection inventory stays as it was. Next step: Ask the person whether they want to connect. Start a new connect operation only if they do. veryfront integration connect fails immediately with integration-consent-denied after a denial, without polling. When nothing arrives, it stops at --timeout seconds (default 300, maximum 3600) with integration-connect-unconfirmed. Press Ctrl+C to cancel sooner.

The connect handoff expired

Signal: Opening the connect URL returns HTTP 400 with Invalid or expired OAuth connect session. Effect: No provider consent started, and inventory is unchanged. Why: POST /oauth/connect/session returns a one-time connect_url and an expires_at deadline 120 seconds away. The URL works once. The deadline covers opening the URL, not the time the person spends on the provider’s consent screen. Next step: Start a new connect operation and open its URL before expires_at. The CLI checks the deadline before it opens the browser.

Wait for a new connection

Confirm a new connection from status and inventory. Record the current status for the scope, start the handoff, show its one-time URL to the person, then poll at a fixed interval and stop at a deadline you choose before you start. The redirect URI is where the browser returns after consent, such as your application’s integration page. Your application supplies presentConnectUrl, which shows the link to the signed-in person:
The example correlates the scope’s OAuth status with an inventory row, so a row from another scope is never accepted. The result is an observed connected identity in that scope, not proof that this particular handoff succeeded: a concurrent connect in the same scope can produce it too. Confirm the account with the person before relying on it. The client-level abort signal ends every request, including connect, at the deadline, and untilDeadline stops waiting for the polling delay at the same deadline. Showing the URL must also finish before its expires_at, because an expired URL cannot start consent. A new connection_generation_id on an existing id means the same account was reconnected. veryfront integration connect performs the same check for you and returns connection_observed with the confirmed row. If the callback arrives but inventory does not show a new generation within its short confirmation window, it returns integration-connect-unconfirmed. Inspect veryfront integration connections gmail --project "<PROJECT_SLUG>" before calling a tool.

The connection expired or was revoked

Signal: The call returns a tool result with isError: true, structuredContent.error set to reconnect_required, and a connectUrl. Inventory can show the row as expired, or as disconnected after a disconnect. The same code is returned when the provider rejects the stored grant, for example after the person revoked access in their provider account settings. Effect: No provider data changed. Veryfront does not fall back to another account, even when the project has one. Next step: Reconnect the same account in the same scope, for example veryfront integration connect gmail --project "<PROJECT_SLUG>" --scope project for a shared connection. The reconnect issues a new connection_generation_id. Read inventory again, update any generation you pinned, and call once.

The connection generation changed

Signal: A call with expected_connection_generation_id (REST and MCP _meta), expectedConnectionGenerationId (GraphQL and TypeScript), or --expected-generation (CLI) fails with the integration-connection-stale condition (status 409): Readiness for the same selection reports selection.state: "stale" and the blocker connection_stale. Effect: The API rejected the call before reading credentials. No provider request was sent. Next step: Read inventory again. Confirm that the row with the same id is still the account you intend to use, then call once with its new generation. Do not select a different account to make the call succeed.

The selected connection is not available

Signal: A call with connection_id fails with validation-failed and the detail Selected connection is not available for this project, integration, and caller. Effect: No provider request was sent, and Veryfront did not substitute another account. Next step: List the project’s connections with the same credential and select a row from that response.

The provider outcome is unknown

Signal: A write tool returns structuredContent.error set to execution_outcome_unknown, the TypeScript client throws IntegrationApiError with outcomeUnknown === true, or the CLI envelope reports outcomeUnknown: true. Effect: The provider may have completed the write. Next step: Check the provider for the result before you repeat anything. The TypeScript client and the CLI never replay the call. A read-only tool that fails at the provider returns a tool result instead of throwing. The result has isError: true and structuredContent.error set to execution_failed; client.call returns it with status: "tool_error". Because a read changes nothing, you can call it once more after the provider recovers.

Verify it worked

  1. Inventory shows the intended row with status: "connected" and the connection_generation_id you plan to pin.
  2. Readiness for that selection reports selection.state: "selected" and local_eligibility.state: "eligible".
  3. One read-only call, such as gmail__list_emails, returns a result with isError unset or false.