Champagne

Webhook endpoints

Manage a club's outbound notification endpoints.

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 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

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

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

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

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

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

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

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

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.

On this page