Webhook endpoints
A webhook endpoint is a URL Plugipay POSTs events to as they happen. You register one per environment (staging, production, the per-PR preview), pick which event types it cares about, and Plugipay signs every delivery with the endpoint's secret. The Go SDK exposes the three webhook-endpoint methods behind client.WebhookEndpoints. To verify an inbound delivery, see Webhooks; to read the persisted event log directly, see Events. For wire shapes, retry semantics, and the full event-type catalog, see API → Webhook endpoints.
Field on the Client
client.WebhookEndpoints — type *plugipay.WebhookEndpointsResource. Installed by NewClient; shares the parent *http.Client, base URL, key, and OnBehalfOf default. Safe for concurrent use.
Methods
List
Signature. func (r *WebhookEndpointsResource) List(ctx context.Context) ([]WebhookEndpoint, error)
Returns every webhook endpoint registered in the workspace. The list is small — typically one or two endpoints per environment — so the method returns a plain slice rather than a paginated Page[T].
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
endpoints, err := client.WebhookEndpoints.List(ctx)
if err != nil {
return err
}
for _, ep := range endpoints {
log.Printf("%s active=%t url=%s events=%v",
ep.ID, ep.Active, ep.URL, ep.Events)
}
The returned Secret field is empty on list — you only see it once, on Create.
Create
Signature. func (r *WebhookEndpointsResource) Create(ctx context.Context, in WebhookEndpointCreateInput) (*WebhookEndpoint, error)
Registers a new endpoint. URL is required (must be https:// in live mode); Events is an optional allow-list (omit to receive every event); Description is free-text. Auto-keyed for idempotency.
ep, err := client.WebhookEndpoints.Create(ctx, plugipay.WebhookEndpointCreateInput{
URL: "https://api.example.com/plugipay/webhooks",
Events: []string{
"checkout_session.completed",
"invoice.paid",
"refund.succeeded",
},
Description: ptr("production handler"),
})
if err != nil {
return err
}
// Save ep.Secret right now — this is the only time it's exposed.
storeSecret(ep.ID, ep.Secret)
Save
Secreton create — it's never returned again. SubsequentListcalls echo every field exceptSecret(it comes back as the empty string). If you lose it, delete and re-create the endpoint — rotation is delete-and-recreate, not patch.
Delete
Signature. func (r *WebhookEndpointsResource) Delete(ctx context.Context, id string) error
Removes an endpoint. Future events stop delivering to that URL immediately. In-flight deliveries (already sent, awaiting your 2xx) continue their retry schedule unless your server stops responding. Returns nil on success; *plugipay.Error{Status: 404} if the id doesn't exist.
if err := client.WebhookEndpoints.Delete(ctx, "we_01HX..."); err != nil {
var pe *plugipay.Error
if errors.As(err, &pe) && pe.Code == "not_found" {
return nil // already gone — treat as success
}
return err
}
There's no Update — rotation, URL changes, and event-list changes all happen by deleting the old endpoint and creating a new one. This keeps the secret-handling story simple (every secret has a single, unambiguous creation moment).
Types
type WebhookEndpoint struct {
ID string `json:"id"` // "we_..."
AccountID string `json:"accountId"`
URL string `json:"url"`
Events []string `json:"events"` // empty slice means "all"
Description *string `json:"description"`
Active bool `json:"active"`
Secret string `json:"secret,omitempty"` // only present on Create
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
For the per-event payload shapes and the event-type catalog: API → Webhook endpoints and API → Events.
Common patterns
One-shot bootstrap script
Wire your CI to provision the endpoint on first deploy, then store the secret in your secret manager:
func provision(ctx context.Context, c *plugipay.Client, url string) error {
eps, err := c.WebhookEndpoints.List(ctx)
if err != nil { return err }
for _, ep := range eps {
if ep.URL == url {
log.Printf("endpoint already exists: %s", ep.ID)
return nil
}
}
ep, err := c.WebhookEndpoints.Create(ctx, plugipay.WebhookEndpointCreateInput{
URL: url,
Events: []string{
"checkout_session.completed",
"invoice.paid",
"subscription.canceled",
},
})
if err != nil { return err }
return writeSecretToManager(ep.ID, ep.Secret)
}
The List-then-Create idiom is safe: webhook endpoints aren't unique on URL server-side, so a duplicate Create would happily mint a second one. Check first.
Rotating a secret
Delete the old endpoint, create a new one at the same URL, store the new secret, then update your handler:
old := "we_01HX..."
ep, err := c.WebhookEndpoints.Create(ctx, plugipay.WebhookEndpointCreateInput{
URL: "https://api.example.com/plugipay/webhooks",
Events: []string{ /* same set as the old endpoint */ },
})
if err != nil { return err }
if err := storeSecret(ep.ID, ep.Secret); err != nil { return err }
// Once your handler is reading the new secret, drop the old endpoint:
if err := c.WebhookEndpoints.Delete(ctx, old); err != nil { return err }
For a brief overlap, have your handler accept signatures from either secret; flip exclusively to the new one once you've drained the old endpoint's deliveries.
Per-environment provisioning
If you want CI to manage a per-PR endpoint that auto-cleans on PR close:
// On PR open:
ep, _ := c.WebhookEndpoints.Create(ctx, plugipay.WebhookEndpointCreateInput{
URL: fmt.Sprintf("https://pr-%d.staging.example.com/plugipay/webhooks", prNum),
Description: ptr(fmt.Sprintf("pr-%d", prNum)),
})
// On PR close:
_ = c.WebhookEndpoints.Delete(ctx, ep.ID)
Errors with errors.As
err := c.WebhookEndpoints.Delete(ctx, id)
var pe *plugipay.Error
if errors.As(err, &pe) {
switch pe.Code {
case "not_found":
// already gone — fine
case "insufficient_scope":
// key lacks plugipay:webhook:write
}
}
Context cancellation
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()
ep, err := c.WebhookEndpoints.Create(ctx, in)
Create is auto-keyed for idempotency — retrying after *plugipay.Error{Code: "timeout"} is safe; the server returns the same endpoint and secret.
Errors
Code |
Status |
Cause |
|---|---|---|
validation_error |
400 | Bad URL (non-https in live mode), unknown event type. |
not_found |
404 | Delete on a missing id. |
insufficient_scope |
403 | Key lacks plugipay:webhook:write. |
Full mechanics: Errors.
Next
- Webhooks — verify deliveries with the stored secret.
- Events — the same data via pull instead of push.
- API → Webhook endpoints — HTTP-level reference.