Skip to main content

Overview

Webhooks are a way for your application to receive real-time notifications when events occur in your account. When an event occurs, we send an HTTP POST request to the webhook’s configured URL. You can use webhooks to trigger custom code, workflows, or integrations in your application.

Creating Webhooks

You can create and manage webhooks directly from the JustPaid dashboard:
  1. Navigate to SettingsDeveloperWebhooks
  2. Click Create Webhook URL
  3. Enter your webhook endpoint URL
  4. Select the event types you want to subscribe to
  5. Click Save
Each webhook includes a signing secret that you can use to verify the authenticity of incoming requests.

Two Different Naming Schemes

JustPaid uses two distinct sets of event-type strings, and mixing them up is the single most common integration bug.
  • Subscription identifiers are UPPER_SNAKE_CASE (e.g. INVOICE_STATUS_CHANGE). These are what you select in the dashboard and what appear in the API when configuring which events an endpoint receives.
  • Delivered event types are lowercase.dotted (e.g. invoice.status_changed). This is the value that actually arrives in the type field of the JSON body we POST to your endpoint.
Match your receiver on the lowercase.dotted values. If you compare the incoming type against INVOICE_STATUS_CHANGE, no event will ever match and your receiver will silently ignore every delivery.

Common integration mistake

A receiver written like this will drop every JustPaid event:
Compare against the delivered value instead:
Returning 200 while ignoring the event means retries will not fire and the delivery is recorded as successful on our side, so this failure mode is silent. Log unmatched type values during integration to catch it.

Event Types

The table below is exhaustive. Every value listed is a real, deliverable event type; there are no others.
Note that CUSTOMER_CONTRACT_CREATED maps to contract.created — the wire value drops the customer_ prefix. It is the one pair where the two names are not a mechanical transformation of each other.

Behavior worth knowing

invoice.created fires for DRAFT invoices. Creation is creation regardless of status — the event is emitted as soon as the invoice row is committed, so the invoice_status in your first event for an invoice is frequently draft. If you only care about issued invoices, filter on invoice_status or subscribe to invoice.status_changed instead. invoice.updated requires a tracked-field change. An update only emits an event when one of these fields actually changed value:
  • amount
  • due_date
  • invoice_date
  • description
  • notes
Editing anything else on an invoice produces no invoice.updated event. Do not rely on this event as a general “something about this invoice changed” signal. Status-change events carry the old value. invoice.status_changed, invoice_payment.status_changed, and credit_memo.status_changed include a data.previous_attributes object holding the prior status (invoice_status, payment_status, and memo_status respectively). It is only present when the status genuinely changed.

Payload Envelope

Every delivery uses the same envelope, regardless of event type: The request body is serialized with sorted keys and no whitespace before signing, so field order on the wire is alphabetical.

Example: invoice.status_changed

Example: invoice.created

A *.created event has no previous_attributes key at all:

Webhook Headers

The two signature headers are sent only when a signing secret is configured on the webhook endpoint. Endpoints without a secret receive only Content-Type.

Signature Verification

The signed input is the timestamp and the raw body joined by a literal period: {timestamp}.{body}. The key is your endpoint’s signing secret. Verify against the raw request body bytes exactly as received — we sign the compact, key-sorted JSON we transmit, so re-serializing a parsed object will produce a different string and the signature will not match.

Delivery and Retries

  • Webhooks are delivered asynchronously on a background queue, after the originating database transaction commits.
  • Each subscribed endpoint is dispatched independently. A failure on one endpoint does not prevent delivery to the others, and a retry never re-sends to an endpoint that already accepted the event.
  • Up to 3 retries, at a fixed 60-second delay between attempts (not exponential backoff).
  • Connection timeout is 5 seconds; response timeout is 30 seconds. Return a 2xx within 30 seconds or the attempt is treated as failed.
  • Every attempt is logged, including the request body, request headers, response status code, and response body. These are visible per endpoint in the dashboard and are the fastest way to diagnose an integration.
  • The event id is stable across retries, so it is safe to use for idempotency/deduplication.

Which failures are retried

Endpoints are never auto-disabled. No matter how many deliveries fail or for how long, we keep attempting future events to a configured endpoint. If an endpoint is dead, remove or update it in the dashboard — it will not deactivate itself, and repeated failures will not stop us from trying.

Best Practices

  1. Match on the delivered type — the lowercase.dotted value, never the UPPER_SNAKE subscription identifier
  2. Respond quickly — return a 2xx status code within 30 seconds
  3. Process asynchronously — acknowledge immediately, then queue the event for background processing
  4. Verify signatures — validate X-JustPaid-Signature against the raw body bytes
  5. Handle duplicates — use the event id for idempotency; it is stable across retries
  6. Log unmatched event types — the surest way to catch a naming mismatch before it becomes silent data loss
  7. Handle out-of-order delivery — events are delivered in order when possible, but independent dispatch and retries mean ordering is not guaranteed