# Create webhook — POST /v1/provider/github/webhook

URL: https://docs.hanzo.ai/docs/openapi/provider/post-provider-github-webhook

> The address the GitHub App delivers events to.

`POST /v1/provider/github/webhook`

> [Provider →](https://docs.hanzo.ai/docs/openapi/provider) · [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|               |                                                   |
| ------------- | ------------------------------------------------- |
| **Address**   | `https://api.hanzo.ai/v1/provider/github/webhook` |
| **Method**    | `POST`                                            |
| **Operation** | `post_provider_github_webhook`                    |
| **Auth**      | `Authorization: Bearer $HANZO_API_KEY`            |

The address the GitHub App delivers events to. A push is handed to the repository sync engine, and an issue or issue-comment event is mirrored into the native todo — idempotently, so the same issue re-syncs to one row however many times it is edited, closed or reopened. A repository event that puts a repo INTO the granted set (created, transferred, unarchived) raises one todo offering to import it; accepting means POSTing that repo to /v1/provider/github/import. It never imports on its own.

It answers a benign 200 for everything it does not act on — the ping, other event types, an unknown installation — deliberately, so GitHub does not enter a retry storm over events that were never going to do anything. Only a bad signature and a genuine sync failure are non-200, and an oversized payload is refused outright.

Two sync rules are worth stating because neither is guessable. EVERY ref syncs, tags as well as branches, because releases are cut by tag and filtering them would stop publishing with nothing reporting a failure. And a delete is NEVER propagated: the native side is canonical, so an inbound delete never removes a native ref.

The payload is verified by HMAC against the webhook secret before it is parsed.

The caller here is the PLATFORM, not a Hanzo tenant, so there is no bearer and no principal. The signature check IS the authentication, and it fails closed. The tenant is never read from the payload either: it is resolved from the verified platform identifier through the connection map, so an event from a workspace nobody connected does nothing. Refusals are written with their own status rather than being flattened to a 500, so a rejected signature reads as 401 and a malformed body as 400.

## Request

The document declares no body for `POST /v1/provider/github/webhook`. The handler is typed in cloud but its shape is not yet emitted, so the fields are not listed here — ask MCP's `describe` for `post_provider_github_webhook`, which answers from the running route.

## Response

The document declares no response body for this operation. It answers `200` on success and the platform error shape on failure — see [Errors](https://docs.hanzo.ai/docs/errors).

## Examples

**CLI**

```bash
hanzo provider github webhook
```

**SDK**

**.ts**

```ts
import { Configuration, ProviderApi } from 'hanzoai';

const api = new ProviderApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postProviderGithubWebhook();
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import ProviderApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = ProviderApi(client).post_provider_github_webhook()
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.ProviderAPI.PostProviderGithubWebhook(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, provider_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = provider_api::post_provider_github_webhook(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.ProviderApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new ProviderApi(client).postProviderGithubWebhook();
```

**HTTP**

```bash
curl -X POST https://api.hanzo.ai/v1/provider/github/webhook \
  -H "Authorization: Bearer $HANZO_API_KEY"
```

**MCP**

MCP reaches **provider** through the `provider` tool, which names its 38 operations with its own verbs — this one among them, under a name only MCP declares. `describe` explains any of them:

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "describe",
         "arguments": {
           "op": "list_connections"
         }
       }
     }'
```

---

[Provider API](https://docs.hanzo.ai/docs/openapi/provider) · [All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
