Webhooks

Configure an HTTPS endpoint to receive signed POST notifications for 9 event types, verify payloads with HMAC-SHA256, and handle queued automatic retries.

Every plan except Free.

Webhooks send HTTP POST requests to your server when events happen in Portico. Examples include a client submitting a response, completing an onboarding, signing a document, or making a payment. Use them to trigger workflows in your own systems.

Webhooks work on Starter, Pro, Business, the Pro trial, and every lifetime and AppSumo plan. Portico checks the plan again before each delivery.

Setup

Only owners and admins can see the Integrations tab.

  1. Go to Settings > Integrations and find the Webhook section.
  2. Enter a public HTTPS address in Webhook URL.
  3. Click Save.

Portico generates a Signing secret when you first save a URL. Portico shows it once, with a Copy button. Copy it right away, because you need it to verify signatures. Saving a different URL later keeps the same secret.

  • Rotate signing secret opens the Rotate the signing secret? dialog. Click Rotate signing secret there to issue a new secret. Events queued before the rotation keep the previous secret's signature, including their retries. Accept both secrets until those deliveries finish, then accept only the new one.
  • Disconnect webhook opens the Disconnect this webhook? dialog. Click Disconnect webhook there to remove the URL and the secret, and new events stop. Deliveries Portico already prepared still go to their recorded address.

The Events card in settings lists five common events. Your endpoint receives all 9 events below.

Events

EventFires when
onboarding.sentYou send or resend an onboarding, from the app, the API, or an automation. It fires when Portico queues the invite email. Reopening an onboarding does not fire it.
onboarding.completedA client submits the final step and the onboarding is complete. Your review can still be pending.
onboarding.startedA client opens a sent onboarding for the first time, which marks it Opened. A later open after a resend does not fire it.
onboarding.overdueAn onboarding passes its due date without completion.
response.submittedA client submits answers. One event covers every answer in that submission.
response.approval_revokedA client changes an answer you already approved and submits it again. Portico revokes the approval automatically.
signature.signedA client signs a signature field, or a team member countersigns one.
payment.succeededA one-time Stripe payment succeeds, or a recurring Stripe subscription becomes active.
file.uploadedA client uploads a file to a file upload field.

The response.approval_revoked event is only sent to your team webhook, not to Zapier subscriptions.

Payload format

Every webhook POST has this structure:

{
  "id": "uuid",
  "event": "onboarding.sent",
  "timestamp": "2026-03-15T10:30:00.123456Z",
  "data": {
    "onboarding_id": "uuid",
    "client_id": "uuid",
    "client_name": "Jane Smith",
    "client_email": "jane@example.com",
    "template_id": "uuid",
    "template_name": "Agency Onboarding",
    "status": "sent",
    "progress_pct": 0,
    "created_at": "2026-03-14T09:00:00.123456+00:00"
  }
}
  • id is the event ID. It matches the X-Portico-Event-Id header and stays the same across retries.
  • timestamp is when the event happened, in UTC.
  • The data object always carries the base fields shown above. Its status and progress_pct are the onboarding's values when the event happened, and created_at is when the onboarding was created.

Each event adds its own fields to data:

EventExtra data fields
onboarding.sentNone
onboarding.completedNone
onboarding.startedNone
onboarding.overduedue_date, due_on, due_timezone
response.submittedfields_submitted (number of answers)
response.approval_revokedfield_ids (array)
signature.signedsignature_id, field_id, signer_name, signer_email, signer_role (client or team)
payment.succeededOne-time: payment_id, field_id, payment_intent_id, amount_cents. Recurring: subscription_id, field_id, payment_intent_id (holds the Stripe subscription ID), amount_cents, recurring, interval
file.uploadedfile_id, field_id, filename, size_bytes

Payloads carry no answer values and no file contents. To read answers, use the Responses endpoint, which needs a plan with API access.

Headers

Every webhook request includes these headers:

Content-Type: application/json
X-Portico-Event: onboarding.completed
X-Portico-Event-Id: uuid
Idempotency-Key: uuid
X-Portico-Signature: a1b2c3d4...

X-Portico-Event-Id and Idempotency-Key both carry the event ID. Use it to ignore a delivery you already processed. Every request to your webhook URL is signed.

Verifying signatures

Verify that a webhook came from Portico by computing the HMAC-SHA256 signature of the raw request body and comparing it to the X-Portico-Signature header. The header holds a lowercase hex digest. Verify the exact bytes you received, before you parse the JSON.

Node.js example:

import crypto from "crypto";

function verifyWebhook(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  if (typeof signature !== "string" || signature.length !== expected.length) {
    return false;
  }
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Python example:

import hmac
import hashlib

def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Use a constant-time comparison (as shown above) to prevent timing attacks.

Delivery and retries

  • Queued — Portico records each event when it happens and sends it from a delivery queue that runs every 5 minutes. Expect a short delay.
  • Timeout — Portico waits up to 10 seconds for your endpoint to respond.
  • Retries — a failed or timed-out attempt is retried, up to 10 attempts per event. The wait doubles after each failure, up to about 17 minutes, plus a few seconds. If your endpoint sends a Retry-After header, Portico waits that long, up to 1 hour.
  • Final failures — a 4xx response other than 408 or 429 stops retries for that event.
  • Non-blocking — webhook delivery never delays the action that triggered it. Your client's experience is unaffected if your endpoint is slow or down.

Your endpoint should return a 2xx status code to acknowledge receipt. The response body is ignored. Portico follows redirects.

Portico has no delivery log or manual retry in the app. To catch events your endpoint missed, poll the Events API endpoint. It needs a plan with API access and an API key with the events:read scope.

Zapier alternative

If you prefer not to manage webhook endpoints yourself, use the Zapier integration instead. It needs a plan with API access. Zapier handles the subscription and delivery automatically.

Testing

During development, use a tool like webhook.site or ngrok to inspect payloads before pointing the webhook at your production endpoint.

To trigger a test event, send an onboarding to yourself and complete it. This fires onboarding.sent, onboarding.started, response.submitted, and onboarding.completed in sequence.

Frequently asked questions

How many webhook event types are available?
9 event types: onboarding sent, started, completed, and overdue; response submitted and approval revoked; signature signed; payment succeeded; and file uploaded.
How are webhook payloads verified?
Every request includes an X-Portico-Signature header. It holds a hex HMAC-SHA256 of the raw request body, made with your signing secret. Compute the same value and compare.
What happens if my endpoint is down?
Portico retries from a delivery queue, up to 10 attempts per event. The wait doubles after each failure. A 4xx response other than 408 or 429 stops retries for that event.