# Events

Source: https://docs.pitchpointsolutions.com/api/webhooks/events/

**Beta**

## Event Topics

A topic names what happened. PPS publishes eight, in two families, and an endpoint may accept any subset of them for each product it declares.

| Topic | Fires when | `data.status` | Extra `data` ids |
|---|---|---|---|
| workflow.pending | A run enters processing, for the first time or on resuming after a failure. | `pending` | |
| workflow.updated | New data becomes available for the workflow: a run becomes ready, or a finding on one of its runs changes. | `ready` when a run became ready; the run's current status when it accompanies a finding change | |
| workflow.failed | A run fails. | `failed` | |
| finding.result_updated | A finding's result was overridden. The effective outcome is `resultOrOverriddenResult` on the finding. | the run's current status | `findingId` |
| finding.reviewed | A finding was cleared, by a reviewer (for example with `reviewed: true` on the edit call) or automatically. | the run's current status | `findingId` |
| finding.note_updated | A finding's review note was set, replaced or cleared. | the run's current status | `findingId` |
| finding.attachment_updated | An attachment was added to a finding, or an existing attachment's content or details changed. | the run's current status | `findingId`, `attachmentId` |
| finding.attachment_removed | An attachment was removed from a finding. Its content can no longer be fetched. | the run's current status | `findingId`, `attachmentId` |

Matching is exact and sensitive to case. `Workflow.Updated` is not `workflow.updated`, and any value outside the eight is rejected with a 400.

On a workflow topic, `data.status` agrees with the topic. On a finding topic it is whatever status the run holds at that moment, for example `pending`, `ready` or `complete`; it does not describe the finding. A [test delivery](https://docs.pitchpointsolutions.com/api/webhooks/events/#send-a-test-delivery) carries no `data.status` at all.

There is no completion event. None of the three workflow topics implies another follows, and none is terminal: a failed run can later announce `workflow.pending` and then `workflow.updated`.

### Finding events

A finding event says which finding changed and how. It fires only when something actually changed: a save that leaves the value as it was publishes nothing.

- `data.findingId` names the finding, in the same `fndg_`-prefixed form the Findings API publishes. Pass it exactly as you received it as the `findingId` of [Read a Workflow Finding](https://docs.pitchpointsolutions.com/api/findings/get_workflow_finding).
- `data.attachmentId` is carried only on the two attachment topics. It names the attachment in the same `jattmt_`-prefixed form the Findings API publishes as `attachmentId`. After `finding.attachment_removed` the attachment is gone, so the attachment routes answer 404 for it.
- `data.runId` is the run the finding belongs to, which can be an earlier run of the workflow than its latest one.

Every finding event is accompanied by its own `workflow.updated`: a separate event, with its own `eventId`, for the same `workflowId` and `runId`. They are not combined, so a change touching several findings, or several things on one finding, publishes one `workflow.updated` per finding event. An endpoint that accepts only `workflow.updated` therefore still hears that a finding changed, just not which one.

A reset, which returns a cleared finding to unreviewed (`reviewed: false`), publishes no finding topic. It arrives as a `workflow.updated` alone, with no `data.findingId`, so refetch the workflow's findings to see which one changed.

Consent is per topic. Accepting `workflow.updated` does not opt you into any finding topic, so an endpoint receives no finding events until it declares them.

An example `finding.reviewed` envelope:

```json
{
  "type": "finding.reviewed",
  "eventId": "msg_6e2b9f47-1c83-4a5d-b0e6-8d4f2a7c9153",
  "timestamp": "2026-08-28T16:02:47Z",
  "data": {
    "workflowId": "wfpop_7b3f1c9e-2a84-4d16-9f05-c3e8a1b47d92",
    "runId": "wfrunpop_7b3f1c9e-2a84-4d16-9f05-c3e8a1b47d92",
    "findingId": "fndg_3a9d6c1e-47b2-4f08-9e5d-2c7b1f0a8e64",
    "correlationId": "9c2e14a7-55d0-4b8e-a731-6f0d2b93e185",
    "serviceName": "ADV",
    "modelName": "ADV-120",
    "status": "complete"
  }
}
```

> **API Reference:** [Receiving Deliveries](https://docs.pitchpointsolutions.com/developer_guides/webhooks/receiving) covers what to do with each topic and why there is no completion event.

---

## Send A Test Delivery

Send a synthetic delivery to one endpoint and report what came back.

`POST /v1/webhooks/endpoints/{id}/test`

```bash
curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/webhooks/endpoints/4711/test \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "serviceName": "ADV",
    "modelName": "ADV-120",
    "eventType": "workflow.updated"
  }'
```

It is synchronous and makes one attempt only: the call blocks for the round trip and there is no retry. It can block for as long as your own server takes to answer, up to about 45 seconds — a connect timeout of 15 seconds plus a read timeout of 30 seconds — before PPS gives up and reports the failure in the body. Set your client's timeout accordingly.

It is the real delivery path, not a simulation of it. It uses the same signing, the same headers, the same destination-safety validation, the same check that the destination domain is authorized, and the same HTTP client that production deliveries use.

You choose a selector and PPS builds the envelope. You can never supply payload bytes. PPS signatures verify against a published public key everywhere, so accepting caller-supplied bytes would make this API a signing oracle.

#### Header Properties

| Property | Value | Required? |
|----------|-------|-----------|
| Authorization | Bearer your_access_token_here | true |
| Content-Type | application/json | true |

#### Path Parameters

| Property | Description | Type | Required |
|----------|-------------|------|----------|
| id | The endpoint's id. | number | yes |

#### Request Data Properties

| Property | Description | Type | Required |
|----------|-------------|------|----------|
| serviceName | The product's service name, matching what you name when starting a workflow and the `acceptedEventTypes` entry you registered. | string | yes |
| modelName | The product's model name, paired with `serviceName` to identify the product exactly as `acceptedEventTypes` does. | string | yes |
| eventType | Which topic to synthesize, any of the eight. It must be one this endpoint has declared for that product; any other value is rejected with a 400, checked after authorization. | string | yes |

The `eventType` must be one this endpoint has declared for this product, or the test is refused with a 400. PPS also checks that something routes that product and topic to the endpoint, and refuses with a 400 when nothing does. An endpoint always receives its own org unit's events, so that second check passes for any endpoint that has declared the topic, and you need no subscription grant to test. Both checks run after authorization, so a caller who may not see this endpoint cannot use either error to learn what it accepts.

A disabled endpoint is still testable and surfaces as a `warnings` entry rather than a refusal. An open circuit does not block a test either: a successful test closes the circuit and doubles as the recovery probe, while a failed test never counts against it. You can therefore test a broken receiver as often as you need without making its situation worse.

A test creates no delivery record. The response body is the only record of it, so the attempt never appears in your delivery listing and there is nothing to replay. It can still return a 429, because it is checked against the same per-second budget as the endpoint's real deliveries. It consumes none of that budget itself, so a test can never starve production traffic.

The test envelope is the same for every topic. It carries `data.test: true`, one random id in both `workflowId` and `runId`, and the `serviceName` and `modelName` you sent. It carries no `status`, no `correlationId`, and, for a finding topic, no `findingId` or `attachmentId`.

### Responses

**200**

The test ran. Read `success` to learn whether your endpoint accepted it.

> **Warning:** A 200 means the test RAN, not that it succeeded. Your endpoint's failure is data in the body, carried by `success`, `statusCode` and `failureReason`, and never an HTTP error from PPS: a 502 from your server is a correct outcome of a working test.

| Property | Description | Type | Notes |
|----------|-------------|------|-------|
| eventId | The synthetic event's id, formatted as `msg_` followed by a version-4 UUID. Structurally disjoint from production event ids, so it can never collide with a real one. A test makes one attempt and is never replayed, so there is nothing to deduplicate on it: do not feed it to whatever tracks your real `webhook-id` values. | string | always emitted |
| topic | The topic that was synthesized, echoing your `eventType`. | string | always emitted |
| url | The URL the test was actually sent to, snapshotted at send time. | string | always emitted |
| sent | Whether an HTTP request actually left PPS. False when the attempt was stopped before the wire. | boolean | always emitted |
| success | Whether a 2xx came back. This is the outcome of the test, not the HTTP status of the call you just made. | boolean | always emitted |
| statusCode | The HTTP status your endpoint returned. Emitted as an explicit `null` when no HTTP response ever arrived. | number | always emitted, may be null |
| failureReason | A short reason token for a failed test, for example `HTTP_404`, `TRANSPORT_SocketTimeoutException` or `SSRF_VALIDATION_FAILED`. Absent when the test succeeded. Not a stable vocabulary to match on. | string | conditional |
| failureDetail | A longer explanation of the failure, written for a person. Absent when the test succeeded. | string | conditional |
| responseBody | Your endpoint's response body, truncated to the first 8,192 characters — the same bound that applies to a real delivery's `lastResponseBody`. The cut is unmarked. Absent when no response arrived or the response carried no body. Untrusted text, and possibly incomplete. | string | conditional |
| durationMillis | Elapsed real time of the HTTP attempt in milliseconds. Measures the outbound call alone. Absent when nothing was sent. | number | conditional |
| payload | The exact JSON body PPS sent, embedded as a JSON object. Always emitted, including when the request never left PPS. | object | always emitted |
| warnings | Advisories about conditions that did not stop the test. Each one describes something that would have stopped a real delivery. English sentences, not codes. | array | always emitted, including `[]` |

`statusCode` is emitted as an explicit `null`. The remaining optional properties are absent when they have nothing to report. The difference is part of the contract.

Example, a successful test:
```json
{
  "eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
  "topic": "workflow.updated",
  "url": "https://hooks.example.com/pps/workflows",
  "sent": true,
  "success": true,
  "statusCode": 200,
  "responseBody": "{\"received\":true}",
  "durationMillis": 187,
  "payload": {
    "type": "workflow.updated",
    "eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
    "timestamp": "2026-08-28T14:26:03Z",
    "data": {
      "test": true,
      "workflowId": "wfpop_8f14e45f-ceea-467a-9c1a-2b6d3e7f4a91",
      "runId": "wfrunpop_8f14e45f-ceea-467a-9c1a-2b6d3e7f4a91",
      "serviceName": "ADV",
      "modelName": "ADV-120"
    }
  },
  "warnings": []
}
```

The synthesized envelope on its own. It matches a production one except for its test markers: `data.test` is `true`, which never appears on a production envelope, and `workflowId` and `runId` are random and resolve to nothing on any PPS API.

```json
{
  "type": "workflow.updated",
  "eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
  "timestamp": "2026-08-28T14:26:03Z",
  "data": {
    "test": true,
    "workflowId": "wfpop_8f14e45f-ceea-467a-9c1a-2b6d3e7f4a91",
    "runId": "wfrunpop_8f14e45f-ceea-467a-9c1a-2b6d3e7f4a91",
    "serviceName": "ADV",
    "modelName": "ADV-120"
  }
}
```

Example, your server answered and rejected it. `sent: true` with `success: false`.
```json
{
  "eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
  "topic": "workflow.updated",
  "url": "https://hooks.example.com/pps/workflows",
  "sent": true,
  "success": false,
  "statusCode": 502,
  "failureReason": "HTTP_502",
  "failureDetail": "The destination returned 502 Bad Gateway.",
  "responseBody": "<html><body>Bad Gateway</body></html>",
  "durationMillis": 412,
  "payload": {
    "type": "workflow.updated",
    "eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
    "timestamp": "2026-08-28T14:26:03Z",
    "data": {
      "test": true,
      "workflowId": "wfpop_8f14e45f-ceea-467a-9c1a-2b6d3e7f4a91",
      "runId": "wfrunpop_8f14e45f-ceea-467a-9c1a-2b6d3e7f4a91",
      "serviceName": "ADV",
      "modelName": "ADV-120"
    }
  },
  "warnings": []
}
```

Example, the request never left PPS. `sent: false` and `statusCode` is an explicit `null`.
```json
{
  "eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
  "topic": "workflow.updated",
  "url": "https://hooks.example.com/pps/workflows",
  "sent": false,
  "success": false,
  "statusCode": null,
  "failureReason": "SSRF_VALIDATION_FAILED",
  "failureDetail": "The destination did not pass destination-safety validation, so nothing was sent.",
  "payload": {
    "type": "workflow.updated",
    "eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
    "timestamp": "2026-08-28T14:26:03Z",
    "data": {
      "test": true,
      "workflowId": "wfpop_8f14e45f-ceea-467a-9c1a-2b6d3e7f4a91",
      "runId": "wfrunpop_8f14e45f-ceea-467a-9c1a-2b6d3e7f4a91",
      "serviceName": "ADV",
      "modelName": "ADV-120"
    }
  },
  "warnings": []
}
```

Example, a success on a disabled endpoint. The condition is a warning, not a refusal.
```json
{
  "eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
  "topic": "workflow.updated",
  "url": "https://hooks.example.com/pps/workflows",
  "sent": true,
  "success": true,
  "statusCode": 200,
  "responseBody": "{\"received\":true}",
  "durationMillis": 203,
  "payload": {
    "type": "workflow.updated",
    "eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
    "timestamp": "2026-08-28T14:26:03Z",
    "data": {
      "test": true,
      "workflowId": "wfpop_8f14e45f-ceea-467a-9c1a-2b6d3e7f4a91",
      "runId": "wfrunpop_8f14e45f-ceea-467a-9c1a-2b6d3e7f4a91",
      "serviceName": "ADV",
      "modelName": "ADV-120"
    }
  },
  "warnings": [
    "Endpoint 4711 is disabled, so a real event of this shape would not have been delivered."
  ]
}
```

A test that succeeds while `warnings` is not empty is telling you the endpoint works but is not yet receiving.

**400**

The selector names an event type this endpoint has not declared for that product. The body is an `about:blank` problem document whose `detail` names the selector.

```json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "This endpoint has not declared eventType 'workflow.failed' for product 'ADV'/'ADV-120'. Add it to the endpoint's acceptedEventTypes first.",
  "instance": "urn:pps:request:24f80b6e-91a3-4c57-bd08-7e5f3a2c169d"
}
```

Edit the endpoint to add the topic to its `acceptedEventTypes`, then test again. A malformed body, or a `Content-Type` naming a character set the service cannot use (`https://pointservices.com/problems/invalid-request`), is also a 400.

**409**

The endpoint's destination domain is no longer authorized, so nothing can be sent to it.

```json
{
  "type": "https://pointservices.com/problems/conflict",
  "title": "The resource's current state refuses this request",
  "status": 409,
  "detail": "This endpoint's destination domain is no longer authorized, so no delivery can be sent to it. Re-authorize the domain, or edit the endpoint to a URL on an authorized one.",
  "instance": "urn:pps:request:13e6a97c-40d8-4b21-8f5a-6c09b2e7d384"
}
```

**429**

The endpoint's delivery rate budget for the current window is spent, shared with its real deliveries.

**503**

PPS could not sign, which is a PPS fault and never a statement about your endpoint.

**401**

No credential was presented, or the one presented was rejected.

**403**

Refused before the operation ran: an `about:blank` problem document, or an HTML body when the platform edge refused the request, for example for a missing `Content-Type` or a value that resembles SQL or script. An endpoint that is not yours is reported as a 404, not a 403.

**404**

No such endpoint, delivery or task is available to this request. This also covers a resource that exists but is not yours, or one you may not act on: the three cases are deliberately indistinguishable so that ids cannot be enumerated, which is why an endpoint that is not yours answers 404 rather than 403. Check the id you sent and the org unit of the credential you sent it with.

See [Errors](https://docs.pitchpointsolutions.com/api/webhooks/index#errors).

---

## Simulate An Event

Fire one synthetic event of the type you name and report every delivery it produced.

`POST /v1/webhooks/events/simulate`

> **Warning:** This is the real fan out, not a dry run. It writes real delivery rows that appear in your delivery listing, count toward retention, retry on the ordinary schedule, and are replayable. A failing receiver will open the circuit exactly as a real event would, and the endpoint then stops receiving real traffic until the circuit closes. The test operation is the one that cannot do this.

```bash
curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/webhooks/events/simulate \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "workflow.updated",
    "serviceName": "ADV",
    "modelName": "ADV-120",
    "correlationId": "9c2e14a7-55d0-4b8e-a731-6f0d2b93e185"
  }'
```

It fans out to every entitled endpoint in your org unit, not to one you choose. There is no endpoint id in the request. If three endpoints in your org unit accept this product and topic, all three receive it. Endpoints in other org units, including org units beneath yours, receive nothing from a simulation. To exercise a single endpoint, use the test operation.

It answers the receiver's question and only that one: would events of this shape arrive at your own endpoints? PPS fires one event per org unit that could produce it for you:

- one attributed to your own org unit, because an endpoint receives its own org unit's events without a subscription grant; this one is always fired, and fired first;
- one for each other org unit holding a live subscription grant that routes this product and topic to your org unit.

Your org unit comes from your credential, never from the request, and every event is delivered only to endpoints in your org unit. You cannot simulate an event on behalf of another org unit, or aim a simulation at another org unit's endpoints.

Simulating a finding topic fires that one event only, not the `workflow.updated` a real finding change is accompanied by. Its envelope omits `data.status` and carries a random `data.findingId`, plus a random `jattmt_`-prefixed `data.attachmentId` for the two attachment topics. Like the random `workflowId` and `runId`, those ids resolve to nothing.

#### Header Properties

| Property | Value | Required? |
|----------|-------|-----------|
| Authorization | Bearer your_access_token_here | true |
| Content-Type | application/json | true |

#### Request Data Properties

| Property | Description | Type | Required |
|----------|-------------|------|----------|
| eventType | The topic to fire, any of the eight. For a workflow topic it also decides `data.status`: `pending`, `ready` or `failed`. A finding topic omits `data.status`. | string | yes |
| serviceName | The product's service name, exactly as you name it when starting a workflow. Together with `modelName` it decides which endpoints are entitled to the event. | string | yes |
| modelName | The product's model name. Matched exactly, so a value that names no real model reaches no endpoint and the simulation reports zero deliveries. | string | yes |
| correlationId | The correlation id to stamp on the simulated envelope. A value you supply is delivered unchanged. Omit it and a random UUID is minted, so the envelope's `data.correlationId` is never absent. The response's `deliveries` entries do not carry it; fetch a delivery to see it in the envelope that was sent. | string | no |

### Responses

**200**

The event was fanned out.

| Property | Description | Type | Notes |
|----------|-------------|------|-------|
| eventId | The simulated event's id, formatted as `msg_` followed by a UUID. One event is fired per producing org unit, and this field carries the **first** of them, the one attributed to your own org unit. | string | always emitted |
| simulated | Always `true` on this response, and the same value that appears on every delivery row this call created. | boolean | always emitted |
| deliveries | One entry per delivery row the fan out created, one row per entitled endpoint. | array | always emitted, including `[]` |
| warnings | Advisories about why the fan out reached fewer endpoints than you expected. English sentences, not codes. | array | always emitted, including `[]` |

#### deliveries Entry Properties

| Property | Description | Type | Notes |
|----------|-------------|------|-------|
| deliveryId | The created delivery's id. Fetch it to see the envelope that was built and, once attempted, what your server returned. | number | always emitted |
| endpointId | The endpoint this delivery is addressed to. It is also the `endpointId` you pass when fetching the delivery. | number | always emitted |
| subscriptionId | The subscription grant that entitled this delivery. Absent when the delivery was created on a path that resolved no single grant, most commonly because the endpoint sits in the org unit that produced the event, which needs no grant to receive it. | number | conditional |
| eventId | The event this delivery belongs to, the same value the receiver sees in its `webhook-id` header. | string | |
| sourceOuGuid | The org unit the simulated event was attributed to: your own org unit for the event PPS always fires, or the granting org unit for each of the others. | string | |

Example, no grant routes to you, and two endpoints in your org unit accept the product and topic. Only the event attributed to your own org unit was fired, so both rows carry the one `eventId` and neither carries a `subscriptionId`.
```json
{
  "eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
  "simulated": true,
  "deliveries": [
    {
      "deliveryId": 80531,
      "endpointId": 4711,
      "eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
      "sourceOuGuid": "OU41502"
    },
    {
      "deliveryId": 80532,
      "endpointId": 4712,
      "eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
      "sourceOuGuid": "OU41502"
    }
  ],
  "warnings": []
}
```

> **Warning:** When another org unit also holds a grant routing this product and topic to you, it is fired as its own event with its own `eventId`, and the top-level `eventId` carries only the first. Read each delivery's own `eventId` rather than assuming the top-level one covers them all.

Example, one other org unit, `OU10432`, holds a grant to yours. Each event reaches the same endpoint of yours, so `endpointId` repeats while `eventId`, `sourceOuGuid` and `subscriptionId` differ.
```json
{
  "eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
  "simulated": true,
  "deliveries": [
    {
      "deliveryId": 80533,
      "endpointId": 4711,
      "eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
      "sourceOuGuid": "OU41502"
    },
    {
      "deliveryId": 80534,
      "endpointId": 4711,
      "subscriptionId": 620,
      "eventId": "msg_6e3f8b41-7c02-4a95-8d16-b504e9a2f731",
      "sourceOuGuid": "OU10432"
    }
  ],
  "warnings": []
}
```

Example, nothing received the event. An empty `deliveries` array is a real answer rather than a failure, and `warnings` says what was missing: here, no endpoint in your org unit has declared the topic, and no other org unit routes it to you.
```json
{
  "eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
  "simulated": true,
  "deliveries": [],
  "warnings": [
    "No enabled endpoint in your org unit has declared that it accepts event type 'workflow.updated' for product 'ADV'/'ADV-120'. An endpoint receives events produced in its own org unit without any subscription grant, so adding this event type to the endpoint's acceptedEventTypes is all that is required for your own events.",
    "No live subscription grant routes product 'ADV'/'ADV-120' and event type 'workflow.updated' to your org unit from any OTHER org unit. Your own events need no grant; a PPS operator must grant this flow from any other org unit whose events you expect to receive."
  ]
}
```

The `warnings` are sentences for a person to read, not codes to match on.

Take a `deliveryId` from the response and fetch it to see the envelope that was built.

```bash
curl -X GET "https://api.pointservices.com/riskinsight-services-ws/resources/v1/webhooks/deliveries/4711/80531" \
  -H "Authorization: Bearer your_access_token_here"
```

**400**

The request body was malformed or carried a value outside this API's vocabulary.

**401**

No credential was presented, or the one presented was rejected.

**403**

Your credential may not manage webhooks in its own org unit. Simulate takes the org unit from your credential, never from the request, and the refusal comes before any event is fired. A 403 with `type: about:blank` can also come back before the operation runs, and the platform edge can refuse a request with an HTML body rather than a problem document.

```json
{
  "type": "https://pointservices.com/problems/not-authorized",
  "title": "Not authorized",
  "status": 403,
  "detail": "The authenticated identity is not permitted to perform this action.",
  "instance": "urn:pps:request:8c41b6e0-25d7-4a39-9f18-3b07d2e54c96"
}
```

**404**

No such endpoint, delivery or task is available to this request. This also covers a resource that exists but is not yours, or one you may not act on: the three cases are deliberately indistinguishable so that ids cannot be enumerated, which is why an endpoint that is not yours answers 404 rather than 403. Check the id you sent and the org unit of the credential you sent it with.

See [Errors](https://docs.pitchpointsolutions.com/api/webhooks/index#errors).

---

## Test Versus Simulate

| | Test | Simulate |
|---|---|---|
| What it addresses | One endpoint, named in the path | Every entitled endpoint; there is no endpoint id in the request |
| Delivery rows | Creates none. The response body is the only record, and the attempt never appears in your delivery listing. | Writes real rows that appear in your delivery listing |
| Retries | No, one attempt only | Yes, on the ordinary schedule |
| Counts against the circuit breaker | No. A failed test cannot open it, and a successful one **closes** it. | Yes, and a failing receiver can open it |
| Needs a subscription grant | No. The endpoint must have declared the topic for the product; an undeclared one is a 400 | No for the event attributed to your own org unit. Each other org unit's event needs a grant routing it to you, and without one that event is simply not fired |
| How the payload is marked | `data.test: true` plus a `webhook-test: true` header | `data.test: true` in the envelope, with no `webhook-test` header, and `simulated: true` on the delivery row |
| Envelope | The same synthetic shape for every topic, with no `status` | Shaped for the topic: `data.status` for a workflow topic, a random `findingId` for a finding topic |

> **API Reference:** [Getting Started](https://docs.pitchpointsolutions.com/developer_guides/webhooks/getting-started) uses the test operation for a first end-to-end check.
