Skip to content

Webhooks and integrations

Postgres functions cannot safely call external systems, and calling them from the browser is not safe either. PotaLab Base gives each project a transactional outbox:

  • A trigger or function enqueues a message in the same transaction as your data change.
  • After commit, Base delivers it and retries on failure.
  • A rolled-back transaction sends nothing. A committed one is not lost.

The outbox is enabled automatically when you create a webhook or configure SMTP.

Create webhooks in the dashboard (Integrations → Webhooks) or with the Management API:

POST /v1/projects/{ref}/webhooks
{ "name": "order created", "table": "public.orders", "events": ["INSERT"],
"url": "https://hooks.example.com/orders",
"headers": { "Authorization": "Bearer {{secret:HOOK_TOKEN}}" },
"timeout_ms": 10000 }

The response includes a signing_secret (whsec_...) shown once; rotate it with PATCH and { "rotate_secret": true }.

  • Payload: { type, schema, table, record, old_record, webhook_id, occurred_at }. A row larger than 1 MB is sent with truncated: true.
  • Signature: requests follow Standard Webhooks. The webhook-id, webhook-timestamp and webhook-signature headers are reserved. Verify them with any Standard Webhooks library and your whsec_... secret.
  • URL: http(s)://.... A literal private IP address is refused (400 url_not_allowed).
  • Works on tables in public and api.
perform base.http_request(
method => 'POST',
url => 'https://api.example-payments.com/v1/refunds',
headers => jsonb_build_object('Authorization', 'Bearer {{secret:PAYMENT_API_KEY}}'),
body => jsonb_build_object('charge', v_charge_id),
callback_function => 'public.on_refund_response', -- optional
timeout_ms => 10000);

Delivery is fire-and-forget: the response never reaches the calling transaction. API roles cannot call it, so wrap it in a SECURITY DEFINER function or trigger. An optional callback schema.fn(jsonb) receives { id, status, delivered, body, error, attempts } after the final outcome and must be granted to base_callback:

grant execute on function public.on_refund_response(jsonb) to base_callback;
perform base.send_email(recipient => new.email, subject => 'Order ' || new.id,
html => '<p>Thanks!</p>', text_body => 'Thanks!');

Mail uses your project’s SMTP settings (owner only, Settings → Email; custom SMTP needs a plan with custom_smtp) or the platform default. Auth emails are sent directly and do not use the outbox.

PUT /v1/projects/{ref}/secrets/PAYMENT_API_KEY { "value": "sk_live_..." }
GET /v1/projects/{ref}/secrets -> names and timestamps only
DELETE /v1/projects/{ref}/secrets/PAYMENT_API_KEY

Names match [A-Za-z_][A-Za-z0-9_]{0,63}, values are at most 16 KB and are encrypted at rest and never readable. Use {{secret:NAME}} in a URL, header or body and it is substituted in memory at send time. No SQL function returns a secret.

Outcome Result
2xx delivered
5xx, 408, 409, 425, 429, network error, unknown secret failed, retried with exponential backoff (at most 6 h between tries)
other 4xx, blocked target, invalid URL, deleted webhook dead immediately
8 attempts, or 24 h since creation dead

Inspect deliveries in Integrations → Deliveries or with GET /v1/projects/{ref}/outbox/deliveries?status=failed, and retry with POST .../outbox/deliveries/{id}/retry.

Inbound third-party webhooks, cron jobs and queues are not available.