Skip to main content

Shellui Actions (hosting webhooks)

Company owners (and staff) can react when hosting events happen using webhook Shellui Action rules in the Shellui admin API. Each rule maps a catalog event type (for example hosting.deployment.succeeded) to an HTTPS webhook endpoint.

Hosting-service delivers webhooks directly on its domain events. There is no central actions service and no message bus.


How it works​

Domain code calls emit_event(type, company_id, payload)
│
▼
Match enabled webhook ActionRule rows for that company + event type
│
▼
Insert ActionOutbox row(s) in the same DB transaction
│
▼
transaction.on_commit → best-effort delivery (timeout-bounded HTTP, off the request thread)
│
▼
DeliveryAttempt audit log; retries via manage.py retry_webhooks
  • No Celery / Redis required for Shellui Actions.
  • API paths do not block on slow external HTTP: delivery runs only after commit (default 5s timeout).
  • Delivery is at-least-once; dedupe on the envelope id (same value as the webhook-id header).
  • n8n: step-by-step setup, signature verification, and retry table in n8n.md.

Event catalog (hosting.*)​

Event typeWhen it fires
hosting.app.createdA hosted app record is created
hosting.app.deletedA hosted app and its deployments are removed
hosting.deployment.createdA deployment row is created (draft, ready for upload)
hosting.deployment.succeededFinalize completed; deployment is active
hosting.deployment.failedArtifact extract failed during finalize

Envelope shape​

CloudEvents-inspired JSON (same headers and signing as identity-service Shellui Actions). The POST body uses UTF-8 JSON with ensure_ascii=false; verify signatures on the raw body bytes.

Extra headers: X-Shellui-Event, X-Shellui-Delivery-Attempt.

Signing secrets may be plain text or Standard Webhooks whsec_<base64> (Shellui decodes the suffix for HMAC).

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "hosting.deployment.succeeded",
"time": "2026-09-24T13:30:00+00:00",
"company": { "id": 1 },
"data": {
"app_id": "550e8400-e29b-41d4-a716-446655440000",
"deployment_id": "660e8400-e29b-41d4-a716-446655440001",
"status": "active"
}
}

Admin REST API​

Same paths as identity-service (Bearer JWT from identity-service):

  • GET /api/v1/actions/events — catalog with sample_envelope
  • GET/POST /api/v1/actions/rules — list/create webhook rules (create returns secret once if generated)
  • GET/PATCH/DELETE /api/v1/actions/rules/<id>
  • POST /api/v1/actions/rules/<id>/rotate-secret — new signing secret (returned once)
  • POST /api/v1/actions/rules/<id>/send-test
  • GET /api/v1/actions/deliveries — paginated delivery log
  • GET /api/v1/actions/deliveries/<uuid> — detail with attempts
  • POST /api/v1/actions/deliveries/<uuid>/requeue

Staff may pass ?company_id= on these routes. Company owners use the company_id claim in the JWT only.


Retries (cron)​

Backoff: 30s * 2^(n-1) capped at 1 hour, max 8 attempts. Default HTTP timeout per attempt: 5 seconds (ACTIONS_WEBHOOK_TIMEOUT_SECONDS).

ResultRetry?
2xxNo (delivered)
404, 408, 409, 425, 429Yes (404 covers inactive n8n workflows)
400, 401, 403, 405, 410, 413, 422No (dead)
Other 4xxYes
5xx, timeouts, connection errorsYes
429 / 503 with Retry-AfterYes; delay is max(backoff, Retry-After) capped at 1 hour
python manage.py retry_webhooks --batch-size 50 --max-seconds 50 --concurrency 4

Example cron (every minute):

* * * * * cd /app && python manage.py retry_webhooks >> /var/log/retry_webhooks.log 2>&1

Run the same command in Docker sidecars or platform schedulers that can exec into the hosting-service container.