Skip to content
Scalekit Docs
Talk to an Engineer Dashboard

Receive trigger events

Verify and parse AgentKit trigger events in Python or Node.js, respond fast, skip repeats with dedupe_key, and read each field of the event.

Each time a trigger fires, Scalekit sends your delivery endpoint an HTTP POST with the event as JSON. Your server verifies the signature, returns a 2xx status quickly and hands the event to your agent. This page shows a handler in Python and Node.js, how to handle repeats and retries, and what each field in the event means.

  • A delivery endpoint and its signing secret, from Set up a trigger. The code below reads the secret from SCALEKIT_TRIGGER_SECRET.
  • Your Scalekit API credentials in SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID and SCALEKIT_CLIENT_SECRET, as in the Quickstart. The SDK client verifies the signature.

Scalekit signs every request with the endpoint’s secret. Verify the signature against the raw request body before you trust the event, so a forged request can’t make your agent act. The SDK checks the webhook-id, webhook-timestamp and webhook-signature headers and rejects requests more than five minutes old. Then parse the body as JSON.

Install the dependencies, then run the server with uvicorn triggers:app --port 3000:

Terminal window
pip install scalekit-sdk-python fastapi uvicorn
triggers.py
import json
import os
from fastapi import FastAPI, Request, Response
from scalekit import ScalekitClient
from scalekit.common.exceptions import WebhookVerificationError
scalekit_client = ScalekitClient(
os.environ["SCALEKIT_ENVIRONMENT_URL"],
os.environ["SCALEKIT_CLIENT_ID"],
os.environ["SCALEKIT_CLIENT_SECRET"],
)
app = FastAPI()
handled = set() # use your database in production
@app.post("/scalekit/triggers")
async def scalekit_trigger(request: Request):
body = await request.body() # the exact bytes Scalekit signed
try:
scalekit_client.verify_webhook_payload(
os.environ["SCALEKIT_TRIGGER_SECRET"], request.headers, body
)
except WebhookVerificationError:
# A bad or missing signature, or a request older than five minutes
return Response(status_code=400)
event = json.loads(body)
# Retries repeat an event. Key on dedupe_key plus the account you act as.
key = (event["dedupe_key"], event["connected_account_id"])
if key in handled:
return Response(status_code=204)
if event["delivery_scope"] == "account":
print(f"{event['trigger_type']} for {event['connected_account_id']}: {event.get('resource_id')}")
else:
# A connection-wide event names no account: your app decides who acts on it
print(f"{event['trigger_type']} on {event['connection_id']}: {event.get('resource_id')}")
# Start your agent in a background job here, then respond
handled.add(key)
return Response(status_code=204)

The signature covers the body byte for byte. Verify the raw body exactly as received: parsing the JSON first and serializing it again changes the bytes and fails verification.

  • Return a 2xx status fast. Start the agent’s work after you respond or in a background job. A response other than 2xx, or no response within 30 seconds, counts as a failure and is retried.
  • Verify before you queue. Verify the signature in the handler that receives the request, then queue the verified event. A queued request can be older than five minutes by the time a worker reads it, and the SDK rejects it.
  • Expect the same event more than once. Delivery is at least once. Scalekit sends an event again when it doesn’t get a 2xx from your endpoint within 30 seconds, including when your server handled the event but its response was slow or didn’t arrive. Each repeat carries the same dedupe_key. Store the keys you’ve handled and skip a repeat. When one connection-wide event makes several of your users act, store the key together with each user’s account, so a retry after a partial failure skips only the ones that finished.
  • Don’t rely on order. Events can arrive out of order. Use occurred_at when order matters.

Scalekit makes up to 8 attempts to deliver an event, with increasing delays between them. Any status outside 2xx is a failure, including 4xx, and each attempt waits up to 30 seconds for a response.

AttemptDelay after the previous attempt
1Immediately
25 seconds
35 minutes
430 minutes
52 hours
65 hours
710 hours
810 hours

If the eighth attempt fails, about 27 hours after the first, the delivery is marked as failed. Each attempt and your server’s response code appear under Recent delivery attempts on Developers > Trigger Delivery.

Every event has the same shape, for webhook and polling triggers alike. This event came from a Salesforce Lead created subscription:

salesforce_lead_created
{
"version": "1",
"trigger_type": "salesforce_lead_created",
"subscription_id": "trgs_141020933185734981",
"delivery_scope": "account",
"connection_id": "conn_133400101014995480",
"connected_account_id": "ca_133400349586228019",
"resource_type": "salesforce_lead_created",
"resource_id": "00Q5g00000AbCdEFGH",
"occurred_at": "2026-10-10T09:14:03Z",
"detection_mode": "poll",
"payload_state": "full",
"payload": {
"attributes": {
"type": "Lead",
"url": "/services/data/v66.0/sobjects/Lead/00Q5g00000AbCdEFGH"
},
"Id": "00Q5g00000AbCdEFGH",
"Name": "Jane Doe",
"FirstName": "Jane",
"LastName": "Doe",
"Email": "jane@example.com",
"Phone": null,
"Company": "Example Corp",
"Title": "VP Sales",
"Status": "Open - Not Contacted",
"LeadSource": "Web",
"OwnerId": "0055g00000XyZabCDE",
"CreatedDate": "2026-10-10T09:14:03.000+0000"
},
"dedupe_key": "ca_5F133400349586228019.trgs_5F141020933185734981.00Q5g00000AbCdEFGH",
"correlation_id": "ca_5F133400349586228019.trgs_5F141020933185734981.00Q5g00000AbCdEFGH"
}
FieldMeaning
versionThe event format’s version, "1"
trigger_typeThe trigger that fired, such as salesforce_lead_created
subscription_idThe subscription that produced the event. Tells apart events from two subscriptions on the same account
delivery_scopeaccount: the event is about one connected account. connection: the event is about the whole connection
connection_idThe connection the event came from. Always present
connected_account_idThe connected account, for account events. An empty string for connection events: branch on delivery_scope, not on this field
resource_typeThe kind of record that changed
resource_idThe app’s ID for the record, such as a Salesforce lead ID. Can be missing
occurred_atWhen the change happened in the app, in UTC (RFC 3339). Can be missing
detection_modepoll for a polling trigger, webhook for a webhook trigger
payload_statefull: payload holds the record. reference: the record couldn’t be included, for example because it’s too large, and payload is null. Fetch the record from the app with resource_id
payloadThe app’s own record or event, as its API returns it, with the app’s field names
dedupe_keyThe same on every delivery of this event. Use it to skip repeats
correlation_idCarry it into the work the event starts, to trace that work back to the event

These fields are the same for every trigger. Only payload differs: it holds the app’s own data, so its fields depend on the trigger. For a polling trigger it’s the record, as above. For a webhook trigger it’s the request body the provider sent. Each connector’s page describes what its triggers send, as in Slack triggers.

The fields keep these snake_case names in every language. occurred_at is an RFC 3339 string in UTC.

  • Treat dedupe_key and correlation_id as opaque strings. Compare them; don’t parse them. Their format can change.
  • Expect new fields and values. Later versions can add fields and new values for delivery_scope, detection_mode and payload_state. Ignore fields you don’t know, and keep a default branch for unknown values.
  • Read payload with the app’s field names. It’s the app’s own data, not a Scalekit format, so its shape differs per connector and trigger.

Make a change the trigger watches for, as in Set up a trigger. Your server prints a line such as salesforce_lead_created for ca_133400349586228019: 00Q5g00000AbCdEFGH, and Recent delivery attempts on Developers > Trigger Delivery shows the attempt with status 204.

The webhook-id, webhook-timestamp and webhook-signature headers didn’t reach the SDK. Check that a proxy in front of your server doesn’t strip them, and pass the request’s headers object unchanged.

Node.js spells it Invalid Signature. The body you verified isn’t byte for byte what Scalekit sent, or the secret belongs to another endpoint or environment. Verify the raw body before any JSON parsing, and copy the secret again with Reveal on Developers > Trigger Delivery.

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

My endpoint returns 2xx but the event is handled twice

Section titled “My endpoint returns 2xx but the event is handled twice”

A retry or a second delivery repeated the event. Skip events whose dedupe_key, together with the account you act as, you’ve already handled.