Skip to content
Scalekit Docs
Talk to an Engineer Dashboard

Listen for account events

Get a webhook when an AgentKit connected account changes. Verify the signature in Python or Node.js, and ask users to reconnect when an account expires.

Scalekit sends a webhook to your server when a connected account changes: when a user finishes authorizing, when a token can’t be refreshed, or when an account expires or is disconnected. Listen for these events to ask users to reconnect before their next tool call fails, instead of polling every account’s status.

  • A server with a public HTTPS URL that can receive POST requests. While you develop, a tunnel to your local server works.
  • The Scalekit SDK installed, as in the Quickstart. It verifies webhook signatures.
  • The Admin or Developer role, to create an endpoint and read its signing secret. See Team members and roles.
EventSent when
connected_account.createdA connected account is created
connected_account.updatedA connected account’s details are updated
connected_account.deletedA connected account is deleted
connected_account.magic_link_generatedAn authorization link is created for the account
connected_account.oauth_tokens_fetchedThe app returned tokens after the user approved access
connected_account.oauth_succeededThe user finished authorizing, including user verification when it’s on
connected_account.token_refresh_succeededScalekit refreshed the account’s access token
connected_account.token_refresh_failedScalekit couldn’t refresh the access token
connected_account.status_updatedThe account’s status changed, for example from ACTIVE to EXPIRED

To know when a user needs to act, connected_account.status_updated is the one to handle. It’s sent only when the status actually changes, not when an account is created, and it carries the old and new status.

connected_account.token_refresh_failed arrives before the status changes, so its status often still reads ACTIVE, and it doesn’t say why the refresh failed. Use it as an early warning, and wait for status_updated before you ask the user to reconnect.

Every event has the same envelope. The account’s details are in data:

connected_account.status_updated
{
"spec_version": "1",
"id": "evt_101652975398683158",
"type": "connected_account.status_updated",
"occurred_at": "2026-09-26T06:31:34.895815554Z",
"environment_id": "env_88640229614813449",
"object": "ConnectedAccount",
"data": {
"id": "ca_133400349586228019",
"identifier": "user_123",
"connection_id": "conn_133400101014995480",
"connection_name": "github-connect",
"provider": "GITHUB",
"authorization_type": "OAUTH",
"status": "EXPIRED",
"old_status": "ACTIVE"
}
}
FieldMeaning
idThe event’s ID. The same event delivered twice has the same ID
typeThe event name from the table above
occurred_atWhen the change happened
data.idThe connected account’s ID
data.identifierYour app’s ID for the user
data.connection_nameThe connection, as named in AgentKit > Connections. It can be missing from status_updated, so read it as optional and fall back to data.connection_id
data.statusThe status after the change: ACTIVE, PENDING_AUTH, PENDING_VERIFICATION, EXPIRED or DISCONNECTED
data.old_statusThe status before the change. Only in status_updated

Events other than status_updated have no old_status. They also include token_expires_at and last_used_at, which are empty strings when Scalekit has no value. In webhooks, status is a string, such as ACTIVE, in both Python and Node.js.

For every event’s full schema, see Connected account events in the REST reference.

  1. In the Scalekit dashboard, select the environment, then open Developers > Webhooks and select Add Webhook. Enter a Display Name and your Endpoint URL, choose the events under Choose events to subscribe, and save.

    Each environment has its own endpoints. Add one in Production too when you go live.

  2. Open the endpoint and select Show under Signing secret. It starts with whsec_. Store it with your other server secrets, for example as SCALEKIT_WEBHOOK_SECRET, and never expose it to a browser.

  3. Scalekit signs every request with the secret. Verify the signature against the raw request body before you trust the payload, so a forged request can’t prompt the wrong user to reconnect. The SDK checks the webhook-id, webhook-timestamp and webhook-signature headers, and rejects requests older than five minutes.

    webhooks.py
    import json
    import os
    from fastapi import FastAPI, HTTPException, Request
    from scalekit import ScalekitClient
    scalekit_client = ScalekitClient(
    env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
    client_id=os.environ["SCALEKIT_CLIENT_ID"],
    client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
    )
    app = FastAPI()
    seen_event_ids = set() # use your database in production
    @app.post("/webhooks/scalekit")
    async def scalekit_webhook(request: Request):
    body = await request.body()
    headers = {k.lower(): v for k, v in request.headers.items()}
    try:
    scalekit_client.verify_webhook_payload(
    os.environ["SCALEKIT_WEBHOOK_SECRET"], headers, body
    )
    except Exception:
    raise HTTPException(status_code=400, detail="invalid signature")
    event = json.loads(body)
    if event["id"] in seen_event_ids:
    return {"ok": True}
    seen_event_ids.add(event["id"])
    data = event["data"]
    if event["type"] == "connected_account.status_updated":
    # connection_name can be missing; fall back to connection_id
    connection = data.get("connection_name") or data.get("connection_id")
    if data.get("status") in ("EXPIRED", "DISCONNECTED"):
    # Ask this user to reconnect, for example with a new authorization link
    print(f"{data.get('identifier')}: reconnect {connection}")
    elif data.get("status") == "ACTIVE":
    print(f"{data.get('identifier')}: {connection} is connected")
    return {"ok": True}

    To ask the user to reconnect, create an authorization link for data.identifier and data.connection_name, as shown in Authorize a user.

  • Return a 2xx status fast. Do slow work, such as sending an email, after you respond or in a background job. Scalekit retries deliveries that fail or time out.
  • Expect the same event more than once. Retries and resends repeat an event with the same id. Store the IDs you’ve handled, and skip a repeat.
  • Don’t rely on order. Events can arrive out of order. Compare occurred_at, or read the account’s current status (get_connected_account_details in Python, getConnectedAccount in Node.js) before you act on an old event.

In your Development environment, create an authorization link for a test user and complete it. Your endpoint receives events including connected_account.magic_link_generated, connected_account.oauth_succeeded and a connected_account.status_updated whose status is ACTIVE.

The endpoint’s page in Developers > Webhooks lists every delivery and its result. Resend a delivery from there to test your handler again.

The webhook-id, webhook-timestamp and webhook-signature headers didn’t reach the SDK. In Python, pass the headers with lowercase keys. Check that a proxy in front of your server doesn’t strip them.

The SDK raises Invalid signature or Invalid Signature

Section titled “The SDK raises Invalid signature or Invalid Signature”

Python raises Invalid signature, and Node.js raises Invalid Signature. The body you verified isn’t byte-for-byte what Scalekit sent, or the secret is from another endpoint or environment. Verify the raw body before any JSON parsing, and copy the secret again from the endpoint’s page.

The request is more than five minutes old, or your server’s clock is off. Sync the clock, and don’t queue requests before you verify them.

Check that the endpoint is in the environment where the change happened, that it subscribes to the event, and that its URL is reachable from the internet. The delivery list on the endpoint’s page shows failed attempts and their responses.