Webhooks & Callbacks

How CodeCourier handles incoming webhooks from Clerk (user lifecycle), Svix signature verification, and the internal Trigger.dev callback endpoint with a complete operation reference organized by domain.

14 min read
webhooksclerksvix

Webhooks allow external services to notify CodeCourier of events in real time. CodeCourier processes webhooks from Clerk (for user lifecycle events) and exposes an internal callback endpoint for Trigger.dev (for background job progress reporting and data operations). This page documents both webhook surfaces, their authentication models, payload formats, signature verification, and the complete Trigger.dev callback operation reference organized by domain.

Clerk Webhooks

CodeCourier registers a webhook endpoint at /clerk/webhook to receive user lifecycle events from Clerk. The primary use case is handling user deletion -- when a user deletes their Clerk account, CodeCourier receives a webhook and can clean up associated data.

Supported Events

  • user.deleted -- Fired when a user account is deleted in Clerk. CodeCourier uses this to trigger data cleanup workflows, including removing or anonymizing user-owned resources.

Payload Format

Clerk webhook payloads follow the Svix standard format. Each payload contains:

  • type -- The event type string (e.g., "user.deleted").
  • data -- The event payload containing the affected resource. For user events, this includes id (the Clerk user ID), email_addresses, and other user fields.
  • object -- Always "event".

Signature Verification

All incoming Clerk webhooks are verified using the Svix signature protocol. This ensures that webhook payloads genuinely originate from Clerk and have not been tampered with in transit.

Verification Process

  1. Extract headers. The server reads three required headers from the incoming request: svix-id (unique message identifier), svix-timestamp (Unix timestamp in seconds), and svix-signature (one or more versioned signatures).
  2. Validate timestamp. The server checks that the timestamp is within five minutes of the current time. Requests outside this tolerance window are rejected to prevent replay attacks.
  3. Compute expected signature. The signed content is constructed as {svix-id}.{svix-timestamp}.{raw-body}. The server computes an HMAC-SHA256 of this content using the webhook secret (the CLERK_WEBHOOK_SECRET environment variable, with the whsec_ prefix stripped and the remainder base64-decoded).
  4. Compare signatures. The svix-signature header may contain multiple signatures separated by spaces, each prefixed with a version (e.g., v1,<base64>). The server checks each v1 signature against the computed value using a timing-safe character-by-character comparison to prevent timing side-channel attacks.
  5. Accept or reject. If any signature matches, the webhook is accepted and processed. Otherwise, it is rejected with a 401 status.

Environment Variables

  • CLERK_WEBHOOK_SECRET -- The Svix signing secret from your Clerk dashboard. This is a base64-encoded HMAC key prefixed with whsec_. The server will refuse to process any webhooks if this variable is not set.

Trigger.dev Callback Endpoint

The /trigger/callback endpoint is an internal API used exclusively by CodeCourier's Trigger.dev background jobs. It is not intended for direct external use -- it is documented here so developers can understand the data flow between the orchestration layer and the Convex database.

Trigger.dev tasks call this endpoint to write data back to Convex (run status updates, sandbox messages, learning extraction, usage recording, etc.) without requiring direct Convex client access from within the Trigger.dev runtime.

Authentication

The callback endpoint uses bearer token authentication. The token is set in the TRIGGER_CALLBACK_SECRET environment variable and must be included as Authorization: Bearer <token> in every request. The server performs timing-safe comparison using XOR byte matching.

POST /trigger/callback
Content-Type: application/json
Authorization: Bearer {callback_secret}

{
  "operation": "domain.action",
  "args": { ... }
}

Warning

The /trigger/callback endpoint uses a separate secret from the project API key (cc_live_* keys). It is protected by the TRIGGER_CALLBACK_SECRET environment variable and is not accessible with user API keys. Attempting to call it with a cc_live_* key will result in a 401 Unauthorized error.

Request Format

Every callback request has the same envelope structure:

json
{
  "operation": "<domain>.<action>",
  "args": {
    // operation-specific arguments
  }
}

Response Format

Successful operations return HTTP 200 with { "result": <value> }. Failed operations return the appropriate HTTP status code (400, 401, or 500) with { "error": "message" }.

Callback Operations Reference

Operations are organized by domain. Each entry shows the operation name and a description of what it does and what args it expects.

run.* - Workflow Run Operations

  • run.get -- Fetch a run record by ID. Args: { runId }
  • run.create -- Create a new run record in the database (called at orchestration start). Args: full run creation payload including workflowId, prompt, status, and optional fields.
  • run.updateStatus -- Update the status of an existing run (e.g., runningcompleted). Args: { runId, status, completedAt? }
  • run.updatePr -- Record the PR URL and status for a run. Args: { runId, prUrl, prStatus, prNumber? }
  • run.createChainRun -- Create a run as part of a sprint chain, linking it to the chain record. Args: { chainId, sprintIndex, ... }
  • run.updateProgress -- Write incremental progress information to a run (e.g., current step description, iteration count). Args: { runId, progress }
  • run.setStopFlag -- Set the stopAfterCurrentTurn flag on a run to gracefully halt after the current agent turn completes. Args: { runId, stop: true }

runStep.* - Run Step Operations

  • runStep.create -- Create a new step record under a run. Args: { runId, role, name, stepIndex }. Valid roles: designer, checker, researcher, evaluator, judge, answerer.
  • runStep.updateStatus-- Update a step's status and optionally write quality scores or test results. Args: { stepId, status, qualityScores?, testResults? }

sandbox.* - Sandbox Operations

  • sandbox.get -- Fetch a sandbox record by ID. Args: { sandboxId }
  • sandbox.create -- Register a newly provisioned E2B sandbox in the database. Args: full sandbox creation payload including runId, e2bSandboxId.
  • sandbox.updateStatus -- Update sandbox lifecycle status (e.g., runningkilled). Args: { sandboxId, status }
  • sandbox.setTriggerRunId -- Associate a Trigger.dev task run ID with a sandbox for tracing. Args: { sandboxId, triggerRunId }
  • sandbox.updateLearningStatus -- Update the status of learning extraction for a sandbox. Args: { sandboxId, learningStatus }
  • sandbox.updatePr -- Write PR metadata (URL, number, status) onto the sandbox record. Args: { sandboxId, prUrl, prNumber, prStatus }
  • sandbox.hasAssistantMessages -- Check whether a sandbox has any stored assistant messages (used to determine whether to emit a notification). Args: { sandboxId }. Returns { result: boolean }.

message.* - Sandbox Message Operations

  • message.store -- Persist a single completed message to the sandbox message log. Args: { sandboxId, role, content, timestamp }
  • message.streamCreate -- Initialize a streaming message record for a new assistant response. Args: { sandboxId, messageId }
  • message.streamAppend -- Append a text chunk to an in-progress streaming message. Args: { messageId, chunk }
  • message.streamFinalize -- Mark a streaming message as complete and write the final accumulated content. Args: { messageId, finalContent }
  • message.listBySandbox -- Retrieve all messages for a sandbox, ordered by timestamp. Args: { sandboxId }

issue.* - Issue Operations

  • issue.getByRun -- Fetch the issue linked to a run (if any). Used by the orchestrator to inject issue context into the agent prompt. Args: { runId }

issueSession.* - Issue Session Operations

  • issueSession.get -- Fetch an issue session record. Args: { sessionId }
  • issueSession.createSandbox -- Create and link a sandbox to an issue session. Args: { sessionId, sandboxPayload }
  • issueSession.updateStatus-- Update an issue session's lifecycle status. Args: { sessionId, status }
  • issueSession.createIssuesFromJson -- Bulk-create issues from a JSON array discovered by the scanning agent. Args: { sessionId, issues: Issue[] }
  • issueSession.createSessionQuestionsFromJson -- Bulk-create session questions for an answering session from a JSON array produced by the question-generation agent. Args: { answeringSessionId, questions: Question[] }
  • issueSession.updateIteration -- Increment the iteration counter on an issue session (used for multi-turn scanning). Args: { sessionId }
  • issueSession.updateProgress -- Write incremental progress text to an issue session. Args: { sessionId, progress }

issueSessionStep.* - Issue Session Step Operations

  • issueSessionStep.create -- Create a step record under an issue session. Args: { sessionId, role, name, stepIndex }
  • issueSessionStep.updateStatus-- Update a session step's status. Args: { stepId, status }

answeringSession.* - Answering Session Operations

  • answeringSession.get -- Fetch an answering session and its questions. Args: { answeringSessionId }
  • answeringSession.createSandbox -- Create and link a sandbox to an answering session for the answerer agent. Args: { answeringSessionId, sandboxPayload }
  • answeringSession.updateStatus -- Update the status of an answering session. Args: { answeringSessionId, status }

sessionQuestions.* - Session Question Operations

  • sessionQuestions.updateAssumptions -- Bulk-update the AI-generated assumptions for a set of session questions (called after the answerer agent produces its initial responses). Args: { updates: Array<{ questionId, assumption }> }
  • sessionQuestions.updateAssumptionsByIssueSession -- Update assumptions for all questions linked to a specific issue session (used when assumptions are derived from session-level context rather than individual questions). Args: { issueSessionId, assumptions: Record<string, string> }

learning.* - Learning Operations

  • learning.dispatchExtraction -- Schedule a learning extraction job for a completed sandbox. The extraction job analyzes the sandbox conversation and distills learnings. Args: { sandboxId, runId }
  • learning.store -- Persist a single learning record extracted from a sandbox. Args: learning payload with sandboxId, content, category, and optional metadata.
  • learning.bulkStore -- Persist multiple learning records in a single operation. Args: { learnings: Learning[] }
  • learning.getCompiled -- Retrieve the compiled (merged and deduplicated) learning content for a project and role. Used by the orchestrator to inject accumulated learnings into agent system prompts. Args: { projectId, role }

usage.* - Usage Recording Operations

  • usage.computeCostAndRecord -- Calculate the cost for a completed sandbox run (based on token counts, compute time, and service rates) and write a usage record. Args: { sandboxId, tokenUsage, durationMs, service }

keys.* - API Key Operations

  • keys.get -- Retrieve a specific provider API key for a project (e.g., Anthropic API key). Used by orchestrators to obtain the configured keys for a project. Args: { projectId, keyType }
  • keys.getWithFallback -- Retrieve a provider API key, falling back to the platform default if the project has not configured its own. Args: { projectId, keyType }

settings.* - Project Settings Operations

  • settings.get -- Retrieve project settings (system prompt overrides, env vars, git config, feature flags). Args: { projectId }

sprintChain.* - Sprint Chain Operations

  • sprintChain.get -- Fetch a sprint chain record. Args: { chainId }
  • sprintChain.updateStatus -- Update the status of a sprint chain (e.g., running completed). Args: { chainId, status }
  • sprintChain.updatePr-- Append a new sprint PR URL to the chain's sprintPrUrls array and update the current sprint index. Args: { chainId, prUrl, sprintIndex }

workflow.* - Workflow Operations

  • workflow.get -- Fetch a workflow blueprint record including step configuration and persona assignments. Args: { workflowId }

persona.* - Persona Operations

  • persona.get -- Fetch a persona record (model selection, system prompt, temperature, and other agent configuration). Args: { personaId }

contexts.* - Context Operations

  • contexts.getByIdInternal -- Fetch a context record by ID for use within the Trigger.dev runtime. Returns full context metadata without public API key authentication. Args: { contextId }

contextVersions.* - Context Version Operations

  • contextVersions.getActiveInternal -- Retrieve the active version content for a context. This is the primary operation used by the orchestrator to inject context content into agent prompts at run time. Args: { contextId }. Returns { version, content, publishedAt }.
  • contextVersions.ensureActiveInternal -- Retrieve the active version, creating a blank version 1 if none exists. Used for bootstrapping new contexts. Args: { contextId }

learningVersions.* - Learning Version Operations

  • learningVersions.getActiveForRole -- Fetch the active compiled learning version for a specific agent role within a project. Learnings are compiled per role so that designer agents receive designer-specific learnings and checker agents receive checker-specific learnings. Args: { projectId, role }. Returns the compiled learning content or null if no version has been compiled.

Retry Policies

Clerk Webhook Retries

Clerk (via Svix) automatically retries failed webhook deliveries using an exponential backoff schedule. If CodeCourier returns a non-2xx response, Svix will retry the delivery over the following hours and days. The five-minute timestamp tolerance window applies to the original timestamp, not the retry time, so retried webhooks may be rejected if they arrive too late.

To ensure reliable processing:

  • Return a 200 response as quickly as possible, even if the actual processing needs to happen asynchronously.
  • If processing fails after accepting the webhook, use Convex's ctx.scheduler.runAfter to retry internally rather than relying on Svix retries.

Trigger.dev Callback Retries

Trigger.dev tasks implement their own retry logic. If a callback to CodeCourier fails (network error or 5xx response), the Trigger.dev task will catch the error and may retry the individual callback operation. The double try-catch architecture in the callback handler ensures that a JSON response is always returned, preventing TCP resets that could cause infinite retries.

Notification Events

CodeCourier also generates internal notification events stored in the notifications table. These are not external webhooks but serve a similar purpose for the dashboard UI. The following notification types are generated:

  • run_completed -- A workflow run finished successfully.
  • run_failed -- A workflow run encountered an error.
  • pr_created -- A pull request was created from a sandbox or run.
  • pr_merged -- A pull request was merged.
  • pr_failed -- Pull request creation failed.
  • member_joined -- A new member accepted a project invitation.
  • workflow_completed -- A workflow execution finished.
  • sprint_completed / sprint_failed -- A sprint chain completed or failed.

Notifications are scoped to a project and user, and include a read flag and optional dismissedAt timestamp for tracking which notifications the user has seen. See the Operations API for endpoints to list, mark read, and dismiss notifications.

Setting Up Webhooks

Clerk Webhook Configuration

  1. Navigate to the Clerk dashboard for your application.
  2. Go to Webhooks in the left sidebar.
  3. Click Add Endpoint.
  4. Enter your CodeCourier Convex deployment URL followed by /clerk/webhook (e.g., https://your-deployment.convex.site/clerk/webhook).
  5. Select the events you want to subscribe to (at minimum, user.deleted).
  6. Copy the signing secret and set it as the CLERK_WEBHOOK_SECRET environment variable in your Convex deployment.

Testing Webhooks

Use the Clerk dashboard's "Send test event" feature to verify your endpoint is working correctly. Check the Convex function logs for any errors in signature verification or event processing.