1. Getting Started
  2. Webhooks & Events

Getting Started

Webhooks & Events

Events

An event is emitted anytime an object is updated on the database. An event is broken down into two sections, topic and context. The topic generally aligns with the object or API that the event was triggered on and the context explains what happened.

For example, shipment.delivered or fulfillment.created are events where shipment and fulfillment are the topics of the events, and the context is that the shipment was delivered and the fulfillment was created. You'll find more details about all events that each topic can emit in their API sections.

Event Model


id string
The unique ID of the event prefixed with event_.

object "event"
The object type.

type string
The event type in dot notation (e.g., shipment.created, fulfillment.updated).

topic string
The topic of the event, corresponding to the resource type (e.g., shipment, fulfillment, contact).

data object|null
The resource data at the time the event was triggered.

message object|null
The message associated with the event, if any.

resource_id string|null
The ID of the resource that triggered the event.

location_id string|null
The ID of the location associated with the event, if applicable.

status string
The processing status of the event.

Status Description
queued Event has been created and is waiting to be picked up for processing.
processing Event is currently being processed (webhooks, notifications, side effects).
completed All processing steps finished successfully.
attempted Processing finished but one or more steps failed. For example, a push notification or SMS could not be delivered, but webhooks and logs succeeded. Inspect steps to see what failed.
failed A critical error occurred during processing - the event handler threw an unrecoverable exception. Any steps that completed before the failure are still recorded.
timed_out The event was stuck in processing for too long and was marked as timed out by the dispatcher.

steps array
An array of processing steps that were performed for this event. Each step tracks the outcome of an individual operation (e.g., webhook delivery setup, log creation, push notifications, SMS/email notifications). Steps provide visibility into exactly which operations succeeded or failed.

Show Object
step.name string - The operation that was performed (e.g., webhooks_list, log_write, user_push_notifications, contact_sms_notification).
step.status string - One of completed (succeeded), failed (encountered an error), or skipped (applicable but intentionally not executed, e.g., webhooks disabled).

batch_id string|null
The ID of the batch associated with the event, if applicable.

initial_webhooks array
The webhook IDs that were assigned when the event was created.

pending_webhooks array
The webhook IDs that have not yet been attempted.

attempted_webhooks array
The webhook IDs that have had at least one delivery attempt.

pending_workflows array
The workflow IDs that are pending execution for this event.

created_at integer
Time in epoch seconds when this event was created.

updated_at integer
Time in epoch seconds when this event was last updated.

Example Response

json
        {
  "object": "event",
  "id": "event_...",
  "type": "shipment.created",
  "topic": "shipment",
  "status": "completed",
  "steps": [
    { "name": "webhooks_list", "status": "completed" },
    { "name": "log_write", "status": "completed" },
    { "name": "user_notifications_create", "status": "completed" },
    { "name": "user_push_notifications", "status": "completed" }
  ],
  "data": { ... },
  "message": null,
  "resource_id": "shp_...",
  "location_id": "loc_...",
  "batch_id": null,
  "initial_webhooks": ["hook_...", "hook_..."],
  "pending_webhooks": ["hook_..."],
  "attempted_webhooks": ["hook_..."],
  "pending_workflows": [],
  "created_at": 1739264400,
  "updated_at": 1739264400
}

      

List Events

GET
`/v1/events`

Retrieve all events for your organization. The response includes a data array and pagination object.

js
        const response = await fetch("https://api.packagex.io/v1/events", {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const events = response.data;
const pagination = response.pagination;

      

Filtering

You can filter events using the following query parameters:

Parameter Type Description
type string Filter by event type (e.g., shipment.created)
topic string Filter by event topic (e.g., shipment, fulfillment)
resource_id string Filter by the resource that triggered the event
status string Filter by processing status: queued, processing, completed, attempted, failed, timed_out
js
        const response = await fetch("https://api.packagex.io/v1/events?topic=shipment&type=shipment.created", {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

      

Retrieve Event

GET
`/v1/events/:event`

Retrieve a single event using its id.

js
        const response = await fetch(`https://api.packagex.io/v1/events/${event_id}`, {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const event = response.data;

      


Webhooks

You may want to listen to these events to trigger actions downstream outside of the platform instead. In the Developer Center section of the dashboard, you can set up webhooks and subscribe to whatever events you want to listen to. You can create a maximum of 10 webhooks per organization. Our webhooks provide a generous 5000ms wait time per attempt, after which the attempt will be marked as failed if a 2XX status code is not received.

Webhook Authentication

When creating the webhook, you are able to specify a custom header of your choice to be sent with the request. You may specify both the key and value. Certain header keys are blocked for security reasons, including standard transport headers (Host, Content-Type, Transfer-Encoding), credential headers (Cookie, Authorization), and proxy headers (X-Forwarded-For, X-Real-IP).

While we encrypt all information in our databases, we believe that the best practice here is to provide a hashed or encrypted value to us, and to implement some extra authentication on your end.

Webhook Payload

When a subscribed event occurs, we send a

POST
request to your webhook URL with the following payload:

json
        {
  "message": "Shipment Updated",
  "data": { ... },
  "event": "shipment.updated",
  "event_id": "event_..."
}

      

Discord and Slack webhook URLs receive simplified payloads formatted for their respective platforms.


Webhook Model


id string
The unique ID of the webhook prefixed with hook_.

object "webhook"
The object type.

url string
The URL that webhook events will be sent to.

events array
The list of events this webhook is subscribed to, in dot notation (e.g., shipment.created).

status string
The current status of the webhook. One of enabled, disabled, or suspended.

retry_mode string
The retry strategy for failed deliveries. One of fast, balanced, or slow. Defaults to balanced.

supersede boolean
When enabled, skips retries for stale events if a newer event exists for the same resource. Defaults to false.

max_concurrency integer
A target for how many deliveries we send to this endpoint at the same time. Between 1 and 250, defaults to 100. Deliveries beyond it wait in the queue and are sent as earlier ones complete, in order per resource. It is a target rather than a hard cap: we count deliveries already in flight, and brief bursts slightly above your value are possible.

header object
A custom header sent with each webhook request.

Show Object
header.key string
header.value string

disabled_at integer|null
Time in epoch seconds when the webhook was disabled or suspended. null if the webhook is active.

enable_at integer|null
Time in epoch seconds when a suspended webhook will be automatically re-enabled. null if not suspended.

stats object
Delivery statistics for the webhook, updated daily.

Show Object
stats.last_24h object
stats.last_24h.success integer
stats.last_24h.failure integer
stats.last_24h.rate number
stats.previous_24h object
stats.previous_24h.success integer
stats.previous_24h.failure integer
stats.previous_24h.rate number
stats.last_48h object
stats.last_48h.success integer
stats.last_48h.failure integer
stats.last_48h.rate number

checksum string
A checksum of the webhook configuration, used for change detection.

created_at integer
Time in epoch seconds when this resource was created.

updated_at integer
Time in epoch seconds when this resource was last updated.

Example Response

json
        {
  "object": "webhook",
  "id": "hook_...",
  "url": "https://example.com/webhook",
  "events": ["shipment.created", "shipment.updated"],
  "status": "enabled",
  "retry_mode": "balanced",
  "supersede": false,
  "max_concurrency": 100,
  "header": { "key": "X-Webhook-Secret", "value": "my-secret" },
  "disabled_at": null,
  "enable_at": null,
  "stats": {
    "last_24h": { "success": 150, "failure": 3, "rate": 0.98 },
    "previous_24h": { "success": 140, "failure": 2, "rate": 0.986 },
    "last_48h": { "success": 290, "failure": 5, "rate": 0.983 }
  },
  "checksum": "a1b2c3...",
  "created_at": 1739264400,
  "updated_at": 1739264400
}

      

Create Webhook

POST
`/v1/webhooks`

Creates a new webhook. On creation, a test

POST
is sent to the provided URL. If the URL does not return a 2XX status code within 5000ms, the request will be rejected.

Request Body

url string (required)
The URL to receive webhook events. Must be a valid http:// or https:// URL. Localhost, raw IP addresses (IPv4/IPv6), and cloud metadata endpoints are blocked.

events array (required)
An array of event types to subscribe to. Must contain at least one event.

header object
A custom header to include with every webhook request.

retry_mode string
The retry strategy. One of fast, balanced (default), or slow.

supersede boolean
Enable supersede mode. Defaults to false.

max_concurrency integer
A target for simultaneous deliveries to your endpoint. Between 1 and 250, defaults to 100. Lower it if your endpoint cannot keep up. Raising it lets us send more at once when there is a backlog for you; it does not reserve capacity or give you priority over other traffic.

js
        const data = {
  url: "https://example.com/webhook",
  events: ["shipment.created", "shipment.updated", "fulfillment.created"],
  header: {
    key: "X-Webhook-Secret",
    value: "my-secret-value",
  },
  retry_mode: "balanced",
  supersede: false,
  max_concurrency: 100,
};

const response = await fetch("https://api.packagex.io/v1/webhooks", {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
}).then((res) => res.json());

const webhook = response.data;

      

Update Webhook

POST
`/v1/webhooks/:webhook`

Updates an existing webhook. If the url or header changes, a test

POST
is sent to the new URL. If you update a disabled webhook with a valid URL, the webhook will be automatically re-enabled upon a successful test.

All fields are optional.

js
        const data = {
  events: ["shipment.created", "shipment.updated", "shipment.delivered"],
  retry_mode: "slow",
  supersede: true,
  max_concurrency: 50,
};

const response = await fetch(`https://api.packagex.io/v1/webhooks/${webhook_id}`, {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
}).then((res) => res.json());

const webhook = response.data;

      

Retrieve Webhook

GET
`/v1/webhooks/:webhook`

Retrieve a single webhook using its id.

js
        const response = await fetch(`https://api.packagex.io/v1/webhooks/${webhook_id}`, {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const webhook = response.data;

      

List Webhooks

GET
`/v1/webhooks`

Retrieve all webhooks for your organization. The response includes a data array and pagination object.

js
        const response = await fetch("https://api.packagex.io/v1/webhooks", {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const webhooks = response.data;
const pagination = response.pagination;

      

Filtering

You can filter webhooks using the following query parameters:

Parameter Type Description
events string Filter by subscribed event (e.g., shipment.created)
topic string Filter by event topic (e.g., shipment, fulfillment)
status string Filter by webhook status: enabled, disabled, or suspended
disabled_at string Filter by disabled date
enable_at string Filter by re-enable date
js
        const response = await fetch("https://api.packagex.io/v1/webhooks?status=enabled&topic=shipment", {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

      

Delete Webhook

DELETE
`/v1/webhooks/:webhook`

Delete a webhook using its id. This will also remove all pending and historical delivery records for the webhook.

js
        await fetch(`https://api.packagex.io/v1/webhooks/${webhook_id}`, {
  method: "DELETE",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
});

      

Outgoing Webhook Model

An outgoing webhook represents a single delivery attempt (or series of attempts) for a specific event to a specific webhook endpoint. Each time an event is created, one outgoing webhook record is created per matching webhook subscription.


id string
The unique ID of the outgoing webhook.

object "outgoing_webhook"
The object type.

organization object
The organization this outgoing webhook belongs to.

Show Object
organization.id string

webhook object
The webhook this delivery is for.

Show Object
webhook.id string

event object
The event being delivered.

Show Object
event.id string

status string
The current delivery status. One of dropped, queued, processing, superseded, completed, failed, or timed_out.

attempt_count integer
The number of delivery attempts made so far (0-5).

attempts array
An array of delivery attempt records. Each attempt contains:

Show Object
attempt.attempt_at integer
attempt.duration number
attempt.status "success"|"failure"|"error"
attempt.message string
attempt.status_code integer|null
attempt.retry_after_ms integer|null

retry_after_ms is the wait your endpoint asked for on this attempt, in milliseconds, after our 5 minute cap. It is set only when the attempt was rate limited with a 429 response and stated a wait - via Retry-After, or failing that RateLimit-Reset or X-RateLimit-Reset. It is the value we took as a FLOOR, not the delay actually applied: the real wait is that floor or our own retry delay, whichever is longer, plus up to 25% spread.

first_attempt_at integer|null
Time in epoch seconds of the first delivery attempt. null if no attempt has been made.

last_attempt_at integer|null
Time in epoch seconds of the most recent delivery attempt. null if no attempt has been made.

last_attempt_status_code integer|null
The HTTP status code from the most recent attempt. null if no attempt has been made or if the attempt was a network error.

last_attempt_duration float|null
The duration in milliseconds of the most recent attempt. null if no attempt has been made.

next_attempt_at integer|null
Time in epoch seconds when the next retry is scheduled. null if no retry is pending.

created_at integer
Time in epoch seconds when this record was created.

updated_at integer
Time in epoch seconds when this record was last updated.

Example Response

json
        {
  "object": "outgoing_webhook",
  "id": "01952...",
  "organization": { "id": "org_..." },
  "webhook": { "id": "hook_..." },
  "event": { "id": "event_..." },
  "status": "completed",
  "attempt_count": 1,
  "attempts": [
    {
      "attempt_at": 1739264401,
      "duration": 567,
      "status": "success",
      "message": "OK",
      "status_code": 200,
      "retry_after_ms": null
    }
  ],
  "first_attempt_at": 1739264401,
  "last_attempt_at": 1739264401,
  "last_attempt_status_code": 200,
  "last_attempt_duration": 567,
  "next_attempt_at": null,
  "created_at": 1739264400,
  "updated_at": 1739264400
}

      

Data Retention

Outgoing webhook records are automatically purged based on their status. Records are cleaned up daily.

Status Retention Description
completed 7 days Successfully delivered
failed 7 days All retry attempts exhausted
dropped 7 days The destination webhook was disabled before this event could be delivered
timed_out 7 days Stuck in processing for over 15 minutes
superseded 7 days Skipped because a newer event exists for the same resource
queued Not purged Actively waiting for delivery. Dropped if the destination webhook stays disabled for 48 hours
processing Not purged Currently being delivered (set to timed_out after 15 minutes if stuck)

List Outgoing Webhooks

GET
`/v1/webhooks/outgoing`

Retrieve all outgoing webhook delivery records for your organization. Use this to monitor webhook delivery health, debug failed deliveries, and inspect attempt history. The response includes a data array and pagination object.

js
        const response = await fetch("https://api.packagex.io/v1/webhooks/outgoing", {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const outgoing_webhooks = response.data;
const pagination = response.pagination;

      

Filtering

You can filter outgoing webhooks using the following query parameters:

Parameter Type Description
webhook_id string Filter by webhook ID (e.g., hook_...)
event_id string Filter by event ID (e.g., event_...)
status string Filter by delivery status: queued, processing, completed, failed, superseded, dropped, timed_out
attempt_count number Filter by attempt count
last_attempt_status_code number Filter by last HTTP status code
created_at string Filter by creation date
next_attempt_at string Filter by next retry date
first_attempt_at string Filter by first attempt date
last_attempt_at string Filter by last attempt date

Ordering

Use the order_by parameter to sort results. Supported values: created_at, next_attempt_at, last_attempt_at, first_attempt_at, attempt_count.

js
        // Get failed outgoing webhooks for a specific webhook, ordered by last attempt
const response = await fetch(
  "https://api.packagex.io/v1/webhooks/outgoing?status=failed&webhook_id=hook_...&order_by=last_attempt_at&order=desc",
  {
    method: "GET",
    headers: {
      "PX-API-KEY": process.env.PX_API_KEY,
      "Content-Type": "application/json",
    },
  }
).then((res) => res.json());

      

List Outgoing Webhooks by Event

GET
`/v1/events/:event/outgoing-webhooks`

Retrieve all outgoing webhook delivery records for a specific event. This is useful for inspecting which webhooks were notified for a particular event and what the delivery status is for each. This endpoint requires both events:read and webhooks:read scopes.

js
        const response = await fetch(`https://api.packagex.io/v1/events/${event_id}/outgoing-webhooks`, {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const outgoing_webhooks = response.data;

      


Retry Modes

When a webhook delivery fails (non-2XX response or timeout), the system will retry the delivery up to 4 additional times for a maximum of 5 total attempts. You can choose between three retry strategies when creating or updating a webhook.

The delays below are exact for an ordinary failure. A retry that was rate limited with a 429 is spread by up to 25% either side of the listed value instead, so a burst of rate-limited deliveries does not come back in lockstep - see Rate Limiting.

Fast Mode

Best for time-sensitive integrations where you want quick retries.

Attempt Delay After Failure
1st retry 10 seconds
2nd retry 20 seconds
3rd retry 30 seconds
4th retry 60 seconds

Total retry window: approximately 2 minutes. For a rate-limited delivery (see Rate limiting) the window can reach approximately 25 minutes.

Balanced Mode (default)

A middle ground, and the default for new webhooks. It gives a struggling endpoint room to recover without holding a delivery for the best part of an hour.

Attempt Delay After Failure
1st retry 15 seconds
2nd retry 1 minute
3rd retry 3 minutes
4th retry 8 minutes

Total retry window: approximately 12 minutes, or up to approximately 29 minutes for a rate-limited delivery.

Slow Mode

Best for integrations where the receiving server may need more time to recover.

Attempt Delay After Failure
1st retry 30 seconds
2nd retry 5 minutes
3rd retry 15 minutes
4th retry 30 minutes

Total retry window: approximately 50 minutes. For a rate-limited delivery (see Rate limiting) the window can reach around 70 minutes.

Existing webhooks keep whichever mode they are already set to. Changing retry_mode affects future retries only; a delivery already waiting keeps the schedule it was given.


Rate Limiting

If your endpoint answers a delivery with 429 Too Many Requests, we treat it as a request to slow down rather than as a failure on your side:

  • We wait as long as your endpoint asks, up to 5 minutes per retry. Tell us how long with Retry-After - either a number of seconds or an HTTP date. If that header is absent or unreadable we also read, in order, RateLimit-Reset-After, X-RateLimit-Reset-After, RateLimit-Reset and X-RateLimit-Reset. Whatever we take from you is reported back on the attempt as retry_after_ms, and it acts as a floor: the actual wait is that value or our own retry delay, whichever is longer, plus the usual spread. Send the reset headers in seconds from now if you can - that is what the -After pair mean, and they cannot be misread. The two without the suffix are ambiguous in the wild, so we read a large value as a UNIX timestamp and a small one as a number of seconds.
  • The whole endpoint pauses, not just the one delivery. A stated wait applies to everything queued for that endpoint. Deliveries held back this way are not attempted at all, so they do not spend one of their 5 attempts waiting for you.
  • Retries are spread out. When a burst of deliveries is rate limited at once, their retries are scattered rather than sent together, so they do not arrive as a second burst. The spread goes both ways, so an individual retry can land slightly sooner than the listed delay as well as later - never sooner than a wait you asked for.
  • Rate limits do not suspend your webhook. 429 responses are excluded from the hourly temporary-suspend check below, so a rate-limited endpoint keeps receiving deliveries at a reduced pace instead of being paused for an hour.
  • We reduce how much we send you, automatically. While your endpoint keeps returning 429, we lower the number of simultaneous deliveries to it, and raise it again once it stops. This needs no configuration from you. It works in terms of simultaneous deliveries rather than requests per second, so if your rate limit is measured per second it will reduce the pressure rather than land exactly on your limit - set max_concurrency on the webhook if you want a specific ceiling.

A 429 still consumes one of the delivery's 5 attempts: a delivery rate limited on every attempt ends as failed like any other.

All of the above applies to 429 only. If your gateway answers with 503 Service Unavailable instead - which is what nginx's limit_req returns by default - the delivery counts as an ordinary failure: we do not read your Retry-After, we do not slow down, and the responses do count toward the hourly suspension. If you rate limit us, answer with 429.

What the daily health checks do and do not catch. They count deliveries that finished - delivered, or out of attempts. A delivery that was rate limited a few times and then accepted counts as a success, so an endpoint that rate limits heavily but eventually accepts everything can look healthy. An endpoint where deliveries genuinely run out of attempts is still reported as degraded and can still be permanently disabled.

If you would rather set the limit yourself than have us discover it, use max_concurrency on the webhook - the automatic reduction applies beneath whatever value you set.

Lowering max_concurrency works even while your endpoint is rate limiting everything - a save that changes only the retry and delivery settings goes straight through. Three saves do send a live test request to your URL first, and a 429 to that request is treated as a failed validation and answered with a 400: creating a webhook, changing its URL or a custom header, and any save against a webhook that is currently disabled. If you need one of those while your endpoint is rate limiting us, either let it recover first or exempt our test request from your rate limit.


Supersede Mode

When supersede is enabled on a webhook, the system will skip retries for an event if a newer event already exists for the same resource and webhook combination. Instead of retrying, the older event is marked as superseded.

This is useful for high-frequency resources like shipment tracking, where the consumer only cares about the latest state. Retrying stale updates would waste the retry budget when a more recent update is already queued.


Ordering Guarantee

Webhooks for the same resource are delivered in chronological order. Only one delivery per resource and webhook combination can be in-flight at a time. If a webhook is processing an event for a given resource, no additional events for that same resource will be dispatched until the current delivery completes, fails all retries, or is abandoned.

A delivery is treated as abandoned if it stays in processing for more than 15 minutes without finishing, which releases the resource so later events are not blocked indefinitely. In that case the abandoned delivery moves to timed_out and the next event for the resource is dispatched.


Webhook Health Monitoring

PackageX automatically monitors the health of your webhooks and takes action to protect both your integration and our delivery infrastructure.

Temporary Suspend

If a webhook accumulates 50 or more failures with 0 successes within the last hour, it will be automatically suspended for 1 hour. Deliveries whose most recent attempt was rate limited with a 429 response do not count toward this - see Rate limiting. Delivery is paused for the suspension: events are still queued, no delivery attempts are made against the endpoint, and the queue is delivered in order once the webhook is re-enabled. No events are lost, and the pause means the suspension does not consume the retry budget of the events waiting behind it.

When a webhook is suspended, a webhook.suspended event is emitted and a notification email is sent. The webhook's enable_at field indicates when it will automatically resume. After the suspension period, the webhook is automatically re-enabled and queued events begin processing.

Permanent Disable

Webhook health is evaluated daily. The degraded warning and the permanent disable are independent checks:

  • Degraded warning: If a webhook has a failure rate above 30% (success rate below 70%) with more than 100 failures in the last 24 hours, a warning email is sent to organization administrators and a webhook.degraded event is emitted.
  • Permanent disable: If a webhook has a failure rate above 30% with more than 500 failures in both the last 24-hour window and the previous 24-hour window, it is permanently disabled. A notification email is sent and a webhook.disabled event is emitted.

Re-enabling a Disabled Webhook

A disabled webhook can be re-enabled by updating it via the API. When you update a disabled webhook (for example, by correcting the URL), a test

POST
is sent to the URL. If the test succeeds, the webhook is automatically set back to enabled.

While a webhook is disabled no delivery attempts are made, and its already-queued events are held for 48 hours so that re-enabling within that window resumes delivery of the backlog. Events still queued after 48 hours are moved to dropped and are not delivered. Re-enable promptly if the pending events matter to you.


Webhook Lifecycle Events

The webhook topic emits the following events, which can be listened to via other webhooks:

Event Description
webhook.created A new webhook was created
webhook.updated A webhook was updated (only emitted when the configuration changes)
webhook.deleted A webhook was deleted
webhook.degraded A webhook's failure rate has exceeded the health threshold
webhook.suspended A webhook has been temporarily suspended for 1 hour after repeated failures
webhook.disabled A webhook has been permanently disabled due to sustained failures

URL Validation

Webhook URLs must be valid http:// or https:// URLs. The following URL types are blocked for security:

  • localhost
  • Raw IPv4 addresses (e.g., 192.168.1.1)
  • Raw IPv6 addresses
  • Cloud metadata endpoints

When creating or updating a webhook, a test

POST
is sent to the URL with a test payload. If the URL does not respond with a 2XX status code within 5000ms, the create or update request will be rejected with a 400 error.

Integration Error Notifications

In addition to standard webhook-driven events, PackageX can send email notifications when integration errors exceed a configured threshold. These notifications apply to both outgoing integrations (e.g., NetSuite, webhooks you push data to) and incoming webhooks (data received from external systems).

When the error count within a configurable time window exceeds your organization's failure limit, an event is published and email notifications are sent to all users subscribed to the corresponding event. These events are internal notification events and are not dispatched to your webhook endpoints.

Event Topic Description
outgoing_integration__error outgoing_integration Outgoing integration errors exceeded the configured failure limit
incoming_webhook__error incoming_webhook Incoming webhook errors exceeded the configured failure limit

To configure these notifications, see the integration_notifications section of your Organization Settings.


Event Subscribers

Event subscribers are organization-managed email destinations that always receive a curated set of system-level operational alerts (webhook health and integration errors), independent of any user account or user notification setting. Use them to guarantee that an operations or compliance inbox is always notified - for example, an alerts@yourcompany.com address that should hear about a permanently disabled webhook regardless of who is logged in.

Subscriber emails are merged - de-duplicated - into the same emails these events already send to your organization's users, so a subscriber receives the identical message body.

Subscribable Events

Only these system-level event types can be subscribed to. Request the live list from GET /v1/events/types?type=system.

Event Description
webhook.degraded A webhook's failure rate has exceeded the health threshold
webhook.suspended A webhook has been temporarily suspended after repeated failures
webhook.disabled A webhook has been permanently disabled
incoming_webhook.error Incoming webhook errors exceeded the configured failure limit
outgoing_integration.error Outgoing integration errors exceeded the configured failure limit

Lifecycle

A subscriber must confirm its address (and, depending on your org, be approved by an owner) before it starts receiving emails. Recipients cannot unsubscribe themselves - only the organization can remove a subscriber.

  1. Create the subscriber. It starts as pending_verification and a verification email is sent to the address.
  2. Verify: the recipient clicks the link in that email, which confirms the address. If the subscriber was already approved at creation it becomes active; otherwise it moves to pending_approval. A subscriber is only auto-approved at creation when it is created by a user who is the organization's sole owner - a subscriber created with an API key is never auto-approved and always moves to pending_approval after verification.
  3. Approve (dual control): a pending_approval subscriber must be approved by an organization owner. Approval is a user action available only to an organization owner who did not create the subscriber (use the dashboard, or a user-authenticated request). API keys cannot approve subscribers - an attempt returns 403. Because of this, a subscriber created with an API key always needs a human owner to approve it before it goes active.
  4. A subscriber whose verification link is never confirmed expires to verification_failed after 3 days; an owner can resend a fresh link (capped at 3 verification emails per address).
Status Meaning
pending_verification Created; awaiting the recipient to confirm the address
pending_approval Address verified; awaiting approval by an organization owner
active Verified and approved; receiving subscribed events
verification_failed The verification link expired before it was confirmed

Who can create and approve

Creation is open to any caller with the notifications:write scope (including API keys). Approval is an owner-only user action and is never available to API keys.

Scenario Result
Created with an API key After verification → pending_approval; an organization owner must approve it
Created by a user who is the org's sole owner Auto-approved at creation → becomes active as soon as the address is verified
Created by an owner, approved by a different owner Becomes active after verification (dual control)
Approval attempted with an API key 403 - API keys cannot approve subscribers
Approval attempted by the same person who created it Rejected - the approver must be a different owner (unless they are the org's sole owner)

Event Subscriber Model


id string
The unique ID of the event subscriber prefixed with esub_.

object "event_subscriber"
The object type.

channel string
The delivery channel. Currently always email.

destination string
The email address that receives the subscribed events.

events array
The list of subscribed system-level event types, in dot notation.

status string
One of pending_verification, pending_approval, active, or verification_failed.

verified_at integer|null
Time in epoch seconds when the address was verified. null if not yet verified.

approved_by string|null
The ID of the user who approved the subscriber. null if not yet approved.

approved_at integer|null
Time in epoch seconds when the subscriber was approved. null if not yet approved.

created_by string|null
The ID of the user who created the subscriber.

created_at integer
Time in epoch seconds when this subscriber was created.

updated_at integer
Time in epoch seconds when this subscriber was last updated.

Example Response

json
        {
  "object": "event_subscriber",
  "id": "esub_...",
  "channel": "email",
  "destination": "alerts@yourcompany.com",
  "events": ["webhook.disabled", "webhook.suspended"],
  "status": "active",
  "verified_at": 1739264500,
  "approved_by": "user_...",
  "approved_at": 1739264600,
  "created_by": "user_...",
  "created_at": 1739264400,
  "updated_at": 1739264600
}

      

List Subscribable Event Types

GET
`/v1/events/types`

Returns the catalog of event types that can be subscribed to. Pass ?type=system to return only the system-level events listed above, or ?type=all (default) for the complete event catalog. The response is a paginated data array of dotted event-type strings.

js
        const response = await fetch("https://api.packagex.io/v1/events/types?type=system", {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const event_types = response.data;

      

Create Event Subscriber

POST
`/v1/events/subscribers`

Creates a subscriber in pending_verification and sends a verification email to the address. Requires the notifications:write scope. An organization may have at most 25 event subscribers.

Request Body

email string (required)
The email address to subscribe.

events array (required)
A non-empty array of system-level event types to subscribe to (see the table above).

js
        const data = {
  email: "alerts@yourcompany.com",
  events: ["webhook.disabled", "webhook.suspended"],
};

const response = await fetch("https://api.packagex.io/v1/events/subscribers", {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
}).then((res) => res.json());

const subscriber = response.data;

      

Update Event Subscriber

POST
`/v1/events/subscribers/:subscriber`

Updates a subscriber. Requires the notifications:write scope. The destination address and channel are immutable (remove and re-add to change the address). The body supports:

events array
Replace the subscribed events. On an active subscriber this is restricted to an organization owner or the subscriber's original creator.

approve boolean
Approve a verified, pending_approval subscriber (dual control). This is an owner-only user action: it succeeds only for an organization owner who did not create the subscriber. It cannot be performed with an API key - an API-key request returns 403. Approval is normally done from the dashboard.

resend boolean
Resend the verification email (only valid while the subscriber is unverified; capped at 3 sends per address).

js
        // Re-scope the events an existing subscriber receives. (Approval - `{ approve: true }` -
// is an owner-only user action and cannot be done with an API key; see above.)
const data = { events: ["webhook.disabled", "webhook.suspended", "webhook.degraded"] };

const response = await fetch(`https://api.packagex.io/v1/events/subscribers/${subscriber_id}`, {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
}).then((res) => res.json());

const subscriber = response.data;

      

Retrieve Event Subscriber

GET
`/v1/events/subscribers/:subscriber`

Retrieve a single subscriber using its id. Requires the notifications:read scope.

js
        const response = await fetch(`https://api.packagex.io/v1/events/subscribers/${subscriber_id}`, {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const subscriber = response.data;

      

List Event Subscribers

GET
`/v1/events/subscribers`

Retrieve all event subscribers for your organization. Requires the notifications:read scope. Supports filtering by status, channel, destination, events, created_at, and updated_at, and ordering by created_at, updated_at, status, or destination.

js
        const response = await fetch("https://api.packagex.io/v1/events/subscribers?status=active", {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const subscribers = response.data;
const pagination = response.pagination;

      

Delete Event Subscriber

DELETE
`/v1/events/subscribers/:subscriber`

Remove a subscriber using its id. Requires the notifications:write scope. This is the only way to stop delivery to an address - recipients cannot unsubscribe themselves.

js
        await fetch(`https://api.packagex.io/v1/events/subscribers/${subscriber_id}`, {
  method: "DELETE",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
});