Why qhook?
qhook is a lightweight event gateway with Push and Pull delivery, built-in queue, retry, and workflow engine. It receives events (webhooks, SNS, API calls) and delivers them reliably — push to HTTP endpoints or pull from queues — from a single action to a multi-step pipeline with error routing and rollback. Single binary, no Redis, no Kubernetes.
The Problem
When an event arrives — a GitHub push, a Stripe payment, a monitoring alert, a tenant signup — you need to reliably call one or more HTTP endpoints in response.
| Current approach | Limitation |
|---|---|
| Custom scripts | No retry, no error routing, no monitoring |
| CI/CD tools (GitHub Actions, etc.) | Weak at orchestrating external HTTP services with retry and rollback |
| Kubernetes-native tools (Argo Events, etc.) | Requires Kubernetes |
| Managed workflow services (Step Functions, etc.) | Vendor lock-in, per-transition costs |
| Heavyweight orchestrators (Conductor, etc.) | JVM + DB + Elasticsearch, complex setup |
qhook fills the gap: lightweight, reliable event-to-action execution without infrastructure overhead.
Push and Pull: Two Delivery Modes, One Gateway
Most webhook infrastructure (Svix, Hookdeck, Convoy) only supports push-based delivery: events arrive and are immediately forwarded to your endpoint. qhook is the only webhook gateway that supports both Push and Pull delivery as first-class modes.
Push mode (handlers):
Stripe → qhook → ACK → POST to your server (with retry + DLQ)
Pull mode (queues):
Stripe → qhook → ACK → event stored in queue
↓
your app polls when ready → ack/nack
Use both in the same config — some events push to always-on services, others queue for batch workers.
Why Pull mode matters
Every competitor (Convoy, Hookdeck, Svix) is push-only. qhook is the only webhook gateway with built-in pull delivery.
| Push-only (competitors) | Push + Pull (qhook) | |
|---|---|---|
| Public endpoint required | Yes — your server must be internet-reachable | Push: yes. Pull: no — your app initiates the connection |
| Timeout pressure | Sender times out if you’re slow (Vercel 10s, Lambda 30s) | Push: managed by retry. Pull: none — process at your own pace |
| Backpressure | Sender controls the rate | Push: rate limiting. Pull: receiver controls the rate |
| Scaling | Must scale to handle spikes instantly | Push: scale to match. Pull: consume at your capacity |
| Firewall-friendly | Requires inbound rules | Push: inbound rules. Pull: outbound only |
| Batch processing | Awkward — must buffer externally | Pull mode with batch parameter, built-in |
Mental model: Push is great for always-on microservices. Pull is great for batch workers, cron jobs, firewalled environments, and anything that needs explicit backpressure control. With qhook, you choose per-source.
How It Works
Event source qhook Your services
Webhook (verified) ---->| |
SNS message ----------->| Route + execute |
API call (POST /events/{source}/{type}) ->| |
| Simple (handler): |
| event → POST /billing --->| one HTTP call
| (retry, DLQ, fan-out) |
| |
| Multi-step (workflow): |
| event → build → deploy -->| chained HTTP calls
| ↓ fail | with error routing
| rollback → alert -->|
Handlers execute a single HTTP action per event — with retry, DLQ, filtering, transformation, and fan-out to multiple services.
Workflows chain multiple HTTP actions — with branching, parallelism, wait, callback, and error routing (catch → rollback). Each step can send custom headers for authentication.
Both are defined in the same YAML config. Both share the same retry, DLQ, and monitoring infrastructure.
Use Cases
Simple: Webhook Fan-out
Receive Stripe webhooks, verify the signature, and deliver to multiple services with independent retry.
sources:
stripe:
type: webhook
verify: stripe
secret: ${STRIPE_WEBHOOK_SECRET}
handlers:
billing:
source: stripe
events: [invoice.paid]
url: http://billing:3000/webhook
idempotency_key: "$.id"
retry: { max: 8 }
analytics:
source: stripe
events: ["*"]
url: http://analytics:3000/ingest
Multi-step: Tenant Provisioning
API event triggers infra creation → DB setup → service configuration, with rollback on failure. Input parameters are validated, and each step can authenticate to external APIs.
sources:
platform:
type: event
workflows:
tenant-provision:
source: platform
events: [tenant.create]
timeout: 300
params:
- name: tenant_id
type: string
- name: region
type: string
- name: plan
type: string
required: false
steps:
- name: create-infra
url: https://api.terraform.io/v2/runs
headers:
Authorization: "Bearer ${TF_API_TOKEN}"
result_path: "$.infra"
retry: { max: 3, errors: [5xx, timeout] }
catch:
- errors: [all]
goto: notify-failure
- name: wait-infra
type: callback
url: https://api.terraform.io/v2/notification-configs
headers:
Authorization: "Bearer ${TF_API_TOKEN}"
callback_timeout: 600
- name: create-db
url: http://db-service:3000/tenant
result_path: "$.db"
catch:
- errors: [all]
goto: rollback-infra
- name: configure-services
url: http://integration:3000/setup
catch:
- errors: [all]
goto: rollback-db
- name: activate
url: http://platform:3000/tenant/activate
end: true
# --- rollback (reverse order) ---
- name: rollback-db
url: http://db-service:3000/tenant/delete
on_failure: continue
goto: rollback-infra
- name: rollback-infra
url: https://api.terraform.io/v2/runs
headers:
Authorization: "Bearer ${TF_API_TOKEN}"
on_failure: continue
goto: notify-failure
- name: notify-failure
url: http://slack:3000/notify
end: true
Multi-step: Deploy with Post-deploy Checks
GitHub push triggers build → deploy → smoke test → notify. Failure routes to rollback. Works as a post-deploy pipeline triggered by CI/CD or ArgoCD.
sources:
github:
type: webhook
verify: github
secret: ${GITHUB_WEBHOOK_SECRET}
workflows:
deploy:
source: github
events: [push]
timeout: 600
steps:
- name: build
url: http://ci:3000/build
retry: { max: 2, errors: [5xx, timeout] }
result_path: "$.build"
- name: deploy-staging
url: http://deployer:3000/deploy
input: |
{"env": "staging", "image": ""}
catch:
- errors: [all]
goto: rollback
- name: smoke-test
url: http://tester:3000/smoke
catch:
- errors: [all]
goto: rollback
- name: deploy-prod
url: http://deployer:3000/deploy
input: |
{"env": "production", "image": ""}
end: true
- name: rollback
url: http://deployer:3000/rollback
end: true
Multi-step: Alert Remediation
PagerDuty alert triggers severity-based action with verification. Signature-verified webhook, authenticated API calls for escalation.
sources:
pagerduty:
type: webhook
verify: pagerduty
secret: ${PAGERDUTY_WEBHOOK_SECRET}
workflows:
remediate:
source: pagerduty
events: [incident.triggered]
timeout: 300
steps:
- name: triage
type: choice
choices:
- condition: "$.severity == critical"
goto: restart
- condition: "$.severity == warning"
goto: scale-up
default: notify
- name: restart
url: http://orchestrator:3000/restart
goto: verify
- name: scale-up
url: http://orchestrator:3000/scale
goto: verify
- name: verify
type: wait
seconds: 30
goto: health-check
- name: health-check
url: http://orchestrator:3000/health
catch:
- errors: [all]
goto: escalate
end: true
- name: escalate
url: https://api.pagerduty.com/incidents
headers:
Authorization: "Token token=${PAGERDUTY_API_KEY}"
Content-Type: "application/json"
end: true
- name: notify
url: http://slack:3000/notify
end: true
Multi-step: Terraform Post-apply
Terraform Cloud run completion triggers application-layer configuration and verification.
sources:
terraform:
type: webhook
verify: terraform
secret: ${TF_NOTIFICATION_SECRET}
workflows:
post-apply:
source: terraform
events: [run:completed]
timeout: 300
steps:
- name: update-config
url: http://config-service:3000/apply
retry: { max: 3, errors: [5xx, timeout] }
catch:
- errors: [all]
goto: rollback-config
- name: health-check
url: http://health-service:3000/check
catch:
- errors: [all]
goto: rollback-config
- name: notify-success
url: http://slack:3000/notify
input: |
{"text": "Terraform apply succeeded, config updated."}
end: true
- name: rollback-config
url: http://config-service:3000/rollback
on_failure: continue
goto: notify-failure
- name: notify-failure
url: http://slack:3000/notify
input: |
{"text": "Terraform post-apply failed. Manual intervention required."}
end: true
Supported Webhook Providers
| Provider | Verification | Header |
|---|---|---|
| GitHub | HMAC-SHA256 | X-Hub-Signature-256 |
| Stripe | HMAC-SHA256 + timestamp | Stripe-Signature |
| Shopify | HMAC-SHA256 (base64) | X-Shopify-Hmac-SHA256 |
| Twilio | HMAC-SHA1 (base64) | X-Twilio-Signature |
| Paddle | HMAC-SHA256 + timestamp | Paddle-Signature |
| PagerDuty | HMAC-SHA256 | X-PagerDuty-Signature |
| Grafana | HMAC-SHA256 | X-Grafana-Alerting-Signature |
| Terraform Cloud | HMAC-SHA512 | X-TFE-Notification-Signature |
| GitLab | Token comparison | X-Gitlab-Token |
| Standard Webhooks | HMAC-SHA256 (base64) + timestamp | webhook-signature |
| Linear | HMAC-SHA256 | Linear-Signature |
| AWS SNS | X.509 certificate | (in body) |
| Custom HMAC | HMAC-SHA256 | X-Webhook-Signature |
How qhook Compares
vs Custom Scripts / Cron
| Scripts | qhook | |
|---|---|---|
| Retry on failure | DIY (if at all) | Built-in (exponential backoff) |
| Error routing | DIY | catch → rollback steps |
| Dead Letter Queue | None | Built-in |
| Monitoring | DIY | Prometheus metrics, alerts |
| Webhook verification | DIY | Built-in (13 providers) |
vs CI/CD Tools (GitHub Actions, etc.)
| CI/CD tools | qhook | |
|---|---|---|
| Trigger | Git events, schedules | Webhooks (verified), SNS, API |
| HTTP orchestration | Weak (shell scripts with curl) | Native (YAML-defined HTTP chains) |
| Retry with error routing | Limited | Built-in (catch → rollback) |
| Auth to external APIs | Per-step secrets | headers field with env var expansion |
| DLQ | None | Built-in |
vs Kubernetes-native Tools
| K8s tools (Argo Events, etc.) | qhook | |
|---|---|---|
| Requires Kubernetes | Yes | No |
| Webhook verification | Limited | Built-in (13 providers) |
| Setup | K8s cluster + CRDs | Single binary |
| Post-deploy hooks | K8s Jobs only | Any HTTP service |
vs Managed Workflow Services (Step Functions, etc.)
| Managed services | qhook | |
|---|---|---|
| Infrastructure | Vendor-specific | Any cloud or on-prem |
| Pricing | Per-transition / per-run | Free (self-hosted) |
| Webhook verification | Not included | Built-in |
| Local dev | Emulators (partial) | SQLite |
| Vendor lock-in | Yes | None |
vs Heavyweight Orchestrators (Conductor, etc.)
| Heavy orchestrators | qhook | |
|---|---|---|
| Runtime | JVM + DB + Elasticsearch | Single Rust binary |
| Setup time | 30+ minutes | 2 minutes |
| Definition | JSON DSL or Code SDK | YAML |
| Web UI | Yes | No (CLI only) |
| Operational burden | High | Low |
Cost Comparison
Self-hosting qhook replaces paid webhook and workflow services. Here’s what the numbers look like at typical scale.
Outbound Webhooks (vs Svix)
| Svix Starter | Svix Business | qhook (self-hosted) | |
|---|---|---|---|
| Monthly cost | $490/mo | $1,290/mo | ~$7–15/mo |
| Messages/mo | 500K | 2M | Unlimited |
| Endpoints | 1,000 | 10,000 | Unlimited |
| Retention | 90 days | 90 days | Configurable |
| Infrastructure | Managed | Managed | 1 VM (t3.small or equivalent) |
Inbound Webhooks (vs Hookdeck)
| Hookdeck Starter | Hookdeck Growth | qhook (self-hosted) | |
|---|---|---|---|
| Monthly cost | $0 (5K/mo) | $100+ | ~$7–15/mo |
| At 100K events/mo | $100/mo | $100/mo | ~$7–15/mo |
| At 1M events/mo | N/A | ~$500/mo | ~$15–30/mo |
| Verification | Limited | Full | 13 providers built-in |
| Infrastructure | Managed | Managed | 1 VM + Postgres |
Workflow Orchestration (vs AWS Step Functions)
| Step Functions Standard | Step Functions Express | qhook (self-hosted) | |
|---|---|---|---|
| Pricing model | $0.025/1K transitions | Duration-based | Fixed infra cost |
| 100K workflows/mo (5 steps) | $12.50/mo | ~$5/mo | ~$7–15/mo |
| 1M workflows/mo (5 steps) | $125/mo | ~$50/mo | ~$15–30/mo |
| 10M workflows/mo | $1,250/mo | ~$500/mo | ~$30–60/mo |
| Vendor lock-in | Yes (AWS) | Yes (AWS) | None |
Infrastructure Estimates
| Setup | Monthly cost | Handles |
|---|---|---|
| Minimal (t3.small, SQLite) | ~$7/mo | Up to 100K events/mo |
| Standard (t3.medium, RDS Postgres) | ~$30/mo | Up to 1M events/mo |
| Scale (2x t3.medium, RDS) | ~$60/mo | Up to 10M events/mo |
Break-even point: qhook pays for itself compared to Svix at ~10K messages/month, compared to Hookdeck at ~50K events/month, and compared to Step Functions at ~5M transitions/month.
These estimates assume on-demand AWS pricing. Reserved instances or spot reduce costs further. qhook’s single-binary design means no Redis, no message broker, no Elasticsearch — just compute + database.
Feature Comparison: qhook vs Svix vs Hookdeck vs Convoy
| Feature | qhook | Svix | Hookdeck | Convoy |
|---|---|---|---|---|
| Architecture | Push + Pull | Push-only | Push-only | Push-only |
| Direction | Inbound + Outbound | Outbound only | Inbound (+ Outpost) | Inbound + Outbound |
| Language | Rust | Rust/Python | Node.js/Go | Go |
| OSS | Apache 2.0 | MIT (core) | Partial (Outpost) | MPL-2.0 |
| Self-host | Single binary | Docker Compose | Docker (Outpost) | Docker Compose |
| Webhook verification | 13 providers | N/A (sending side) | Limited | Limited |
| Workflow engine | Built-in (YAML) | No | No | No |
| Retry + DLQ | Built-in | Built-in | Built-in | Built-in |
| Self-host cost | Free (single binary) | Free (Docker Compose) | Free (Docker) | Free (Docker Compose) |
Key takeaways
- Pull mode is unique: Every competitor is push-only. qhook is the only gateway with built-in pull delivery — consumers poll when ready, no public endpoint needed, no timeout pressure. Push and Pull work side-by-side in the same config.
- Workflow engine included: qhook is the only option with a built-in multi-step workflow engine (branching, parallelism, rollback) — no need for a separate orchestrator.
- Simplest self-hosting: qhook is a single binary with no external dependencies (no Redis, no message broker). Competitors require Docker Compose with multiple services.
Where qhook Fits
qhook is purpose-built for one pattern: event arrives → execute HTTP actions reliably. Whether the event is an external webhook (Stripe, GitHub) or an internal API call (your app, IDP, CI/CD), qhook handles both through the same engine.
It is not a replacement for:
- Kubernetes-native tools if you already run K8s
- Managed workflow services at massive scale
- Heavyweight orchestrators with Web UI and plugin ecosystems
It is the right choice when:
- You need to turn events into reliable HTTP actions (one or many)
- You want retry, error routing, and DLQ without writing infrastructure code
- You need to call authenticated external APIs as part of a pipeline
- You don’t run Kubernetes and don’t want vendor lock-in
- You want a single binary with zero dependencies
Summary
| Capability | qhook |
|---|---|
| Deployment | Single binary, zero external deps |
| Database | SQLite (dev) / Postgres or MySQL (prod) |
| Input | Webhooks (13 providers verified), SNS, internal API (POST /events/{source}/{type}) |
| Outbound Webhooks | Standard Webhooks compliant, per-endpoint signing secrets, subscription-based routing |
| Delivery | Push (HTTP handlers) and Pull (queue polling with ack/nack) |
| Actions | HTTP with custom headers for authentication |
| Management API | Track events, jobs, and workflow runs programmatically |
| Reliability | Retry (exponential backoff), error routing, DLQ |
| Workflows | Sequential, choice, parallel, map, wait, callback |
| Input validation | Workflow params with type checking |
| Monitoring | Prometheus metrics, health checks, Slack/Discord alerts |
| Config | Single YAML file, env var expansion, validation CLI |