# Webhooks

URL: https://submitator.com/docs/api/webhooks

> Events pushed to your HTTPS endpoint as they happen.

Events pushed to your HTTPS endpoint as they happen. Each delivery is a POST with the event as JSON, signed as Standard Webhooks describes: verify it with an official Standard Webhooks library and your `whsec_` secret.

Answer with any 2xx within 10 seconds (5 seconds to connect); redirects are not followed. A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, then marked `failed`. An endpoint with no successful delivery for 72 hours and at least 10 failed events is disabled and you get an email; `PATCH` it with `enabled: true` to turn it back on.

Deliveries can arrive out of order. Compare `data.previous_status`, or read the object again, before you act on an older event.

## List webhook endpoints

`GET https://api.submitator.com/v1/webhook_endpoints`

Returns your webhook endpoints in the key's mode, newest first. Secrets are not included.

| Parameter | In | Required | What it is |
| --- | --- | --- | --- |
| `cursor` | query | No | `next_cursor` from the previous page of the same list, as you received it. Leave it out for the first page. |
| `limit` | query | No | How many items to return, 1 to 100. Defaults to 20. |

Response 200:

```json
{
  "object": "list",
  "data": [
    {
      "object": "webhook_endpoint",
      "id": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
      "livemode": false,
      "url": "https://hooks.northlight.test/listings",
      "description": "Northlight CRM sync",
      "events": [
        "listing.*",
        "project.*"
      ],
      "enabled": true,
      "disabled_reason": null,
      "secret": null,
      "previous_secret_expires_at": null,
      "delivery_summary": {
        "last_attempted_at": "2026-10-08T14:17:02Z",
        "last_http_status": 200,
        "attempted_7d": 148,
        "succeeded_7d": 147
      },
      "created_at": "2026-10-06T12:00:04Z",
      "updated_at": "2026-10-06T12:00:04Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

Typical errors: [`invalid_cursor`](https://submitator.com/docs/errors#invalid_cursor), [`rate_limited`](https://submitator.com/docs/errors#rate_limited).

## Add a webhook endpoint

`POST https://api.submitator.com/v1/webhook_endpoints`

Registers an HTTPS URL that receives events. The response holds the signing `secret`, shown this once only. A new endpoint receives events created after it, not the ones before.

| Parameter | In | Required | What it is |
| --- | --- | --- | --- |
| `Idempotency-Key` | header | No | A unique string, such as a UUID, that makes a retry safe. A success is kept for 24 hours: a repeat with the same key and body returns it again with `Idempotent-Replayed: true`, and the same key with another body returns 422 `idempotency_key_reused`. A repeat while the first request runs returns 409 `idempotency_key_in_use`. An error changes nothing, so a repeat after one runs again. |

| Body field | Type | Required | What it is |
| --- | --- | --- | --- |
| `url` | string | Yes | A public `https` URL without a user name or password, up to 2,048 characters. Deliveries do not follow redirects, so use the final URL. |
| `events` | array | No | The event types to receive, or groups such as `listing.*`. Defaults to `["*"]`, every type. |
| `description` | string or null | No | Your note about the endpoint, up to 200 characters. |
| `enabled` | boolean | No | Defaults to `true`. Set `false` to create it turned off. |

Request:

```json
{
  "url": "https://hooks.northlight.test/listings",
  "events": [
    "listing.*",
    "project.*"
  ],
  "description": "Northlight CRM sync"
}
```

Response 201:

```json
{
  "object": "webhook_endpoint",
  "id": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
  "livemode": false,
  "url": "https://hooks.northlight.test/listings",
  "description": "Northlight CRM sync",
  "events": [
    "listing.*",
    "project.*"
  ],
  "enabled": true,
  "disabled_reason": null,
  "secret": "whsec_9laRgOMT0C3d8Je6FbArCUY0bGJM+r2emaNUGJCH+aw=",
  "previous_secret_expires_at": null,
  "delivery_summary": {
    "last_attempted_at": null,
    "last_http_status": null,
    "attempted_7d": 0,
    "succeeded_7d": 0
  },
  "created_at": "2026-10-06T12:00:04Z",
  "updated_at": "2026-10-06T12:00:04Z"
}
```

Typical errors: [`endpoint_url_invalid`](https://submitator.com/docs/errors#endpoint_url_invalid), [`validation_failed`](https://submitator.com/docs/errors#validation_failed), [`quota_exceeded`](https://submitator.com/docs/errors#quota_exceeded).

## Read a webhook endpoint

`GET https://api.submitator.com/v1/webhook_endpoints/{webhook_endpoint_id}`

Returns one endpoint with a summary of its deliveries in the last 7 days. The secret is not included.

| Parameter | In | Required | What it is |
| --- | --- | --- | --- |
| `webhook_endpoint_id` | path | Yes | The webhook endpoint's id. |

Response 200:

```json
{
  "object": "webhook_endpoint",
  "id": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
  "livemode": false,
  "url": "https://hooks.northlight.test/listings",
  "description": "Northlight CRM sync",
  "events": [
    "listing.*",
    "project.*"
  ],
  "enabled": true,
  "disabled_reason": null,
  "secret": null,
  "previous_secret_expires_at": null,
  "delivery_summary": {
    "last_attempted_at": "2026-10-08T14:17:02Z",
    "last_http_status": 200,
    "attempted_7d": 148,
    "succeeded_7d": 147
  },
  "created_at": "2026-10-06T12:00:04Z",
  "updated_at": "2026-10-06T12:00:04Z"
}
```

Typical errors: [`not_found`](https://submitator.com/docs/errors#not_found), [`rate_limited`](https://submitator.com/docs/errors#rate_limited).

## Update a webhook endpoint

`PATCH https://api.submitator.com/v1/webhook_endpoints/{webhook_endpoint_id}`

Changes the URL, the event filter or the description, or turns the endpoint off and on. Setting `enabled` to `true` also turns on an endpoint that was disabled after failed deliveries.

| Parameter | In | Required | What it is |
| --- | --- | --- | --- |
| `webhook_endpoint_id` | path | Yes | The webhook endpoint's id. |

| Body field | Type | Required | What it is |
| --- | --- | --- | --- |
| `url` | string | No | A public `https` URL without a user name or password, up to 2,048 characters. |
| `events` | array | No | The event types to receive, or groups such as `listing.*`. Replaces the whole list. |
| `description` | string or null | No | Your note about the endpoint, up to 200 characters. |
| `enabled` | boolean | No | `false` turns it off, `true` turns it on again, including after it was disabled for failing. |

Request:

```json
{
  "events": [
    "listing.live",
    "listing.not_accepted",
    "project.*"
  ]
}
```

Response 200:

```json
{
  "object": "webhook_endpoint",
  "id": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
  "livemode": false,
  "url": "https://hooks.northlight.test/listings",
  "description": "Northlight CRM sync",
  "events": [
    "listing.live",
    "listing.not_accepted",
    "project.*"
  ],
  "enabled": true,
  "disabled_reason": null,
  "secret": null,
  "previous_secret_expires_at": null,
  "delivery_summary": {
    "last_attempted_at": "2026-10-08T14:17:02Z",
    "last_http_status": 200,
    "attempted_7d": 148,
    "succeeded_7d": 147
  },
  "created_at": "2026-10-06T12:00:04Z",
  "updated_at": "2026-10-08T15:25:40Z"
}
```

Typical errors: [`endpoint_url_invalid`](https://submitator.com/docs/errors#endpoint_url_invalid), [`validation_failed`](https://submitator.com/docs/errors#validation_failed), [`not_found`](https://submitator.com/docs/errors#not_found).

## Delete a webhook endpoint

`DELETE https://api.submitator.com/v1/webhook_endpoints/{webhook_endpoint_id}`

Deletes the endpoint at once. Deliveries still waiting for it are dropped; the events stay in `GET /events`.

| Parameter | In | Required | What it is |
| --- | --- | --- | --- |
| `webhook_endpoint_id` | path | Yes | The webhook endpoint's id. |

Response 200:

```json
{
  "object": "webhook_endpoint",
  "id": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
  "livemode": false,
  "deleted": true
}
```

Typical errors: [`not_found`](https://submitator.com/docs/errors#not_found), [`permission_denied`](https://submitator.com/docs/errors#permission_denied).

## Roll the signing secret

`POST https://api.submitator.com/v1/webhook_endpoints/{webhook_endpoint_id}/rotate_secret`

Issues a new signing secret, shown in this response only. For the next 24 hours each delivery carries 2 signatures, one per secret, so you can switch your code without missing an event.

| Parameter | In | Required | What it is |
| --- | --- | --- | --- |
| `webhook_endpoint_id` | path | Yes | The webhook endpoint's id. |

Response 200:

```json
{
  "object": "webhook_endpoint",
  "id": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
  "livemode": false,
  "url": "https://hooks.northlight.test/listings",
  "description": "Northlight CRM sync",
  "events": [
    "listing.*",
    "project.*"
  ],
  "enabled": true,
  "disabled_reason": null,
  "secret": "whsec_BFXycpAAK/Ay+ue0CxyVr5JQrZILyVqHTkjpX3GtfmY=",
  "previous_secret_expires_at": "2026-10-09T15:30:00Z",
  "delivery_summary": {
    "last_attempted_at": "2026-10-08T14:17:02Z",
    "last_http_status": 200,
    "attempted_7d": 148,
    "succeeded_7d": 147
  },
  "created_at": "2026-10-06T12:00:04Z",
  "updated_at": "2026-10-08T15:30:00Z"
}
```

Typical errors: [`not_found`](https://submitator.com/docs/errors#not_found), [`permission_denied`](https://submitator.com/docs/errors#permission_denied).

## Send a test event

`POST https://api.submitator.com/v1/webhook_endpoints/{webhook_endpoint_id}/test`

Sends a `ping` event to the endpoint now, whatever its `events` filter and even when it is disabled. Use it to check your signature code; the delivery shows up in the endpoint's deliveries.

| Parameter | In | Required | What it is |
| --- | --- | --- | --- |
| `webhook_endpoint_id` | path | Yes | The webhook endpoint's id. |

Response 202:

```json
{
  "object": "webhook_delivery",
  "id": "wd_0Tp4Kx9Wq2Lt7Vb3Rz8NmF",
  "livemode": false,
  "endpoint": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
  "event": "evt_7Aa2Kq8Wx3Lt9Vb5Rz1NdM",
  "event_type": "ping",
  "status": "pending",
  "attempts": 0,
  "next_attempt_at": "2026-10-06T12:01:15Z",
  "last_attempt": null,
  "created_at": "2026-10-06T12:01:15Z"
}
```

Typical errors: [`not_found`](https://submitator.com/docs/errors#not_found), [`rate_limited`](https://submitator.com/docs/errors#rate_limited).

## List deliveries

`GET https://api.submitator.com/v1/webhook_endpoints/{webhook_endpoint_id}/deliveries`

Returns the deliveries to one endpoint, newest first, with the outcome of the last attempt. Filter by `status` to find the ones that are still retrying or have failed.

| Parameter | In | Required | What it is |
| --- | --- | --- | --- |
| `webhook_endpoint_id` | path | Yes | The webhook endpoint's id. |
| `status` | query | No | Only deliveries in this status. |
| `cursor` | query | No | `next_cursor` from the previous page of the same list, as you received it. Leave it out for the first page. |
| `limit` | query | No | How many items to return, 1 to 100. Defaults to 20. |

Response 200:

```json
{
  "object": "list",
  "data": [
    {
      "object": "webhook_delivery",
      "id": "wd_1Bq7Xm3Kt9Lw5Rv2Nz8HdC",
      "livemode": false,
      "endpoint": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
      "event": "evt_5Rt8Wq2Lm7Xn3Kb9Vz1PdJ",
      "event_type": "listing.live",
      "status": "succeeded",
      "attempts": 1,
      "next_attempt_at": null,
      "last_attempt": {
        "attempted_at": "2026-10-08T14:17:02Z",
        "http_status": 200,
        "duration_ms": 182,
        "error": null
      },
      "created_at": "2026-10-08T14:17:01Z"
    },
    {
      "object": "webhook_delivery",
      "id": "wd_3Cr8Kx2Wq7Lt4Vb9Nz1HmP",
      "livemode": false,
      "endpoint": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
      "event": "evt_6Lr3Kx8Wq2Nt7Vb1Rz5MpA",
      "event_type": "listing.action_required",
      "status": "pending",
      "attempts": 5,
      "next_attempt_at": "2026-10-08T17:49:31Z",
      "last_attempt": {
        "attempted_at": "2026-10-08T11:49:31Z",
        "http_status": null,
        "duration_ms": 10000,
        "error": "timeout"
      },
      "created_at": "2026-10-08T09:13:30Z"
    }
  ],
  "has_more": true,
  "next_cursor": "cur_Dl5Kx2Wq8Lt3Vb7N"
}
```

Typical errors: [`not_found`](https://submitator.com/docs/errors#not_found), [`invalid_cursor`](https://submitator.com/docs/errors#invalid_cursor).

## Retry a delivery

`POST https://api.submitator.com/v1/webhook_deliveries/{webhook_delivery_id}/retry`

Sends a delivery again now, whatever its status, and resets nothing else. The endpoint must be enabled: a disabled endpoint returns 422 `validation_failed`.

| Parameter | In | Required | What it is |
| --- | --- | --- | --- |
| `webhook_delivery_id` | path | Yes | The delivery's id. |

Response 202:

```json
{
  "object": "webhook_delivery",
  "id": "wd_3Cr8Kx2Wq7Lt4Vb9Nz1HmP",
  "livemode": false,
  "endpoint": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
  "event": "evt_6Lr3Kx8Wq2Nt7Vb1Rz5MpA",
  "event_type": "listing.action_required",
  "status": "pending",
  "attempts": 5,
  "next_attempt_at": "2026-10-08T15:31:10Z",
  "last_attempt": {
    "attempted_at": "2026-10-08T11:49:31Z",
    "http_status": null,
    "duration_ms": 10000,
    "error": "timeout"
  },
  "created_at": "2026-10-08T09:13:30Z"
}
```

Typical errors: [`not_found`](https://submitator.com/docs/errors#not_found), [`validation_failed`](https://submitator.com/docs/errors#validation_failed).

## A project launched

The `project.launched` event, sent as a POST to your endpoint.

Sent once when a project launches; the new listings send no events of their own. `data.listings_created` counts them, all in `pending`, and `data.previous_status` is `draft`.

Body:

```json
{
  "object": "event",
  "id": "evt_3Km7Qx2Wr9Lt5Vb8Nz1HcP",
  "type": "project.launched",
  "created_at": "2026-10-03T14:05:41Z",
  "livemode": false,
  "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
  "data": {
    "object": {
      "object": "project",
      "id": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
      "livemode": false,
      "status": "in_progress",
      "hold_reason": null,
      "action": null,
      "url": "https://ledgerly.test",
      "name": "Ledgerly",
      "maker_name": "Maya Chen",
      "contact_email": "founder@ledgerly.test",
      "tagline": "Double-entry bookkeeping for solo founders",
      "short_description": "Ledgerly keeps the books for one-person companies: bank sync, invoices and a year-end pack for your accountant.",
      "long_description": null,
      "category": "saas",
      "pricing_model": "freemium",
      "price_amount_cents": 1200,
      "price_period": "monthly",
      "launch_date": null,
      "twitter_url": null,
      "competitors": [],
      "tags": [
        "accounting",
        "bookkeeping",
        "invoicing"
      ],
      "audience": {
        "b2b": false,
        "ai": false,
        "directory": false
      },
      "promo_code": null,
      "promo_description": null,
      "logo": {
        "object": "image",
        "id": "img_5Mx2Kq8Wt3Lr9Vb7Nz1HdQ",
        "status": "ready",
        "url": "https://feed.example.net/a/Lq7Wx2Km.r9Tz4",
        "error": null
      },
      "screenshots": [
        {
          "object": "image",
          "id": "img_2Kr9Lx3Wq7Mt5Vb8Nz1HcF",
          "status": "ready",
          "url": "https://feed.example.net/a/Sv3Nk8Qp.m2Wx7",
          "error": null
        }
      ],
      "autofill": {
        "status": "complete",
        "name_source": "site",
        "filled": [
          "name",
          "tagline",
          "short_description"
        ],
        "image_candidates": [
          {
            "url": "https://ledgerly.test/apple-touch-icon.png",
            "suggested_as": "logo",
            "width": 180,
            "height": 180
          },
          {
            "url": "https://ledgerly.test/og-image.png",
            "suggested_as": "screenshot",
            "width": 1200,
            "height": 630
          }
        ]
      },
      "readiness": {
        "ready": true,
        "missing": []
      },
      "domain_rating": 34,
      "badge": {
        "status": "not_checked",
        "checked_at": null
      },
      "auto_replace": true,
      "excluded_directories": [],
      "allowance": {
        "total": 100,
        "used": 0,
        "reserved": 87,
        "available": 13
      },
      "progress": {
        "total": 87,
        "pending": 87,
        "in_progress": 0,
        "action_required": 0,
        "submitted": 0,
        "live": 0,
        "not_accepted": 0,
        "cancelled": 0
      },
      "external_id": "crm_8812",
      "metadata": {
        "account_owner": "Priya",
        "crm_stage": "onboarding"
      },
      "created_at": "2026-10-03T13:58:12Z",
      "updated_at": "2026-10-03T14:05:40Z",
      "launched_at": "2026-10-03T14:05:40Z"
    },
    "previous_status": "draft",
    "listings_created": 87
  }
}
```

## Directories were added

The `project.listings_added` event, sent as a POST to your endpoint.

Sent when directories are added to a launched project, by `POST /projects/{project_id}/listings` or by auto-replace. `data.listings_added` counts the new listings, all in `pending`. The project's status does not change, so `data.previous_status` is `null`.

Body:

```json
{
  "object": "event",
  "id": "evt_4Ns2Kw9Lq5Xt8Vb3Rz7MdF",
  "type": "project.listings_added",
  "created_at": "2026-10-04T10:02:15Z",
  "livemode": false,
  "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
  "data": {
    "object": {
      "object": "project",
      "id": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
      "livemode": false,
      "status": "in_progress",
      "hold_reason": null,
      "action": null,
      "url": "https://ledgerly.test",
      "name": "Ledgerly",
      "maker_name": "Maya Chen",
      "contact_email": "founder@ledgerly.test",
      "tagline": "Double-entry bookkeeping for solo founders",
      "short_description": "Ledgerly keeps the books for one-person companies: bank sync, invoices and a year-end pack for your accountant.",
      "long_description": null,
      "category": "saas",
      "pricing_model": "freemium",
      "price_amount_cents": 1200,
      "price_period": "monthly",
      "launch_date": null,
      "twitter_url": null,
      "competitors": [],
      "tags": [
        "accounting",
        "bookkeeping",
        "invoicing"
      ],
      "audience": {
        "b2b": false,
        "ai": false,
        "directory": false
      },
      "promo_code": null,
      "promo_description": null,
      "logo": {
        "object": "image",
        "id": "img_5Mx2Kq8Wt3Lr9Vb7Nz1HdQ",
        "status": "ready",
        "url": "https://feed.example.net/a/Lq7Wx2Km.r9Tz4",
        "error": null
      },
      "screenshots": [
        {
          "object": "image",
          "id": "img_2Kr9Lx3Wq7Mt5Vb8Nz1HcF",
          "status": "ready",
          "url": "https://feed.example.net/a/Sv3Nk8Qp.m2Wx7",
          "error": null
        }
      ],
      "autofill": {
        "status": "complete",
        "name_source": "site",
        "filled": [
          "name",
          "tagline",
          "short_description"
        ],
        "image_candidates": [
          {
            "url": "https://ledgerly.test/apple-touch-icon.png",
            "suggested_as": "logo",
            "width": 180,
            "height": 180
          },
          {
            "url": "https://ledgerly.test/og-image.png",
            "suggested_as": "screenshot",
            "width": 1200,
            "height": 630
          }
        ]
      },
      "readiness": {
        "ready": true,
        "missing": []
      },
      "domain_rating": 34,
      "badge": {
        "status": "installed",
        "checked_at": "2026-10-04T09:58:03Z"
      },
      "auto_replace": true,
      "excluded_directories": [],
      "allowance": {
        "total": 100,
        "used": 53,
        "reserved": 39,
        "available": 8
      },
      "progress": {
        "total": 96,
        "pending": 9,
        "in_progress": 30,
        "action_required": 0,
        "submitted": 31,
        "live": 22,
        "not_accepted": 3,
        "cancelled": 1
      },
      "external_id": "crm_8812",
      "metadata": {
        "account_owner": "Priya",
        "crm_stage": "onboarding"
      },
      "created_at": "2026-10-03T13:58:12Z",
      "updated_at": "2026-10-04T10:02:15Z",
      "launched_at": "2026-10-03T14:05:40Z"
    },
    "previous_status": null,
    "listings_added": 9
  }
}
```

## A project is moving again

The `project.in_progress` event, sent as a POST to your endpoint.

Sent when a project returns to `in_progress` from `action_required`, `on_hold` or `completed`. A launch does not send it: the project starts in `in_progress` with `project.launched`.

Body:

```json
{
  "object": "event",
  "id": "evt_2Zp5Kx1Wq9Lt4Vb8Nr3McU",
  "type": "project.in_progress",
  "created_at": "2026-10-06T16:40:09Z",
  "livemode": false,
  "project": "prj_1Nc7Vd3Kq8Zr5Tx2Wm9LbH",
  "data": {
    "object": {
      "object": "project",
      "id": "prj_1Nc7Vd3Kq8Zr5Tx2Wm9LbH",
      "livemode": false,
      "status": "in_progress",
      "hold_reason": null,
      "action": null,
      "url": "https://quotafy.test",
      "name": "Quotafy",
      "maker_name": "Tom Okafor",
      "contact_email": "sales@quotafy.test",
      "tagline": "Quotes your clients sign in one click",
      "short_description": "Quotafy turns a price list into quotes that clients accept and sign online, with reminders and a deposit link.",
      "long_description": null,
      "category": "saas",
      "pricing_model": "paid",
      "price_amount_cents": 2900,
      "price_period": "monthly",
      "launch_date": "2026-08-20",
      "twitter_url": "https://x.com/quotafy",
      "competitors": [
        {
          "name": "Bidsheet",
          "url": "https://bidsheet.test"
        }
      ],
      "tags": [
        "quotes",
        "proposals",
        "e-signature"
      ],
      "audience": {
        "b2b": true,
        "ai": false,
        "directory": false
      },
      "promo_code": "QUOTE30",
      "promo_description": "30% off the first 3 months",
      "logo": {
        "object": "image",
        "id": "img_1Qa8Kx3Wm7Lt2Vb9Rz5NcE",
        "status": "ready",
        "url": "https://feed.example.net/a/Qf2Lm8Wx.k5Tp3",
        "error": null
      },
      "screenshots": [
        {
          "object": "image",
          "id": "img_4Qb2Kw9Lx5Mt8Vb3Rz7NdG",
          "status": "ready",
          "url": "https://feed.example.net/a/Qg9Nt4Vb.z7Kr2",
          "error": null
        }
      ],
      "autofill": {
        "status": "complete",
        "name_source": "input",
        "filled": [],
        "image_candidates": []
      },
      "readiness": {
        "ready": true,
        "missing": []
      },
      "domain_rating": 12,
      "badge": {
        "status": "not_checked",
        "checked_at": null
      },
      "auto_replace": true,
      "excluded_directories": [
        "dir_3Vb8Qm2Xk6Lt9Wr4Nc1ZpG"
      ],
      "allowance": {
        "total": 100,
        "used": 40,
        "reserved": 52,
        "available": 8
      },
      "progress": {
        "total": 92,
        "pending": 30,
        "in_progress": 22,
        "action_required": 0,
        "submitted": 25,
        "live": 15,
        "not_accepted": 0,
        "cancelled": 0
      },
      "external_id": "crm_8907",
      "metadata": {},
      "created_at": "2026-10-05T09:40:18Z",
      "updated_at": "2026-10-06T16:40:09Z",
      "launched_at": "2026-10-05T10:12:07Z"
    },
    "previous_status": "on_hold"
  }
}
```

## A project needs you or your client

The `project.action_required` event, sent as a POST to your endpoint.

Sent when a project needs someone to act. `data.object.action` says what to do and who: `waiting_on` is `client` or `agency`, and `client_message` is ready to forward.

Body:

```json
{
  "object": "event",
  "id": "evt_6Mz4Kq9Wr3Lt8Vb2Nx6HcE",
  "type": "project.action_required",
  "created_at": "2026-10-08T09:13:30Z",
  "livemode": false,
  "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
  "data": {
    "object": {
      "object": "project",
      "id": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
      "livemode": false,
      "status": "action_required",
      "hold_reason": null,
      "action": {
        "code": "install_badge",
        "waiting_on": "client",
        "client_message": "Please add the directory badges block to https://ledgerly.test. 9 directories list Ledgerly once their badges show on the site."
      },
      "url": "https://ledgerly.test",
      "name": "Ledgerly",
      "maker_name": "Maya Chen",
      "contact_email": "founder@ledgerly.test",
      "tagline": "Double-entry bookkeeping for solo founders",
      "short_description": "Ledgerly keeps the books for one-person companies: bank sync, invoices and a year-end pack for your accountant.",
      "long_description": null,
      "category": "saas",
      "pricing_model": "freemium",
      "price_amount_cents": 1200,
      "price_period": "monthly",
      "launch_date": null,
      "twitter_url": null,
      "competitors": [],
      "tags": [
        "accounting",
        "bookkeeping",
        "invoicing"
      ],
      "audience": {
        "b2b": false,
        "ai": false,
        "directory": false
      },
      "promo_code": null,
      "promo_description": null,
      "logo": {
        "object": "image",
        "id": "img_5Mx2Kq8Wt3Lr9Vb7Nz1HdQ",
        "status": "ready",
        "url": "https://feed.example.net/a/Lq7Wx2Km.r9Tz4",
        "error": null
      },
      "screenshots": [
        {
          "object": "image",
          "id": "img_2Kr9Lx3Wq7Mt5Vb8Nz1HcF",
          "status": "ready",
          "url": "https://feed.example.net/a/Sv3Nk8Qp.m2Wx7",
          "error": null
        }
      ],
      "autofill": {
        "status": "complete",
        "name_source": "site",
        "filled": [
          "name",
          "tagline",
          "short_description"
        ],
        "image_candidates": [
          {
            "url": "https://ledgerly.test/apple-touch-icon.png",
            "suggested_as": "logo",
            "width": 180,
            "height": 180
          },
          {
            "url": "https://ledgerly.test/og-image.png",
            "suggested_as": "screenshot",
            "width": 1200,
            "height": 630
          }
        ]
      },
      "readiness": {
        "ready": true,
        "missing": []
      },
      "domain_rating": 34,
      "badge": {
        "status": "not_found",
        "checked_at": "2026-10-08T09:12:44Z"
      },
      "auto_replace": true,
      "excluded_directories": [],
      "allowance": {
        "total": 100,
        "used": 78,
        "reserved": 14,
        "available": 8
      },
      "progress": {
        "total": 96,
        "pending": 0,
        "in_progress": 5,
        "action_required": 9,
        "submitted": 9,
        "live": 69,
        "not_accepted": 3,
        "cancelled": 1
      },
      "external_id": "crm_8812",
      "metadata": {
        "account_owner": "Priya",
        "crm_stage": "onboarding"
      },
      "created_at": "2026-10-03T13:58:12Z",
      "updated_at": "2026-10-08T09:13:30Z",
      "launched_at": "2026-10-03T14:05:40Z"
    },
    "previous_status": "in_progress"
  }
}
```

## Work on a project paused

The `project.on_hold` event, sent as a POST to your endpoint.

Sent when work on a project pauses. `data.object.hold_reason` is `review` while we review the project, which clears without action from you, or `paused` when we paused it; contact support about a pause.

Body:

```json
{
  "object": "event",
  "id": "evt_1Qx9Kr4Wm8Lt3Vb7Nz2HdY",
  "type": "project.on_hold",
  "created_at": "2026-10-06T08:00:21Z",
  "livemode": false,
  "project": "prj_1Nc7Vd3Kq8Zr5Tx2Wm9LbH",
  "data": {
    "object": {
      "object": "project",
      "id": "prj_1Nc7Vd3Kq8Zr5Tx2Wm9LbH",
      "livemode": false,
      "status": "on_hold",
      "hold_reason": "review",
      "action": null,
      "url": "https://quotafy.test",
      "name": "Quotafy",
      "maker_name": "Tom Okafor",
      "contact_email": "sales@quotafy.test",
      "tagline": "Quotes your clients sign in one click",
      "short_description": "Quotafy turns a price list into quotes that clients accept and sign online, with reminders and a deposit link.",
      "long_description": null,
      "category": "saas",
      "pricing_model": "paid",
      "price_amount_cents": 2900,
      "price_period": "monthly",
      "launch_date": "2026-08-20",
      "twitter_url": "https://x.com/quotafy",
      "competitors": [
        {
          "name": "Bidsheet",
          "url": "https://bidsheet.test"
        }
      ],
      "tags": [
        "quotes",
        "proposals",
        "e-signature"
      ],
      "audience": {
        "b2b": true,
        "ai": false,
        "directory": false
      },
      "promo_code": "QUOTE30",
      "promo_description": "30% off the first 3 months",
      "logo": {
        "object": "image",
        "id": "img_1Qa8Kx3Wm7Lt2Vb9Rz5NcE",
        "status": "ready",
        "url": "https://feed.example.net/a/Qf2Lm8Wx.k5Tp3",
        "error": null
      },
      "screenshots": [
        {
          "object": "image",
          "id": "img_4Qb2Kw9Lx5Mt8Vb3Rz7NdG",
          "status": "ready",
          "url": "https://feed.example.net/a/Qg9Nt4Vb.z7Kr2",
          "error": null
        }
      ],
      "autofill": {
        "status": "complete",
        "name_source": "input",
        "filled": [],
        "image_candidates": []
      },
      "readiness": {
        "ready": true,
        "missing": []
      },
      "domain_rating": 12,
      "badge": {
        "status": "not_checked",
        "checked_at": null
      },
      "auto_replace": true,
      "excluded_directories": [
        "dir_3Vb8Qm2Xk6Lt9Wr4Nc1ZpG"
      ],
      "allowance": {
        "total": 100,
        "used": 40,
        "reserved": 52,
        "available": 8
      },
      "progress": {
        "total": 92,
        "pending": 40,
        "in_progress": 12,
        "action_required": 0,
        "submitted": 25,
        "live": 15,
        "not_accepted": 0,
        "cancelled": 0
      },
      "external_id": "crm_8907",
      "metadata": {},
      "created_at": "2026-10-05T09:40:18Z",
      "updated_at": "2026-10-06T08:00:21Z",
      "launched_at": "2026-10-05T10:12:07Z"
    },
    "previous_status": "in_progress"
  }
}
```

## A project completed

The `project.completed` event, sent as a POST to your endpoint.

Sent when no listing is `pending`, `in_progress` or `action_required` and auto-replace has nothing to add. Listings in `submitted` can still go live and send `listing.live`.

Body:

```json
{
  "object": "event",
  "id": "evt_3Vw7Kq2Lx8Mt5Nb1Rz9HdT",
  "type": "project.completed",
  "created_at": "2026-10-07T18:22:47Z",
  "livemode": false,
  "project": "prj_3Ac9Kx4Wq7Lt2Vb8Rz5NmD",
  "data": {
    "object": {
      "object": "project",
      "id": "prj_3Ac9Kx4Wq7Lt2Vb8Rz5NmD",
      "livemode": false,
      "status": "completed",
      "hold_reason": null,
      "action": null,
      "url": "https://acme.test",
      "name": "Acme CRM",
      "maker_name": "Lena Park",
      "contact_email": "hello@acme.test",
      "tagline": "The CRM for teams of five",
      "short_description": "Acme CRM keeps contacts, deals and follow-ups in one shared list, with a weekly digest for the whole team.",
      "long_description": null,
      "category": "saas",
      "pricing_model": "freemium",
      "price_amount_cents": 900,
      "price_period": "monthly",
      "launch_date": null,
      "twitter_url": null,
      "competitors": [],
      "tags": [
        "crm",
        "sales"
      ],
      "audience": {
        "b2b": true,
        "ai": false,
        "directory": false
      },
      "promo_code": null,
      "promo_description": null,
      "logo": {
        "object": "image",
        "id": "img_3Ad7Kx2Wq8Lt4Vb9Rz1NmH",
        "status": "ready",
        "url": "https://feed.example.net/a/Ac6Rk1Wn.p4Lx8",
        "error": null
      },
      "screenshots": [
        {
          "object": "image",
          "id": "img_0Ae5Kw1Lq9Xt3Vb7Rz2McJ",
          "status": "ready",
          "url": "https://feed.example.net/a/Ad3Mq7Zt.v9Kc1",
          "error": null
        }
      ],
      "autofill": {
        "status": "complete",
        "name_source": "site",
        "filled": [
          "name",
          "tagline"
        ],
        "image_candidates": []
      },
      "readiness": {
        "ready": true,
        "missing": []
      },
      "domain_rating": 51,
      "badge": {
        "status": "installed",
        "checked_at": "2026-10-03T15:08:30Z"
      },
      "auto_replace": false,
      "excluded_directories": [],
      "allowance": {
        "total": 100,
        "used": 85,
        "reserved": 0,
        "available": 15
      },
      "progress": {
        "total": 88,
        "pending": 0,
        "in_progress": 0,
        "action_required": 0,
        "submitted": 3,
        "live": 82,
        "not_accepted": 2,
        "cancelled": 1
      },
      "external_id": "crm_8650",
      "metadata": {},
      "created_at": "2026-10-03T15:01:44Z",
      "updated_at": "2026-10-07T18:22:47Z",
      "launched_at": "2026-10-03T15:12:09Z"
    },
    "previous_status": "in_progress"
  }
}
```

## Work on a listing started

The `listing.in_progress` event, sent as a POST to your endpoint.

Sent when a listing leaves `pending`, or resumes after `action_required`. It is not sent again while the listing stays in progress.

Body:

```json
{
  "object": "event",
  "id": "evt_1Wd8Kq3Lx7Mt2Vb9Rz5NcG",
  "type": "listing.in_progress",
  "created_at": "2026-10-03T16:05:12Z",
  "livemode": false,
  "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
  "data": {
    "object": {
      "object": "listing",
      "id": "lst_5Wq9Lm2Xr7Kt3Vb8Nz1HcD",
      "livemode": false,
      "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
      "directory": {
        "id": "dir_6Pw3Kx9Ty2Rq7Lm1Zb5VcN",
        "name": "SaaSHub",
        "domain_rating": 61,
        "logo_url": "https://feed.example.net/a/h4Sw8Lp.q2Xn"
      },
      "status": "in_progress",
      "reason": null,
      "action": null,
      "live_url": null,
      "proof_url": null,
      "created_at": "2026-10-03T14:00:00Z",
      "submitted_at": null,
      "live_at": null
    },
    "previous_status": "pending"
  }
}
```

## A listing needs you or your client

The `listing.action_required` event, sent as a POST to your endpoint.

Sent when a listing waits on someone. `data.object.action` says what to do; for `install_badge`, forward `client_message` to your client and verify the badge once it is installed.

Body:

```json
{
  "object": "event",
  "id": "evt_6Lr3Kx8Wq2Nt7Vb1Rz5MpA",
  "type": "listing.action_required",
  "created_at": "2026-10-08T09:13:30Z",
  "livemode": false,
  "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
  "data": {
    "object": {
      "object": "listing",
      "id": "lst_2Tz6Kp9Wm3Qx8Lr5Vb1NcJ",
      "livemode": false,
      "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
      "directory": {
        "id": "dir_3Vb8Qm2Xk6Lt9Wr4Nc1ZpG",
        "name": "Uneed",
        "domain_rating": 58,
        "logo_url": "https://feed.example.net/a/u7Nd3Kc.w5Rb"
      },
      "status": "action_required",
      "reason": null,
      "action": {
        "code": "install_badge",
        "waiting_on": "client",
        "client_message": "Please add the directory badges block to https://ledgerly.test. Uneed lists Ledgerly once its badge shows on the site."
      },
      "live_url": null,
      "proof_url": null,
      "created_at": "2026-10-04T10:00:00Z",
      "submitted_at": null,
      "live_at": null
    },
    "previous_status": "in_progress"
  }
}
```

## A directory has the listing

The `listing.submitted` event, sent as a POST to your endpoint.

Sent when the directory has the listing and reviews it. Also sent, with `data.previous_status` `live`, in the rare case a live listing goes back to review.

Body:

```json
{
  "object": "event",
  "id": "evt_2Hq8Wn3Kx7Lt1Vb6Rz9McS",
  "type": "listing.submitted",
  "created_at": "2026-10-05T11:20:33Z",
  "livemode": false,
  "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
  "data": {
    "object": {
      "object": "listing",
      "id": "lst_5Wq9Lm2Xr7Kt3Vb8Nz1HcD",
      "livemode": false,
      "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
      "directory": {
        "id": "dir_6Pw3Kx9Ty2Rq7Lm1Zb5VcN",
        "name": "SaaSHub",
        "domain_rating": 61,
        "logo_url": "https://feed.example.net/a/h4Sw8Lp.q2Xn"
      },
      "status": "submitted",
      "reason": null,
      "action": null,
      "live_url": null,
      "proof_url": null,
      "created_at": "2026-10-03T14:00:00Z",
      "submitted_at": "2026-10-05T11:00:00Z",
      "live_at": null
    },
    "previous_status": "in_progress"
  }
}
```

## A listing is live

The `listing.live` event, sent as a POST to your endpoint.

Sent when the listing is published. `data.object.live_url` points to it, or is `null` when the listing was confirmed without a link. A listing can go live without a `listing.submitted` event first.

Body:

```json
{
  "object": "event",
  "id": "evt_5Rt8Wq2Lm7Xn3Kb9Vz1PdJ",
  "type": "listing.live",
  "created_at": "2026-10-08T14:17:01Z",
  "livemode": false,
  "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
  "data": {
    "object": {
      "object": "listing",
      "id": "lst_7Hk2Qn4Rt9Vx1Bm6Cz3LdK",
      "livemode": false,
      "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
      "directory": {
        "id": "dir_2Lm8Wq3Zk7Rt1Xc5Vb9NdF",
        "name": "BetaList",
        "domain_rating": 73,
        "logo_url": "https://feed.example.net/a/x1Lq7Rt.k9Zm"
      },
      "status": "live",
      "reason": null,
      "action": null,
      "live_url": "https://betalist.com/startups/ledgerly",
      "proof_url": "https://feed.example.net/a/Pf8Kq2Wm.t6Lx9",
      "created_at": "2026-10-03T14:00:00Z",
      "submitted_at": "2026-10-04T09:00:00Z",
      "live_at": "2026-10-08T14:00:00Z"
    },
    "previous_status": "submitted"
  }
}
```

## A directory did not list the product

The `listing.not_accepted` event, sent as a POST to your endpoint.

Sent when the directory declines the listing, cannot take it, or takes down a live listing. `data.object.reason` says which, and the allowance the listing held returns to the project.

Body:

```json
{
  "object": "event",
  "id": "evt_0Hm6Kx3Wq8Lt2Vb7Rz4NcR",
  "type": "listing.not_accepted",
  "created_at": "2026-10-06T07:41:55Z",
  "livemode": false,
  "project": "prj_3Ac9Kx4Wq7Lt2Vb8Rz5NmD",
  "data": {
    "object": {
      "object": "listing",
      "id": "lst_3Fp8Wx2Kq6Lm9Rt4Vb1NzD",
      "livemode": false,
      "project": "prj_3Ac9Kx4Wq7Lt2Vb8Rz5NmD",
      "directory": {
        "id": "dir_2Lm8Wq3Zk7Rt1Xc5Vb9NdF",
        "name": "BetaList",
        "domain_rating": 73,
        "logo_url": "https://feed.example.net/a/x1Lq7Rt.k9Zm"
      },
      "status": "not_accepted",
      "reason": "declined",
      "action": null,
      "live_url": null,
      "proof_url": null,
      "created_at": "2026-10-03T15:00:00Z",
      "submitted_at": "2026-10-04T08:00:00Z",
      "live_at": null
    },
    "previous_status": "submitted"
  }
}
```

## A listing was withdrawn

The `listing.cancelled` event, sent as a POST to your endpoint.

Sent when we withdraw a listing before it reaches the directory, with `reason` `withdrawn`. The allowance it held returns to the project.

Body:

```json
{
  "object": "event",
  "id": "evt_0Gn4Kw8Lq3Xt9Vb2Rz6MdQ",
  "type": "listing.cancelled",
  "created_at": "2026-10-03T18:03:26Z",
  "livemode": false,
  "project": "prj_3Ac9Kx4Wq7Lt2Vb8Rz5NmD",
  "data": {
    "object": {
      "object": "listing",
      "id": "lst_6Cv1Nb8Lz3Kx9Wq2Rt7MpH",
      "livemode": false,
      "project": "prj_3Ac9Kx4Wq7Lt2Vb8Rz5NmD",
      "directory": {
        "id": "dir_3Vb8Qm2Xk6Lt9Wr4Nc1ZpG",
        "name": "Uneed",
        "domain_rating": 58,
        "logo_url": "https://feed.example.net/a/u7Nd3Kc.w5Rb"
      },
      "status": "cancelled",
      "reason": "withdrawn",
      "action": null,
      "live_url": null,
      "proof_url": null,
      "created_at": "2026-10-03T15:00:00Z",
      "submitted_at": null,
      "live_at": null
    },
    "previous_status": "pending"
  }
}
```

## A report is ready

The `report.ready` event, sent as a POST to your endpoint.

Sent when a report has been built. Download it from `GET /reports/{report_id}/file` before `data.object.expires_at`, 7 days later.

Body:

```json
{
  "object": "event",
  "id": "evt_6Tk3Wq9Lx2Nt8Vb5Rz1MhK",
  "type": "report.ready",
  "created_at": "2026-10-08T15:02:41Z",
  "livemode": false,
  "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
  "data": {
    "object": {
      "object": "report",
      "id": "rep_6Hn2Wq8Lx3Kt9Vb5Rz1MdP",
      "livemode": false,
      "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
      "format": "pdf",
      "include_logins": false,
      "status": "ready",
      "file_name": "ledgerly-listings-2026-10-08.pdf",
      "created_at": "2026-10-08T15:02:10Z",
      "ready_at": "2026-10-08T15:02:41Z",
      "expires_at": "2026-10-15T15:02:41Z"
    },
    "previous_status": "building"
  }
}
```

## A report failed

The `report.failed` event, sent as a POST to your endpoint.

Sent when a report could not be built. Start a new one; if it fails again, contact support with the event id.

Body:

```json
{
  "object": "event",
  "id": "evt_6Uf8Kx2Wq7Lt3Vb9Rz4NcW",
  "type": "report.failed",
  "created_at": "2026-10-08T15:05:31Z",
  "livemode": false,
  "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
  "data": {
    "object": {
      "object": "report",
      "id": "rep_2Gk7Wn3Lq9Xt5Vb8Rz1MhC",
      "livemode": false,
      "project": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
      "format": "xlsx",
      "include_logins": false,
      "status": "failed",
      "file_name": null,
      "created_at": "2026-10-08T15:05:00Z",
      "ready_at": null,
      "expires_at": null
    },
    "previous_status": "building"
  }
}
```

## A test event

The `ping` event, sent as a POST to your endpoint.

Sent by `POST /webhook_endpoints/{webhook_endpoint_id}/test` and by the console's test button, to one endpoint only, whatever its `events` filter. `data.object` is the endpoint, `project` is `null`, and the event also appears in `GET /events`.

Body:

```json
{
  "object": "event",
  "id": "evt_7Aa2Kq8Wx3Lt9Vb5Rz1NdM",
  "type": "ping",
  "created_at": "2026-10-06T12:01:15Z",
  "livemode": false,
  "project": null,
  "data": {
    "object": {
      "object": "webhook_endpoint",
      "id": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
      "livemode": false,
      "url": "https://hooks.northlight.test/listings",
      "description": "Northlight CRM sync",
      "events": [
        "listing.*",
        "project.*"
      ],
      "enabled": true,
      "disabled_reason": null,
      "secret": null,
      "previous_secret_expires_at": null,
      "delivery_summary": {
        "last_attempted_at": null,
        "last_http_status": null,
        "attempted_7d": 0,
        "succeeded_7d": 0
      },
      "created_at": "2026-10-06T12:00:04Z",
      "updated_at": "2026-10-06T12:00:04Z"
    },
    "previous_status": null
  }
}
```
