Skip to main content
Webhooks

Receive Webhook events

Verify Webhook requests and reliably process events that may be delivered more than once.

Webhook is currently in Beta. APIs, fields, and behavior may change in future versions.

HTTP request

When an event occurs, Qoder Cloud Agents sends an HTTP POST request to the Endpoint URL:
POST /webhooks/qoder HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: QoderCloudAgents-Webhook/1.0
Webhook-ID: whe_01k1jbxexample
Webhook-Event-Type: forward.schedule_run.succeeded
Webhook-Timestamp: 1788253679
Webhook-Signature: v1,BASE64_HMAC_SHA256
HeaderDescription
Webhook-IDEvent ID. It remains the same across retries and can be used as the consumer idempotency key.
Webhook-Event-TypeEvent type.
Webhook-TimestampUnix timestamp in seconds used to generate the signature.
Webhook-SignatureRequest signature, currently formatted as v1,<base64>.
Any 2xx response indicates successful processing. Verify the signature, write the event to your reliable queue, and return the response promptly.

Event structure

Forward business events use a common envelope:
{
  "type": "event",
  "id": "whe_01k1jbxexample",
  "created_at": "2026-09-01T09:07:59Z",
  "data": {
    "type": "forward.schedule_run.succeeded",
    "source": "tool",
    "source_session_id": "sess_origin",
    "id": "srun_01k1jbrexample",
    "schema_version": 1,
    "run_id": "srun_01k1jbrexample",
    "schedule_id": "sched_01k1jbqexample",
    "identity_id": "idn_01k1jbnexample",
    "template_id": "tmpl_01k1jbmexample",
    "status": "completed",
    "trigger_type": "schedule",
    "attempt": 1,
    "push_status": "succeeded",
    "duration_ms": 15432
  }
}
FieldTypeDescription
typestringAlways event.
idstringWebhook event ID. Matches Webhook-ID.
created_atstringEvent time, in RFC 3339 format.
data.typestringEvent type. Matches Webhook-Event-Type.
data.idstringID of the resource that emitted the event.
data.schema_versionintegerEvent data version. Currently 1.
Ignore new fields you do not recognize to remain compatible with future extensions.

Schedule events

The data of all four Schedule events below includes source and source_session_id. See Schedule creation source for their meaning. Run events inherit the parent Schedule's creation source. Events newly enqueued after service deployment always include both fields. Historical events and their retries may omit them, so clients must tolerate missing fields. If needed, query the source through Get a schedule or Get a schedule run.

forward.schedule.created

Sent when a Schedule is created successfully:
{
  "type": "event",
  "id": "whe_01k1jbxcreated",
  "created_at": "2026-09-01T09:00:00Z",
  "data": {
    "type": "forward.schedule.created",
    "source": "api",
    "source_session_id": null,
    "id": "sched_01k1jbqexample",
    "schema_version": 1,
    "schedule_id": "sched_01k1jbqexample",
    "identity_id": "idn_01k1jbnexample",
    "template_id": "tmpl_01k1jbmexample",
    "status": "active",
    "trigger_policy_type": "cron",
    "next_trigger_at": "2026-09-02T01:00:00Z"
  }
}
trigger_policy_type and next_trigger_at are returned only when values are available.

forward.schedule.archived

Sent when a Schedule first changes from unarchived to archived. data.status is always archived. This covers the following archive paths:
  • Calling the archive API or the delete_forward_schedule tool in a Session.
  • Batch archiving, with one event for each Schedule actually archived by that operation.
  • Automatic archiving after a one-time task finishes, or when a trigger plan has no next run because of stop_at.
  • Cascaded archiving when clearing an Identity, and archiving triggered by unavailable dependencies. Pausing a Schedule alone does not send this event.
{
  "type": "event",
  "id": "whe_01k1jbxarchived",
  "created_at": "2026-09-22T02:00:00Z",
  "data": {
    "type": "forward.schedule.archived",
    "id": "sched_01k1jbqexample",
    "schema_version": 1,
    "schedule_id": "sched_01k1jbqexample",
    "identity_id": "idn_01k1jbnexample",
    "template_id": "tmpl_01k1jbmexample",
    "source": "tool",
    "source_session_id": "sess_origin",
    "status": "archived",
    "archived_at": "2026-09-22T02:00:00Z"
  }
}
FieldDescription
schedule_idID of the archived Schedule, the same as data.id.
identity_id, template_idIdentity and Template that the Schedule belongs to.
source, source_session_idThe Schedule's creation source, not the source of this archive operation. For API creation, these are api and null; for tool creation, they are tool and the ID of the Session that created it.
statusAlways archived. When querying a Schedule object, continue to determine its archive state from a non-null archived_at.
archived_atActual archive time in RFC 3339 format. The outer created_at corresponds to this time.
The archive state and notification event are saved in the same transaction. Archiving the Schedule again does not generate a new event, but delivery retries can deliver the same event more than once. Deduplicate by Webhook-ID. Previously archived historical data does not receive backfilled notifications. This event means the Schedule is archived. It does not mean existing Runs have finished or that all other cleanup steps for clearing an Identity are complete. Delivery order is not guaranteed between archive, creation, and Run result notifications. When synchronizing local state, do not let a late creation notification reactivate an archived task. This is a separate Webhook notification and does not add an archive event to the Session. Existing Endpoints must add forward.schedule.archived to events; Endpoints subscribed to * receive it automatically.

forward.schedule_run.succeeded

Sent when a Schedule Run completes successfully. data.status is completed.

forward.schedule_run.failed

Sent when a Schedule Run fails. The structure is the same as the success event, data.status is failed, and error_type may be included:
{
  "type": "event",
  "id": "whe_01k1jbxfailed",
  "created_at": "2026-09-01T09:07:59Z",
  "data": {
    "type": "forward.schedule_run.failed",
    "source": "tool",
    "source_session_id": "sess_origin",
    "id": "srun_01k1jbrfailed",
    "schema_version": 1,
    "run_id": "srun_01k1jbrfailed",
    "schedule_id": "sched_01k1jbqexample",
    "identity_id": "idn_01k1jbnexample",
    "template_id": "tmpl_01k1jbmexample",
    "status": "failed",
    "trigger_type": "schedule",
    "attempt": 1,
    "push_status": "failed",
    "duration_ms": 1200,
    "error_type": "runtime_error"
  }
}
Use the corresponding Schedule Run query API as the source of truth for business results. Webhooks notify you of status changes.

Managed Agents resource events

Forward also supports subscribing to Session, Session Thread, Agent, Environment, Memory Store, Vault, and Vault Credential events. See Supported public events for full event names and triggers. These events use the same outer envelope, but data carries only a resource reference. It does not include the schema_version, business status, or result body of Schedule events. For example, when a Session becomes idle:
{
  "type": "event",
  "id": "whe_01k1jbxidled",
  "created_at": "2026-09-01T09:07:59Z",
  "data": {
    "type": "session.status_idled",
    "id": "sess_01k1jbsexample"
  }
}
Event familydata.idAdditional fields
session.status_*, session.updated, session.deletedSession IDNone.
session.thread_*ID of the parent Sessionsession_thread_id: the specific thread ID.
agent.*Managed Agent IDNone.
environment.*Environment IDNone.
memory_store.*Memory Store IDNone.
vault.*Vault IDNone.
vault_credential.*Vault Credential IDvault_id: ID of the parent Vault.
The * in this table only groups event families. It is not a prefix wildcard you can put in events. After a notification arrives, query the current details by resource ID. Resources referenced by deletion events may no longer be queryable. session.status_idled does not mean the business run succeeded; judge the outcome from the Session output and status. webhook.test does not use the resource event structure above. See Send a test event for its payload.

Verify signatures

The signing_secret returned when an Endpoint is created verifies request signatures. The signed content combines three parts:
signed_content = Webhook-ID + "." + Webhook-Timestamp + "." + raw_body
Use the raw request body before parsing or reserialization. Python example:
import base64
import hashlib
import hmac
import time


def verify_webhook(
    secret: str,
    webhook_id: str,
    timestamp: str,
    signature_header: str,
    raw_body: bytes,
    tolerance_seconds: int = 300,
) -> bool:
    if not secret.startswith("whsec_"):
        return False

    try:
        ts = int(timestamp)
        key = base64.b64decode(secret[len("whsec_"):], validate=True)
    except (ValueError, TypeError):
        return False

    if abs(int(time.time()) - ts) > tolerance_seconds:
        return False

    signed_content = (
        webhook_id.encode()
        + b"."
        + timestamp.encode()
        + b"."
        + raw_body
    )
    expected = base64.b64encode(
        hmac.new(key, signed_content, hashlib.sha256).digest()
    ).decode()

    signatures = [
        value.removeprefix("v1,")
        for value in signature_header.split()
        if value.startswith("v1,")
    ]
    return any(hmac.compare_digest(expected, value) for value in signatures)
Also verify that:
  1. Webhook-Timestamp differs from the current time by no more than five minutes.
  2. Webhook-Signature contains at least one valid v1 signature.
  3. A previously processed Webhook-ID does not repeat side-effecting business operations.

Retries and idempotency

Webhook delivery is at least once. The system may retry the same event after a network failure, timeout, or non-2xx response.
  • Deduplicate using Webhook-ID. Do not generate an idempotency key from the arrival time.
  • Return success for duplicate requests with the same Webhook-ID, and do not repeat side effects.
  • Return non-2xx on processing failure. Return 2xx after successful processing or when the event was already processed.