# Manage endpoints

> Register, update, pause, resume, rotate, and remove endpoints with the combined client or the standalone PostgreSQL provider.

Source: https://good-webhooks.dev/docs/guides/endpoints



An endpoint is a URL in one [scope](https://good-webhooks.dev/docs/concepts/scopes.md) that subscribes to some of your event types. Endpoint management stores it with its status and its encrypted signing secret. To let signed-in users manage their own endpoints over HTTP, use the [Better Auth plugin](https://good-webhooks.dev/docs/better-auth.md) instead.

## Register an endpoint [#register-an-endpoint]

**Combined client**

The `webhooks` client from [installation](https://good-webhooks.dev/docs/installation.md#create-the-client) has an `endpoints` API. The root client works in application scope; `forScope()` selects a customer's scope.

```ts
import { webhooks } from './webhooks.js'

const customer = webhooks.forScope({ type: 'organization', id: organizationId })

const { endpoint, secret } = await customer.endpoints.create({
  url: 'https://receiver.example/webhooks',
  description: 'Invoice notifications',
  eventTypes: ['invoice.paid'],
})
```

**Standalone provider**

`createPostgresManagement()` stores endpoints without the delivery tables. Use it with a [custom sender](https://good-webhooks.dev/docs/guides/custom-senders.md), or when endpoint management and delivery live in different databases.

```ts
import { createPostgresManagement } from 'good-webhooks/management/postgres'
import { pool } from './db.js'

const management = createPostgresManagement({
  database: pool,
  eventTypes: ['invoice.created', 'invoice.paid'],
  encryptionKey: process.env.WEBHOOK_ENCRYPTION_KEY!,
})
await management.check()

const { endpoint, secret } = await management.create(
  { type: 'organization', id: organizationId },
  { url: 'https://receiver.example/webhooks', eventTypes: ['invoice.paid'] },
)
```

It needs only the management table:

```sh
node -e 'import("good-webhooks/migrations").then(m => process.stdout.write(m.getPostgresMigration({ component: "management" })))' > 001-webhook-management.sql
psql "$DATABASE_URL" --single-transaction -v ON_ERROR_STOP=1 -f 001-webhook-management.sql
```

Both need the storage encryption key from [installation](https://good-webhooks.dev/docs/installation.md#create-a-storage-key). Every process that reads these endpoints needs the **same** key.

### Hand over the signing secret [#hand-over-the-signing-secret]

`create()` returns the plaintext signing secret once; no read returns it again. Show it to the customer once, or send it over a secure channel. Their receiver passes it to [`parseWebhook()`](https://good-webhooks.dev/docs/guides/receive.md).

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

Endpoint status:

1. **active**: Receives new events and sends queued deliveries.
2. **paused**: Still subscribed. Queued deliveries wait for `resume()`.
3. **deleted**: Permanent. Secrets cleared, queued deliveries cancelled.

```ts
await customer.endpoints.update(id, { eventTypes: ['invoice.created', 'invoice.paid'] })
await customer.endpoints.pause(id)
await customer.endpoints.resume(id)
const { secret } = await customer.endpoints.rotateSecret(id, { graceMs: 3_600_000 })
await customer.endpoints.remove(id)
```

| Method         | Effect                                                                                     |
| -------------- | ------------------------------------------------------------------------------------------ |
| `update`       | Changes the URL, description, or subscriptions. A new URL is checked again                 |
| `pause`        | Keeps subscriptions and queued deliveries. The worker holds them until resume or expiry    |
| `resume`       | Checks the URL again, then queued deliveries send                                          |
| `rotateSecret` | Returns a new signing secret. Both sign requests during the overlap (default and max 24 h) |
| `remove`       | Permanent. Clears stored secrets, cancels queued deliveries, and keeps a deleted record    |

These calls return as soon as the record changes. A request the worker was already sending can still arrive after a pause or remove. See [delivery guarantees](https://good-webhooks.dev/docs/concepts/delivery-guarantees.md#endpoint-changes-while-work-is-queued).

## Limits [#limits]

| Limit                    | Value                                                                |
| ------------------------ | -------------------------------------------------------------------- |
| Live endpoints per scope | 1,000                                                                |
| Subscribed event types   | 1 to 100 per endpoint, each one of your configured names             |
| URL                      | Up to 2,048 characters, HTTPS, no credentials, no unsafe IP literals |
| Description              | Up to 2,000 characters                                               |

The standalone provider also resolves the URL's DNS at registration.

## When a scope owner leaves [#when-a-scope-owner-leaves]

Standalone management doesn't know about your users. Call `removeScope(scope)` from your own deletion workflow to remove every endpoint in that scope:

```ts
import { createPostgresManagement } from 'good-webhooks/management/postgres'

const management = createPostgresManagement({ database: pool, eventTypes, encryptionKey })
await management.removeScope({ type: 'organization', id: organizationId })
```

The combined client doesn't expose `removeScope`. Either build the provider separately as above, with the same database, schema, and key, or create it first, pass it to `createWebhooks({ management })`, and keep a reference.

## Next steps [#next-steps]

- [Receive webhooks](https://good-webhooks.dev/docs/guides/receive.md): What the customer does with the signing secret.
- [Key rotation](https://good-webhooks.dev/docs/operations/key-rotation.md): Rotate signing secrets and the storage encryption key.
- [Management reference](https://good-webhooks.dev/docs/reference/management.md): The full contract, limits, and errors.
