# Webhook endpoints

Manage a club's outbound notification endpoints.

Source: https://docs.champagne.dev-ukab.fr/fr/graphql/webhooks

Only a staff account with the `admin` role **for the requested club**, or the `settings_manage` permission, can use these operations. Member sessions, planning staff, anonymous requests and administrators of a different club are denied. The Settings page uses the same contract. The [delivery contract](/fr/webhooks) describes event types, signing, retries and the embedded pg-boss consumers.

A club registers up to **10 endpoints**. Each endpoint has its own URL, event selection, signing secret and paused state, and receives its own copy of every event it subscribes to.

## List endpoints [#list-endpoints]

```graphql
query ClubWebhookEndpoints($clubId: ID!) {
  clubWebhookEndpoints(clubId: $clubId) {
    id
    clubId
    url
    enabled
    enabledEvents
    createdAt
    updatedAt
  }
}
```

`clubId` must be a UUID in the authenticated administrator's club scope. Endpoints are returned oldest first; a club without endpoints returns an empty list. `enabled` is false while the endpoint is paused. `enabledEvents` lists selected event type IDs. The query never returns a signing secret. There are no filters, caller-selected sorting or pagination.

## Available event types [#available-event-types]

```graphql
query ClubWebhookEventCatalog($clubId: ID!) {
  clubWebhookEventCatalog(clubId: $clubId) {
    type
    title
    description
  }
}
```

The global `webhook_event_catalog` database table is seeded at API startup with currently implemented notification types. Settings renders its entries and descriptions in a multi-select. Entries are returned in display order; there are no filters, pagination or caller-selected sorting. Synthetic `webhook.test` is not in the catalog and cannot be selected. `clubId` must be a UUID in the administrator's club scope even though the catalog contents are global.

## Add an endpoint [#add-an-endpoint]

```graphql
mutation CreateClubWebhookEndpoint($clubId: ID!, $url: String!, $enabledEvents: [String!]) {
  createClubWebhookEndpoint(clubId: $clubId, url: $url, enabledEvents: $enabledEvents) {
    endpoint {
      id
      url
      enabled
      enabledEvents
    }
    signingSecret
  }
}
```

Pass a public HTTPS URL with a DNS hostname (no IP literal), no embedded credentials and no fragment. Pass `enabledEvents` as a list of `type` IDs from the global catalog; omitting it selects every implemented event, and an empty list queues nothing. The endpoint starts enabled. The response includes a random `signingSecret` generated for this endpoint. It is stored in the private PostgreSQL database and is visible to database administrators; protect database access and backups.

`BAD_USER_INPUT` is returned, without saving anything, for an invalid URL, an unknown club, a URL the club already registered, duplicate or unknown event IDs (including `webhook.test`), or an eleventh endpoint.

## Change or pause an endpoint [#change-or-pause-an-endpoint]

```graphql
mutation UpdateClubWebhookEndpoint(
  $clubId: ID!
  $endpointId: ID!
  $url: String
  $enabledEvents: [String!]
  $enabled: Boolean
) {
  updateClubWebhookEndpoint(
    clubId: $clubId
    endpointId: $endpointId
    url: $url
    enabledEvents: $enabledEvents
    enabled: $enabled
  ) {
    id
    url
    enabled
    enabledEvents
    updatedAt
  }
}
```

Every argument besides the two IDs is optional; an omitted or null argument keeps the current value. `url` and `enabledEvents` follow the rules of creation, and a URL edit keeps the signing secret. Pass `enabled: false` to pause the endpoint and `enabled: true` to resume it.

Events are queued for an endpoint only while it is enabled **and** their type is selected. Pausing skips its pending deliveries, including retries; deselecting a type skips the pending deliveries of that type. Resuming or reselecting does not forward events that happened meanwhile. Other endpoints of the club are unaffected. An endpoint that does not belong to the club returns `BAD_USER_INPUT`.

## Delete an endpoint [#delete-an-endpoint]

```graphql
mutation DeleteClubWebhookEndpoint($clubId: ID!, $endpointId: ID!) {
  deleteClubWebhookEndpoint(clubId: $clubId, endpointId: $endpointId)
}
```

Returns `true` once the endpoint and its signing secret are removed. Its pending deliveries are skipped. An endpoint that does not belong to the club returns `BAD_USER_INPUT`.

## Reveal signing secret [#reveal-signing-secret]

```graphql
mutation RevealClubWebhookSecret($clubId: ID!, $endpointId: ID!) {
  revealClubWebhookSecret(clubId: $clubId, endpointId: $endpointId) {
    signingSecret
  }
}
```

An administrator of the specified club can explicitly retrieve the secret of one of its endpoints; each reveal is recorded in the club activity. An endpoint outside the club or an invalid UUID returns `BAD_USER_INPUT`. The list query never includes secret material. The Settings screen keeps the value hidden until the administrator requests it.

## Reset signing secret [#reset-signing-secret]

```graphql
mutation RotateClubWebhookSecret($clubId: ID!, $endpointId: ID!) {
  rotateClubWebhookSecret(clubId: $clubId, endpointId: $endpointId) {
    endpoint {
      id
      url
      updatedAt
    }
    signingSecret
  }
}
```

Returns a fresh secret for that endpoint only; the other endpoints keep theirs. An endpoint outside the club returns `BAD_USER_INPUT`. Update the receiver to accept the new secret; in-flight and retried events use the active secret at delivery time. Do not put the signing secret in logs or a URL. The local GraphQL debugger redacts `signingSecret` fields in mutation responses; production logs must also remain private.

## Send a test delivery [#send-a-test-delivery]

```graphql
mutation SendClubTestWebhook($clubId: ID!, $endpointId: ID!) {
  sendClubTestWebhook(clubId: $clubId, endpointId: $endpointId) {
    eventId
    error
  }
}
```

This mutation requires the same scope and UUID validation as the other operations. It sends a real signed HTTPS POST to the endpoint's **saved** URL using its current signing secret, whether or not the endpoint is paused. The synthetic `webhook.test` payload contains `id`, `type`, `occurredAt`, `clubId`, and `data: { synthetic: true }`, but no member data. It bypasses the outbox and event selection, so it does not retry or generate a business event. A 2xx response returns `eventId` and null `error`; an endpoint outside the club or a failed delivery (a non-2xx response, a timeout or an unreachable endpoint) returns null `eventId` and a generic `error` without exposing the URL or signing secret. This `error` is an answer about the club's endpoint, not an API failure; the back office shows it as such. There are no filters, sorting, or pagination.
