Skip to content

Webhooks

OpenPR uses webhooks to notify external systems of state changes in real time. Every webhook delivery is signed with HMAC-SHA256, recorded in the webhook_deliveries table for auditability, and includes rich contextual data for downstream automation.

  1. A state change occurs (issue created, sprint started, proposal submitted, etc.)
  2. OpenPR queries the webhooks table for active webhooks in the workspace that subscribe to that event type
  3. For each matching webhook, a payload is constructed with full entity data
  4. The payload is signed with the webhook’s secret key using HMAC-SHA256
  5. An HTTP POST is sent to the webhook URL with signature headers
  6. The delivery result (status, body, duration) is recorded in webhook_deliveries

OpenPR fires 30 event types organized into seven categories.

EventFired When
issue.createdA new issue is created
issue.updatedIssue fields are modified (includes changes diff)
issue.assignedIssue assignee changes (includes old/new assignee IDs)
issue.state_changedIssue state transitions (includes old/new state)
issue.deletedAn issue is deleted
EventFired When
comment.createdA comment is added to an issue (includes mentions array)
comment.updatedA comment is edited
comment.deletedA comment is deleted
EventFired When
label.addedA label is attached to an issue
label.removedA label is removed from an issue
EventFired When
sprint.startedA sprint status changes to active
sprint.completedA sprint is marked as completed
EventFired When
project.createdA new project is created
project.updatedProject fields are modified
project.deletedA project is deleted
member.addedA user is added to the workspace
member.removedA user is removed from the workspace
EventFired When
proposal.createdA new proposal is drafted
proposal.updatedProposal fields are modified
proposal.deletedA proposal is deleted
proposal.submittedA proposal is submitted for review
proposal.voting_startedVoting opens on a proposal
proposal.archivedA proposal is archived
proposal.vote_castA vote is cast on a proposal
veto.exercisedA vetoer exercises their veto right
veto.withdrawnA veto is withdrawn
EventFired When
escalation.startedAn escalation process begins
appeal.createdAn appeal is filed against a decision
EventFired When
governance_config.updatedGovernance configuration is changed
EventFired When
ai.task_completedAn AI task finishes successfully
ai.task_failedAn AI task fails after exhausting retries

Webhooks are configured per workspace via the API. Each webhook specifies:

FieldTypeDescription
urlstringHTTPS endpoint to receive events
secretstringShared secret for HMAC-SHA256 signing
eventsJSONBArray of event types to subscribe to
activebooleanEnable or disable the webhook
bot_user_idUUID (optional)If set, enables bot context enrichment for AI task dispatch

When bot_user_id is set and the event involves an issue assigned to that bot, the payload includes a bot_context object with agent dispatch information.

Every webhook delivery includes these HTTP headers:

HeaderValue
Content-Typeapplication/json
User-AgentOpenPR-Webhook/1.0
X-Webhook-Signaturesha256=<hex-encoded HMAC>
X-Webhook-EventEvent type (e.g., issue.created)
X-Webhook-DeliveryUnique delivery UUID
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"event": "issue.created",
"timestamp": "2026-03-18T10:30:00.000Z",
"workspace": {
"id": "workspace-uuid",
"name": "My Workspace"
},
"project": {
"id": "project-uuid",
"name": "Backend API",
"key": "IM01"
},
"actor": {
"id": "user-uuid",
"name": "Admin",
"email": "admin@example.com",
"entity_type": "human"
},
"data": {
"issue": {
"id": "issue-uuid",
"key": "IM01-A1B2C3D4",
"title": "Fix authentication flow",
"description": "The login endpoint returns 500...",
"state": "todo",
"priority": "high",
"assignee_ids": ["bot-uuid"],
"label_ids": ["label-uuid"],
"sprint_id": "sprint-uuid",
"created_at": "2026-03-18T10:30:00.000Z",
"updated_at": "2026-03-18T10:30:00.000Z"
}
},
"bot_context": {
"is_bot_task": true,
"bot_id": "bot-uuid",
"bot_name": "Claude Agent",
"bot_agent_type": "claude-code",
"trigger_reason": "created",
"webhook_id": "webhook-uuid"
}
}

The bot_context field is present only when:

  1. The webhook has a bot_user_id configured, AND
  2. The issue is assigned to that bot user, OR
  3. The bot is @mentioned in a comment

The trigger_reason field indicates why the bot was triggered:

ReasonWhen
createdIssue was created with bot as assignee
assignedIssue was assigned or updated with bot as assignee
status_changedIssue state transitioned
mentionedBot was @mentioned in a comment
completedAI task completed
failedAI task failed

For issue.updated, issue.assigned, and issue.state_changed events, the data object includes a changes field showing what changed:

{
"data": {
"issue": { "..." : "..." },
"changes": {
"state": {
"old": "todo",
"new": "in_progress"
}
}
}
}

For comment.created events, the data includes a mentions array of user UUIDs:

{
"data": {
"comment": { "..." : "..." },
"issue": { "..." : "..." },
"mentions": ["user-uuid-1", "user-uuid-2"]
}
}

To verify a webhook delivery, compute the HMAC-SHA256 of the raw request body using your webhook secret and compare it to the signature in the X-Webhook-Signature header.

import hmac
import hashlib
def verify_webhook(secret: str, body: bytes, signature_header: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
const crypto = require("crypto");
function verifyWebhook(secret, body, signatureHeader) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(body).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}

Every webhook delivery is persisted in the webhook_deliveries table with:

  • Delivery UUID
  • Webhook ID
  • Event type
  • Full payload (JSONB)
  • Request headers
  • Response status code
  • Response body
  • Error message (if delivery failed)
  • Duration in milliseconds
  • Success flag
  • Timestamp

The webhook’s last_triggered_at timestamp is updated after each delivery attempt.

  • Timeout: 10 seconds per delivery attempt
  • No automatic retries: Failed deliveries are recorded but not retried (the AI task system has its own retry mechanism)
  • Async dispatch: Webhooks are triggered in a background Tokio task and do not block the API response
  • Best-effort: If payload construction fails, the error is recorded and the webhook is skipped