{"id":34044774,"url":"https://github.com/opsani/servox-webhooks","last_synced_at":"2026-03-11T12:13:19.898Z","repository":{"id":38229674,"uuid":"289587871","full_name":"opsani/servox-webhooks","owner":"opsani","description":"A flexible webhooks connector for Opsani Servo assemblies","archived":false,"fork":false,"pushed_at":"2023-08-14T19:48:04.000Z","size":176,"stargazers_count":0,"open_issues_count":10,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-12-15T16:12:25.636Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/opsani.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2020-08-23T00:17:07.000Z","updated_at":"2021-10-26T09:03:49.000Z","dependencies_parsed_at":"2023-07-12T21:02:36.295Z","dependency_job_id":null,"html_url":"https://github.com/opsani/servox-webhooks","commit_stats":null,"previous_names":["opsani/servo-webhooks"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/opsani/servox-webhooks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opsani%2Fservox-webhooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opsani%2Fservox-webhooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opsani%2Fservox-webhooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opsani%2Fservox-webhooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/opsani","download_url":"https://codeload.github.com/opsani/servox-webhooks/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opsani%2Fservox-webhooks/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30380935,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-11T06:09:32.197Z","status":"ssl_error","status_checked_at":"2026-03-11T06:09:17.086Z","response_time":84,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-13T23:02:25.205Z","updated_at":"2026-03-11T12:13:19.886Z","avatar_url":"https://github.com/opsani.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# servo-webhooks\n\n![Run\nTests](https://github.com/opsani/servo-webhooks/workflows/Run%20Tests/badge.svg)\n[![license](https://img.shields.io/github/license/opsani/servo-webhooks.svg)](https://github.com/opsani/servo-webhooks/blob/master/LICENSE)\n[![PyPI](https://img.shields.io/pypi/v/servo-webhooks.svg)](https://pypi.org/project/servo-webhooks/)\n[![release](https://img.shields.io/github/release/opsani/servo-webhooks.svg)](https://github.com/opsani/servo-webhooks/releases/latest)\n[![GitHub release\ndate](https://img.shields.io/github/release-date/opsani/servo-webhooks.svg)](https://github.com/opsani/servo-webhooks/releases)\n\nAn Opsani [Servo](https://github.com/opsani/servox) connector that provides a\nflexible webhooks emitter based on [servo\nevents](https://github.com/opsani/servox/#understanding-events).\n\nThe webhooks connector extends the eventing infrastruture provided by the servo\nto enable events to be dispatched via HTTP or HTTP/2 request callbacks. Requests\nare delivered asynchronously on a best effort basis. Webhooks can be registered\nto execute *before* or *after* any event defined in the servo assembly. Before\nevent webhooks should be used with care as they can block execution of the event\npending delivery of the webhook or cancel the event entirely through the\nresponse (see below). Support is provided for configurable automatic retry and\ntimeout of webhook requests.\n\nWebhook requests are sent with the HTTP `POST` method and a JSON request body.\nThe webhook request body is dynamically defined based on the parameters and\nreturn value of the event registered with the servo. This mechanism generalizes\nthe webhook connector to support arbitrary events defined by any connector\nwithin the servo assembly. The `Content-Type` header and request body JSON\nSchema can be obtained via the `webhooks` CLI subcommand (see usage below).\n\n## Configuration\n\n```yaml\nwebhooks:\n  - name: my_measure_handler # Optional. Name of the webhook.\n    description: Store measurement info into Elastic Search. # Optional: Textual description of the webhook\n    events:\n    - after:measure # Required. Format: `(before|after):[EVENT_NAME]`\n    url: https://example.com/webhooks # Required. Format: [URL]\n    secret: s3cr3t # Required. Secret value for computing webhook signatures\n    headers: # Optional, Dict[str, str]\n      - name: x-some-header\n        value: some value\n    backoff: # Optional. Setting to `false` disables retries.\n      max_tries: 3\n      max_time: 5m\n```\n\nA starting point configuration can be added to your servo assembly via: `servo\ngenerate --defaults webhooks`.\n\n## Example Webhook Requests\n\n# TODO: Insert headers and request body for a couple of events\n```console\n```\n\n## Installation\n\nservo-webhooks is distributed as an installable Python package via PyPi and can\nbe added to a servo assembly via Poetry:\n\n```console\n❯ poetry add servo-webhooks\n```\n\nFor convenience, servo-webhooks is included in the default servox assembly\n[Docker images](https://hub.docker.com/repository/docker/opsani/servox).\n\n## Usage\n\n1. Listing webhooks: `servo webhooks list`\n1. Getting event content type and payload schema: `servo webhooks schema\n   after:measure`\n1. Triggering an ad-hoc webhook: `servo webhooks trigger after:adjust\n   ([NAME|URL])`\n\n### Implementing Webhook Responders\n\nTODO: Content type, etc. headers. Include connector version, other event\nmetadata. Schema versioning.\n\n### Validating Webhook Signatures\n\nAll webhook requests are sent with a `X-Servo-Signature` header. This value of\nthis header is a hex string representation of an HMAC SHA1 digest computed over\nthe body of the request using the value of the `secret` key from the webhook\nconfiguration. The signature can be easily verified to validate the authenticity\nand integrity of the webhook payload. HMAC computation is supported on all major\nplatforms and in the standard library of most modern programming languages.\n\nAn example of computing an HMAC SHA1 digest from a webhook request in Python\nlooks like this:\n\n```python\nsecret = \"super secret authentication code\"\nexpected_signature = request.headers[\"x-servo-signature\"]\nbody = request.read()\nsignature = str(hmac.new(secret.encode(), body, hashlib.sha1).hexdigest())\nassert signature == expected_signature\n```\n\n\n### Cancelling an Event via a Webhook\n\nLet's say that you want to implement a webhook that implements authorization of\nadjustments based on criteria such as a schedule that only permits them during\nmidnight and 3am. To implement this, the webhook responder will return a 200\n(OK) status code and a response body modeling a `servo.errors.CancelEventError`\nobject. The `servo-webhooks` connector will deserialize the `CancelEventError`\nrepresentation and raise a `CancelEventError` exception within the assembly,\ncancelling the event. To indicate that your response body is a representation of\na `CancelEventError` error, set the `Content-Type` header to\n`application/vnd.opsani.servo.errors.CancelEventError+json` and return a JSON\nobject that includes a `reason` property describing why the event was cancelled:\nTODO: What's the best status code/response for cancellation? Return a 200 (OK)\nresponse with `Content-Type` of :\n\n```\n\u003e POST http://webhooks.example.com/servo-webhooks\n\u003e Content-Type: application/vnd.opsani.servo.events.Event+json # TODO: Not the right content type\n\u003e {\n\u003e  ...\n\u003e }\n\n\u003c 200 (OK)\n\u003c Content-Type: application/vnd.opsani.servo.errors.CancelEventError+json\n\u003c {\n\u003c  \"reason\": \"Unable to authorize adjustment: Adjustments are only permitted between midnight and 3am.\"\n\u003c }\n```\n\n### Configuring Backoff Retries \u0026 Timeouts\n\nTODO: Disabling backoff to avoid blocking on a before handler.\n\n## Technical Details\n\nWebhook requests are managed non-persistently in memory. Requests are made via\nan asynchronous [httpx](https://www.python-httpx.org/) client built on top of\n[asyncio](https://asyncio.readthedocs.io/). Support for webhook request body\nJSON Schema is provided via the deep integration of\n[Pydantic](https://pydantic-docs.helpmanual.io/) in servox. Backoff and retry\nsupported is provided via the [backoff](https://pypi.org/project/backoff/)\nlibrary.\n\n## Testing\n\nAutomated tests are implemented via\n[Pytest](https://docs.pytest.org/en/stable/): `pytest .`\n\n## License\n\nservo-webhooks is distributed under the terms of the Apache 2.0 Open Source\nlicense.\n\nA copy of the license is provided in the [LICENSE](LICENSE) file at the root of\nthe repository.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopsani%2Fservox-webhooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopsani%2Fservox-webhooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopsani%2Fservox-webhooks/lists"}