Subscribing to connection expiry events

Markdown

Composio automatically refreshes OAuth tokens before they expire. But when a refresh token is revoked or expires, the connection enters an EXPIRED state and the user must re-authenticate.

You can detect this proactively using the composio.connected_account.expired webhook event instead of waiting for a tool execution to fail.

This event is only available with V3 webhook payloads. Set the subscription version to V3 when subscribing to connection expiry events.

Subscribe to expiry events

Add composio.connected_account.expired to your webhook subscription's enabled_events:

curl -X POST https://backend.composio.dev/api/v3.1/webhook_subscriptions \
  -H "X-API-KEY: <your-composio-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://example.com/webhook",
    "enabled_events": [
      "composio.trigger.message",
      "composio.connected_account.expired"
    ],
    "version": "V3"
  }'

Handle the event

When a connection expires, Composio sends a webhook with the connected account details:

{
  "id": "evt_847cdfcd-d219-4f18-a6dd-91acd42ca94a",
  "type": "composio.connected_account.expired",
  "metadata": {
    "project_id": "pr_your-project-id",
    "org_id": "ok_your-org-id"
  },
  "data": {
    "id": "ca_your-connected-account-id",
    "toolkit": { "slug": "gmail" },
    "auth_config": {
      "id": "ac_your-auth-config-id",
      "auth_scheme": "OAUTH2"
    },
    "status": "EXPIRED",
    "status_reason": "OAuth refresh token expired"
  },
  "timestamp": "2026-02-06T12:00:00.000Z"
}

Route on type to handle expiry alongside trigger events:

from composio import Composio, WebhookEventType

composio = Composio()

@app.post("/webhook")
async def webhook_handler(request: Request):
    payload = await request.json()
    event_type = payload.get("type")

    if event_type == WebhookEventType.CONNECTION_EXPIRED:
        account_id = payload["data"]["id"]
        toolkit = payload["data"]["toolkit"]["slug"]

        # Look up the user and send them a re-auth link
        session = composio.create(user_id=lookup_user(account_id))
        connection_request = session.authorize(toolkit)
        notify_user(connection_request.redirect_url)

    elif event_type == WebhookEventType.TRIGGER_MESSAGE:
        # Handle trigger events
        pass

    return {"status": "ok"}
export default async function webhookHandler(req: NextApiRequest, res: NextApiResponse) {
  const payload = req.body;

  if (payload.type === 'composio.connected_account.expired') {
    const accountId = payload.data.id;
    const toolkit = payload.data.toolkit.slug;

    // Look up the user and send them a re-auth link
    const session = await composio.create(lookupUser(accountId));
    const connectionRequest = await session.authorize(toolkit);
    if (connectionRequest.redirectUrl) {
      notifyUser(connectionRequest.redirectUrl);
    }

  } else if (payload.type === 'composio.trigger.message') {
    // Handle trigger events
  }

  res.status(200).json({ status: 'ok' });
}

Re-authenticate the user

Use session.authorize() to generate a new Connect Link for the expired toolkit. The user completes OAuth again, and the connected account returns to ACTIVE status.

from composio import Composio

composio = Composio()

session = composio.create(user_id="user_123")
connection_request = session.authorize("gmail")

# Send this URL to the user (email, in-app notification, etc.)
print(connection_request.redirect_url)

# Optionally wait for completion
connected_account = connection_request.wait_for_connection(60000)
print(f"Re-connected: {connected_account.id}")
import { Composio } from '@composio/core';

const composio = new Composio();

const session = await composio.create("user_123");
const connectionRequest = await session.authorize("gmail");

// Send this URL to the user (email, in-app notification, etc.)
console.log(connectionRequest.redirectUrl);

// Optionally wait for completion
const connectedAccount = await connectionRequest.waitForConnection(60000);
console.log(`Re-connected: ${connectedAccount.id}`);

Always verify webhook signatures before processing events in production.