# Endpoints

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

**Beta**

## Register An Endpoint

Create a webhook endpoint in an org unit you hold registration rights for.

`POST /v1/webhooks/endpoints`

```bash
curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/webhooks/endpoints \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/pps/workflows",
    "organizationalUnitGuid": "OU41502",
    "displayName": "Production order hook",
    "acceptedEventTypes": [
      {
        "serviceName": "ADV",
        "modelName": "ADV-120",
        "eventTypes": ["workflow.updated", "workflow.failed"]
      }
    ],
    "contacts": ["integrations@example.com"],
    "signatureMode": "SYMMETRIC",
    "signingSecret": "whsec_REPLACE_WITH_YOUR_OWN_RANDOM_KEY_MATERIAL"
  }'
```

The same request body on its own, so you can copy it without unpicking the shell quoting.

```json
{
  "url": "https://hooks.example.com/pps/workflows",
  "organizationalUnitGuid": "OU41502",
  "displayName": "Production order hook",
  "acceptedEventTypes": [
    {
      "serviceName": "ADV",
      "modelName": "ADV-120",
      "eventTypes": ["workflow.updated", "workflow.failed"]
    }
  ],
  "contacts": ["integrations@example.com"],
  "signatureMode": "SYMMETRIC",
  "signingSecret": "whsec_REPLACE_WITH_YOUR_OWN_RANDOM_KEY_MATERIAL"
}
```

`organizationalUnitGuid` is required and names the org unit the endpoint belongs to. There is no default, so a body omitting it is rejected with a 400. An org unit outside your hierarchy, or one you hold no registration rights for, is rejected with a 403 rather than falling back to your own.

PPS checks that the URL's domain is authorized, and runs destination-safety validation, before the endpoint is created. An unauthorized domain, or one that fails validation, is rejected with a 400 rather than accepted in a pending state. HTTPS is required.

Supply `signingSecret` when `signatureMode` is `SYMMETRIC`, including when you omit `signatureMode` entirely, since `SYMMETRIC` is the default. An `ASYMMETRIC` endpoint holds no shared secret, so supplying one is rejected rather than ignored.

> **Warning:** `whsec_REPLACE_WITH_YOUR_OWN_RANDOM_KEY_MATERIAL` is a placeholder, not key material, and PPS rejects it with a 400. Generate your own secret from a cryptographically secure random source, one per endpoint; [Getting Started](https://docs.pitchpointsolutions.com/developer_guides/webhooks/getting-started#step-1-choose-a-signing-secret) shows a command that does.

#### Header Properties

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

#### Request Data Properties

| Property | Description | Type | Required |
|----------|-------------|------|----------|
| url | The destination deliveries are POSTed to. Its domain must be one PPS has authorized, and it is checked against destination-safety rules; a URL that fails either is rejected with a 400 and the endpoint is not created. HTTPS is required. | string | yes |
| organizationalUnitGuid | The org unit the endpoint is created in, written as a string like `OU41502`. There is no default. Your PPS account manager supplies this value. | string | yes |
| displayName | Your own label. Trimmed, and the trimmed value must be 255 characters or fewer. | string | no |
| acceptedEventTypes | What this endpoint accepts, declared per product. Sending `[]` means "accept nothing", which registers an endpoint that receives nothing. | array | no |
| contacts | Notification contact email addresses, used to tell you when PPS stops delivering to this endpoint because its circuit opened. No email is sent when delivery resumes; read the endpoint's `circuitState` to see that it is `CLOSED` again. Registering none is permitted, but then nobody is told and deliveries stop silently. Each value is trimmed and normalized to lowercase before storage. | array | no |
| enabled | Whether the endpoint is switched on. A newly registered endpoint is enabled regardless of this property. | boolean | no |
| signatureMode | `SYMMETRIC` or `ASYMMETRIC`. Defaults to `SYMMETRIC`. | string | no |
| signingSecret | The secret PPS signs this endpoint's deliveries with: the `whsec_` prefix followed by 24 to 64 bytes of key material in base64. Required when `signatureMode` is `SYMMETRIC`, including when you omit the mode entirely; must be absent when it is `ASYMMETRIC`. | string | conditional |

#### acceptedEventTypes Entry Properties

| Property | Description | Type | Required |
|----------|-------------|------|----------|
| serviceName | The product's service name, exactly as you name it when starting a workflow. An unrecognized value is rejected with a 400. | string | no |
| modelName | The product's model name. Stored as given and matched exactly at delivery time, so a mistyped model receives nothing rather than being rejected. | string | no |
| eventTypes | The topics you want for this product, from the eight in [Event Topics](https://docs.pitchpointsolutions.com/api/webhooks/events#event-topics): `workflow.pending`, `workflow.updated`, `workflow.failed`, `finding.result_updated`, `finding.reviewed`, `finding.note_updated`, `finding.attachment_updated` and `finding.attachment_removed`. Repeats are collapsed. | array | no |

An empty `eventTypes` array is permitted and means this entry accepts nothing, which is equivalent to omitting the entry.

### Responses

**200**

The created endpoint, with no secret in it. A successful register returns 200, not 201.

| Property | Description | Type | Notes |
|----------|-------------|------|-------|
| id | The endpoint's id. It is the handle every other call that names an endpoint takes. | number | |
| organizationalUnitGuid | The org unit this endpoint belongs to, read back from the stored endpoint rather than echoed from your request. | string | |
| displayName | Your own label. Absent when none was ever set. | string | |
| url | The destination deliveries are POSTed to. | string | |
| enabled | Whether the endpoint is switched on. Only one of the three conditions behind `receiving`. | boolean | |
| circuitState | `CLOSED` or `OPEN`. A freshly registered endpoint reports `CLOSED`. | string | |
| receiving | Whether nothing stands between this endpoint and delivery: it is enabled, its circuit is `CLOSED`, and it has declared at least one product and topic. | boolean | |
| notReceivingReason | Why `receiving` is false: `NOT_DECLARED`, `NO_GRANT`, `DISABLED` or `CIRCUIT_OPEN`. Absent when `receiving` is true, never null and never a `NONE` sentinel. | string | conditional |
| hasNoContactsRegistered | True when zero notification contacts are registered. A permitted configuration, but the notification sent when this endpoint's circuit opens would then reach nobody, and you would not learn that deliveries had stopped. | boolean | |
| signatureMode | `SYMMETRIC` or `ASYMMETRIC`. | string | |
| acceptedEventTypes | What this endpoint has declared it accepts, grouped by product. Always present, including as `[]`. | array | always emitted |
| warnings | Advisories that block nothing, as English sentences. Write paths always emit it, including as `[]`. | array | write paths only |

There is no `signingSecret` property on this schema, and no other response schema in this API has one either. A signing secret travels from you to PPS and never back.

Example:
```json
{
  "id": 4711,
  "organizationalUnitGuid": "OU41502",
  "displayName": "Production order hook",
  "url": "https://hooks.example.com/pps/workflows",
  "enabled": true,
  "circuitState": "CLOSED",
  "receiving": true,
  "hasNoContactsRegistered": false,
  "signatureMode": "SYMMETRIC",
  "acceptedEventTypes": [
    {
      "serviceName": "ADV",
      "modelName": "ADV-120",
      "eventTypes": ["workflow.updated", "workflow.failed"]
    }
  ],
  "warnings": []
}
```

A newly registered endpoint is enabled. It will not receive anything until it declares at least one accepted event type, so read `receiving` rather than `enabled`.

Registering without `acceptedEventTypes` is allowed, and returns a `warnings` entry saying the endpoint receives nothing.

```json
{
  "id": 4711,
  "organizationalUnitGuid": "OU41502",
  "displayName": "Production order hook",
  "url": "https://hooks.example.com/pps/workflows",
  "enabled": true,
  "circuitState": "CLOSED",
  "receiving": false,
  "notReceivingReason": "NOT_DECLARED",
  "hasNoContactsRegistered": false,
  "signatureMode": "SYMMETRIC",
  "acceptedEventTypes": [],
  "warnings": [
    "This endpoint declares no accepted event types, so it will receive nothing until you add some."
  ]
}
```

> **Warning:** A misspelled `acceptedEventTypes` registers the same receive-nothing endpoint that omitting it does. Unknown keys are dropped rather than rejected, so `acceptedEventTyps` produces a 200 and an endpoint that never receives anything. Compare the `acceptedEventTypes` echoed back against what you sent.

Registering into a child org unit succeeds, and the endpoint then receives that child org unit's own events, not the parent's. An org unit's events reach its own endpoints without a grant. Events produced in any other org unit, the parent included, reach the endpoint only through a subscription grant that names the child org unit as its destination: grants match the endpoint's org unit exactly and do not inherit down the tree.

Read `organizationalUnitGuid` and `receiving` back off the response to confirm the endpoint landed where you intended. An endpoint that has declared a product and topic reports `receiving: true`, as here. That tells you it is wired up for its own org unit's events. It does not tell you that any other org unit routes events to it.

```json
{
  "id": 4711,
  "organizationalUnitGuid": "OU41507",
  "displayName": "Production order hook",
  "url": "https://hooks.example.com/pps/workflows",
  "enabled": true,
  "circuitState": "CLOSED",
  "receiving": true,
  "hasNoContactsRegistered": false,
  "signatureMode": "SYMMETRIC",
  "acceptedEventTypes": [
    {
      "serviceName": "ADV",
      "modelName": "ADV-120",
      "eventTypes": ["workflow.updated", "workflow.failed"]
    }
  ],
  "warnings": []
}
```

Accepting `workflow.updated` without `workflow.failed` is permitted and returns a warning rather than an error.

```json
{
  "id": 4711,
  "organizationalUnitGuid": "OU41502",
  "displayName": "Production order hook",
  "url": "https://hooks.example.com/pps/workflows",
  "enabled": true,
  "circuitState": "CLOSED",
  "receiving": true,
  "hasNoContactsRegistered": false,
  "signatureMode": "SYMMETRIC",
  "acceptedEventTypes": [
    {
      "serviceName": "ADV",
      "modelName": "ADV-120",
      "eventTypes": ["workflow.updated"]
    }
  ],
  "warnings": [
    "This endpoint accepts workflow.updated for ADV/ADV-120 without workflow.failed, so it will not be told when a run fails."
  ]
}
```

**400**

`organizationalUnitGuid` omitted.

```json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "organizationalUnitGuid is required",
  "instance": "urn:pps:request:c40a92e7-1b58-4d73-8f26-5a9e3c0b7d41"
}
```

A URL whose domain PPS has not authorized, or which fails destination-safety validation, is also rejected here. The endpoint is not created and there is no pending state to wait in.

```json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Webhook URL host is not an approved destination: hooks.example.net. Contact PPS support to request approval.",
  "instance": "urn:pps:request:5a7d02be-63c1-4f89-b204-9e138c7a06f5"
}
```

> **Warning:** Both 400s above carry `type: "about:blank"`, so you cannot branch on `type` to tell them apart. They are identical except for `detail`, which is prose written for a person and is not a stable code. Branch on the status, then read `detail` to log what went wrong; do not pattern-match its text. Two 400s do carry a dedicated type: `https://pointservices.com/problems/invalid-request` for a `Content-Type` naming a character set the service cannot use, and `https://iana.org/assignments/http-problem-types#digest-mismatched-values` for a body that did not match its digest. A domain rejection is not something you can fix in the request — ask your PPS account manager to authorize the domain, then register again.

**403**

An org unit you hold no registration rights over — `OU90311` here. The endpoint is not created, and PPS does not fall back to your own org unit. This is a 403 because it is about what you may act on; a rejected destination domain is a 400 because it is about the request. A blank `organizationalUnitGuid` is a 400 too, for the same reason.

```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:6b17d9f4-2e83-4a05-9c7b-8d1f0e5a3c62"
}
```

`detail` is fixed prose and never names the org unit it refused, so read the org unit you sent rather than parsing it out of the message.

**401**

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

**409**

The resource's current state refuses the request, or it changed under you.

See [Errors](https://docs.pitchpointsolutions.com/api/webhooks/index#errors) for the `invalid-token`, `about:blank` and `concurrent-modification` bodies.

---

## List Endpoints

Return a page of the webhook endpoints you may see, ascending by endpoint `id`.

`GET /v1/webhooks/endpoints`

```bash
curl -X GET "https://api.pointservices.com/riskinsight-services-ws/resources/v1/webhooks/endpoints?marker=4711&limit=50" \
  -H "Authorization: Bearer your_access_token_here"
```

#### Header Properties

| Property | Value | Required? |
|----------|-------|-----------|
| Authorization | Bearer your_access_token_here | true |

#### Query Parameters

| Property | Description | Type | Default |
|----------|-------------|------|---------|
| marker | The pagination cursor: pass back the `nextMarker` from a previous response. It is exclusive, so the endpoint it names is not sent again, and paging proceeds toward newer endpoints. A string on the wire that must parse as a positive integer; a value that will not is rejected with a 400. | string | |
| limit | The requested page size. Valid range is 1 to 100. A value outside that range is neither an error nor clamped: it is replaced outright by the default of 100. | number | 100 |

> **Warning:** `limit=0` and `limit=5000` both yield 100. A value outside the range is replaced outright by the default rather than clamped or rejected. The API reports the page size it applied rather than guessing what you meant, so compare the response's own `limit` against what you sent.

### Responses

**200**

| Property | Description | Type | Notes |
|----------|-------------|------|-------|
| limit | The page size actually applied, after an absent or out-of-range request was replaced with the default of 100. | number | always emitted |
| marker | The cursor you supplied, echoed back verbatim. Absent when none was supplied. | string | conditional |
| count | How many endpoints this page carries. | number | always emitted, including 0 |
| nextMarker | An opaque resume position, present whenever more endpoints may exist to see. | string | conditional |
| results | The endpoints, always in the shape that carries no secret. | array | always emitted, including `[]` |

> **Warning:** A page can come back short, or even completely empty, and still carry `nextMarker`. Access is decided per endpoint after rows are read, so the server bounds how much work it will do for one request. Stop paging only when `nextMarker` is absent, never on a short or empty page alone.

This is what that looks like: one endpoint under a limit of 50, and a cursor anyway, because there is more to see.

```json
{
  "limit": 50,
  "marker": "4711",
  "count": 1,
  "nextMarker": "4783",
  "results": [
    {
      "id": 4712,
      "organizationalUnitGuid": "OU41502",
      "displayName": "Production order hook (EU)",
      "url": "https://hooks.example.com/pps/workflows",
      "enabled": true,
      "circuitState": "CLOSED",
      "receiving": true,
      "hasNoContactsRegistered": false,
      "signatureMode": "SYMMETRIC",
      "acceptedEventTypes": [
        {
          "serviceName": "ADV",
          "modelName": "ADV-120",
          "eventTypes": ["workflow.updated", "workflow.failed"]
        }
      ]
    }
  ]
}
```

`nextMarker` here is `4783`, which is not the id of any endpoint in `results`. On a full page the cursor is the last endpoint returned; after a bounded scan it is instead the position the scan reached, which need not appear in `results` at all and may not correspond to any endpoint you can see. Treat it as opaque in both cases and pass it back unmodified. A loop that stopped because `count` was less than `limit` would have stopped here and missed everything the scan had not yet reached.

Entries carry no `warnings` key, because this is a read path.

Example:
```json
{
  "limit": 50,
  "marker": "4710",
  "count": 2,
  "nextMarker": "4712",
  "results": [
    {
      "id": 4711,
      "organizationalUnitGuid": "OU41502",
      "displayName": "Production order hook",
      "url": "https://hooks.example.com/pps/workflows",
      "enabled": true,
      "circuitState": "CLOSED",
      "receiving": true,
      "hasNoContactsRegistered": false,
      "signatureMode": "SYMMETRIC",
      "acceptedEventTypes": [
        {
          "serviceName": "ADV",
          "modelName": "ADV-120",
          "eventTypes": ["workflow.updated", "workflow.failed"]
        }
      ]
    },
    {
      "id": 4712,
      "organizationalUnitGuid": "OU41502",
      "displayName": "Production order hook (EU)",
      "url": "https://hooks.example.com/pps/workflows",
      "enabled": true,
      "circuitState": "CLOSED",
      "receiving": true,
      "hasNoContactsRegistered": false,
      "signatureMode": "SYMMETRIC",
      "acceptedEventTypes": [
        {
          "serviceName": "ADV",
          "modelName": "ADV-120",
          "eventTypes": ["workflow.updated", "workflow.failed"]
        }
      ]
    }
  ]
}
```

A request sending `limit=5000` comes back with `limit: 100`, which is how you see that what you sent was replaced.

```json
{
  "limit": 100,
  "count": 1,
  "results": [
    {
      "id": 4711,
      "organizationalUnitGuid": "OU41502",
      "displayName": "Production order hook",
      "url": "https://hooks.example.com/pps/workflows",
      "enabled": true,
      "circuitState": "CLOSED",
      "receiving": true,
      "hasNoContactsRegistered": false,
      "signatureMode": "SYMMETRIC",
      "acceptedEventTypes": [
        {
          "serviceName": "ADV",
          "modelName": "ADV-120",
          "eventTypes": ["workflow.updated", "workflow.failed"]
        }
      ]
    }
  ]
}
```

**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.

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

---

## Get One Endpoint

Return one endpoint in the shape that carries no secret.

`GET /v1/webhooks/endpoints/{id}`

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

#### Path Parameters

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

### Responses

**200**

The same shape the register operation returns, minus `warnings`.

Example:
```json
{
  "id": 4711,
  "organizationalUnitGuid": "OU41502",
  "displayName": "Production order hook",
  "url": "https://hooks.example.com/pps/workflows",
  "enabled": true,
  "circuitState": "CLOSED",
  "receiving": true,
  "hasNoContactsRegistered": false,
  "signatureMode": "SYMMETRIC",
  "acceptedEventTypes": [
    {
      "serviceName": "ADV",
      "modelName": "ADV-120",
      "eventTypes": ["workflow.updated", "workflow.failed"]
    }
  ]
}
```

This body carries no `warnings` key at all. Read paths never emit it, while write paths always do, including as `[]`. An absent key and `[]` are different statements: `warnings: []` means your configuration produced no advisories, and an absent key means the response did not come from a write path.

**401**

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

**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).

---

## Edit An Endpoint

Update an endpoint. Every property of the request body is optional, and an omitted property is left unchanged.

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

```bash
curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/webhooks/endpoints/4711 \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Production order hook (EU)",
    "acceptedEventTypes": [
      {
        "serviceName": "ADV",
        "modelName": "ADV-120",
        "eventTypes": [
          "workflow.updated",
          "workflow.failed",
          "workflow.pending",
          "finding.reviewed",
          "finding.result_updated",
          "finding.note_updated",
          "finding.attachment_updated",
          "finding.attachment_removed"
        ]
      }
    ]
  }'
```

Those are merge semantics: this is a POST rather than a PUT because it updates the properties you send and leaves the rest alone, which is not what PUT promises.

An edit is validated as strictly as a create. Any URL change runs the same destination-safety validation again, and a change of URL must land on an authorized domain or the entire edit is rejected with a 400, with no part of it applied. There is no state in which an edit waits to be authorized.

#### Header Properties

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

#### Request Data Properties

| Property | Description | Type | Required |
|----------|-------------|------|----------|
| url | The destination deliveries are POSTed to. | string | no |
| organizationalUnitGuid | A different value **moves** the endpoint to that org unit. Omitting it, or sending the value it already has, leaves it where it is. | string | no |
| displayName | Your own label. | string | no |
| acceptedEventTypes | Omitting it leaves the declared set alone; sending `[]` replaces it with nothing. | array | no |
| contacts | Notification contact email addresses, used to tell you when this endpoint's circuit opens. | array | no |
| enabled | Only the JSON literals `true` and `false` are accepted. A number, including `0` or `1`, or a quoted string is rejected with a 400. | boolean | no |
| signatureMode | `SYMMETRIC` or `ASYMMETRIC`. Send it with `signingSecret` to switch modes. | string | no |
| signingSecret | Sent alone, rotates the secret of an endpoint that is already `SYMMETRIC`. Sent alone to an `ASYMMETRIC` endpoint, rejected with a 400. | string | no |

### Omitted versus explicitly empty

For the array properties, an omitted key and an explicitly empty array are different instructions. This body changes the label and leaves the declared topics as they are.

```json
{
  "displayName": "Production order hook (EU)"
}
```

This body changes the label and clears the declared topics, after which the endpoint receives nothing.

```json
{
  "displayName": "Production order hook (EU)",
  "acceptedEventTypes": []
}
```

### Rotating the signing secret

> **Warning:** A rotated signing secret takes effect immediately, including for deliveries already queued and awaiting an attempt, and there is no overlap window in which both the old and the new secret verify. Install the new value in your consumer first, then call this. In the other order there is an interval during which you cannot verify what PPS sends.

```bash
curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/webhooks/endpoints/4711 \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "signingSecret": "whsec_REPLACE_WITH_YOUR_OWN_RANDOM_KEY_MATERIAL"
  }'
```

Choose the value from a cryptographically secure random source. PPS checks that it is well formed — the `whsec_` prefix followed by 24 to 64 bytes of key material in base64 — but cannot check that it is unguessable. Treat it as a credential: do not send it over any channel but this API, and do not paste it into a support ticket.

### Switching signature mode

Send `signatureMode` in the same request as the secret change. Switching to `SYMMETRIC` requires `signingSecret`; switching to `ASYMMETRIC` forbids it and clears the stored value, so that request carries `signatureMode` alone. One request makes the transition atomic, with no interval in which the endpoint is `SYMMETRIC` but unsignable.

```bash
curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/webhooks/endpoints/4711 \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "signatureMode": "ASYMMETRIC"
  }'
```

### Moving to another org unit

> **Warning:** Moving an endpoint changes which events it receives, and this is the one consequence the API cannot warn you about at the time. An endpoint receives the events of the org unit it belongs to, plus those a subscription grant routes to that exact org unit. The moment it lands elsewhere it stops seeing the events it was built for, including any a grant routed to its old org unit, and starts seeing the destination's instead. The move returns 200 and the endpoint still reports itself `enabled` and `receiving: true`, so nothing in the response looks wrong while your integration goes quiet. Confirm the destination is the org unit whose events you actually want, and, if you rely on another org unit's events, that a grant names the destination, before you move.

```bash
curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/webhooks/endpoints/4711 \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationalUnitGuid": "OU41507"
  }'
```

The move request body on its own.

```json
{
  "organizationalUnitGuid": "OU41507"
}
```

The destination is authorized in its own right, not merely the endpoint you already own. Moving into an org unit outside your hierarchy, or one you hold no rights over, is refused with a 403 and the endpoint is left exactly where it was. Deliveries already queued for the endpoint are not rewritten by a move.

### Disabling an endpoint

```bash
curl -X POST https://api.pointservices.com/riskinsight-services-ws/resources/v1/webhooks/endpoints/4711 \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": false
  }'
```

There is no operation that deletes an endpoint. Disabling is how you take one out of service, and the endpoint continues to exist and to appear in your endpoint listing. Disabling stops all deliveries, and deliveries queued at the moment of disabling are parked, not cancelled. Enabling the endpoint again and then issuing a bulk replay resurrects them. Recovery is bounded by the delivery retention window, which is 90 days, so parked rows are eventually reaped like any other finished row. See [Deliveries](https://docs.pitchpointsolutions.com/api/webhooks/deliveries).

### Responses

**200**

The updated endpoint, with no secret in it. Same shape as the register response.

Example, after the edit at the top of this section:
```json
{
  "id": 4711,
  "organizationalUnitGuid": "OU41502",
  "displayName": "Production order hook (EU)",
  "url": "https://hooks.example.com/pps/workflows",
  "enabled": true,
  "circuitState": "CLOSED",
  "receiving": true,
  "hasNoContactsRegistered": false,
  "signatureMode": "SYMMETRIC",
  "acceptedEventTypes": [
    {
      "serviceName": "ADV",
      "modelName": "ADV-120",
      "eventTypes": [
        "workflow.updated",
        "workflow.failed",
        "workflow.pending",
        "finding.reviewed",
        "finding.result_updated",
        "finding.note_updated",
        "finding.attachment_updated",
        "finding.attachment_removed"
      ]
    }
  ],
  "warnings": []
}
```

After a move, the endpoint is still enabled and still reports `receiving: true`, even if the destination org unit never produces the events it has declared. `receiving` says nothing stands between the endpoint and delivery; it cannot say whether an org unit will produce events.

```json
{
  "id": 4711,
  "organizationalUnitGuid": "OU41507",
  "displayName": "Production order hook",
  "url": "https://hooks.example.com/pps/workflows",
  "enabled": true,
  "circuitState": "CLOSED",
  "receiving": true,
  "hasNoContactsRegistered": false,
  "signatureMode": "SYMMETRIC",
  "acceptedEventTypes": [
    {
      "serviceName": "ADV",
      "modelName": "ADV-120",
      "eventTypes": ["workflow.updated", "workflow.failed"]
    }
  ],
  "warnings": []
}
```

**400**

`signingSecret` sent alone to an `ASYMMETRIC` endpoint.

```json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Endpoint 4711 is ASYMMETRIC and holds no shared secret, so `signingSecret` cannot be set on its own. Send `signatureMode` and `signingSecret` together to switch it to SYMMETRIC.",
  "instance": "urn:pps:request:d82e50a9-4c17-4b63-97fa-0e6b3d1c8547"
}
```

`enabled` sent as the number `0`.

```json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "`enabled` must be the JSON literal true or false. It carried the number 0, which is not converted to a boolean.",
  "instance": "urn:pps:request:9e0b4a37-6d52-4f18-8c94-2a7f5e3b0d61"
}
```

**403**

A move to `OU90311`, a destination outside your hierarchy. The endpoint stays where it was. This is the loud failure; the silent one is a move into an org unit you may reach but that produces nothing the endpoint has declared, which returns 200.

```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:f93c05b6-7a21-4e48-b0d9-3c6e1a84f572"
}
```

**401**

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

**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.

**409**

The resource's current state refuses the request, or it changed under you.

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

---

## Endpoint Vocabularies

#### signatureMode

| Value | Signature entry | Key material | Key rotation |
|---|---|---|---|
| SYMMETRIC | `v1`, HMAC-SHA256 | Your `signingSecret`, which you set and rotate | You rotate the secret and install it in your consumer first |
| ASYMMETRIC | `v1a`, Ed25519 | The PPS published public key, fetched through OIDC discovery | PPS rotates its key and you change nothing |

`SYMMETRIC` is the default, so an endpoint registered without mentioning signing at all is a symmetric one and must carry a secret from the moment it exists.

> **Warning:** An `ASYMMETRIC` endpoint holds no shared secret, so if PPS cannot produce the `v1a` signature the delivery is not sent at all and is retried on the ordinary schedule. It is never downgraded to an unsigned request and never to `v1`.

#### circuitState

| Value | Meaning |
|---|---|
| CLOSED | The healthy state. Deliveries are attempted normally, and a freshly registered endpoint reports `CLOSED`. |
| OPEN | PPS has stopped attempting deliveries because the endpoint failed repeatedly. Deliveries are held rather than dropped. |

There are exactly two states. There is no third.

| Event | Effect on the circuit |
|---|---|
| Five consecutive failed attempts with no success between them | Opens it. The unit is the attempt, not the delivery, and there is no time window. |
| Your endpoint answers `410 Gone` | Opens it. |
| One delivery exhausts its full retry schedule | Opens it. That is ten attempts over about 75 hours; see [the retry schedule](https://docs.pitchpointsolutions.com/developer_guides/webhooks/receiving#the-retry-schedule). |
| A successful attempt | Resets the failure run to zero. |
| One successful delivery while the circuit is open | Closes it, and the held deliveries resume. |
| Five minutes elapsed since it opened | One probe delivery is admitted, one in flight at a time. |
| A successful test delivery | Closes it. A test is not subject to the circuit, and a failed test never counts against it. |

A failure is a transport error or any response outside the 2xx range, including a 4xx. Only failures newer than the endpoint's most recent successful attempt are counted. The probe is admitted without changing the reported state, so an endpoint under trial still reports `OPEN` until the probe succeeds.

#### notReceivingReason

| Value | Meaning | Who fixes it |
|---|---|---|
| NOT_DECLARED | The endpoint declares no accepted event types, so no event can ever match it. | You, by adding products and topics to `acceptedEventTypes`. |
| DISABLED | The endpoint has declared something, but it is switched off (`enabled: false`). PPS does not switch an endpoint off after failures; it opens the circuit instead. | You, by re-enabling it. |
| CIRCUIT_OPEN | Declared and enabled, but the circuit breaker opened after repeated delivery failures or a `410 Gone`. | It closes on its own once your endpoint accepts a probe, or when a test delivery succeeds. |
| NO_GRANT | The endpoint declared products and topics, but no live subscription grant covers any of them. An endpoint that has declared anything receives its own org unit's events without a grant, so this value is not expected in practice. Handle it anyway. | PPS. Contact your PPS account manager. |

`receiving` is true only when the endpoint is enabled, its circuit is `CLOSED`, and it has declared at least one product and topic in its `acceptedEventTypes`. It is one answer for the whole endpoint, not one per product: an endpoint that declared `ADV-120` reports `receiving: true` whatever else you expect it to receive. Declaring is the only step you take for your own org unit's events. A subscription grant is needed only for events produced in another org unit, and `receiving` does not tell you whether one exists.

Exactly one cause is reported in `notReceivingReason` even when several hold at once, in this order: `NOT_DECLARED`, then `DISABLED`, then `CIRCUIT_OPEN`, then `NO_GRANT`. Work the reported cause, then read the field again. Treat an unrecognized value as "not receiving, cause unknown" so a reason added later does not break your client.

#### organizationalUnitGuid versus sourceOuGuid

Both are `OU`-prefixed strings, and org unit GUIDs are globally unique, so the two are told apart by where they appear rather than by their shape.

| Field | Where it appears | What it names |
|---|---|---|
| `organizationalUnitGuid` | Register and edit requests; every endpoint response | The org unit the endpoint belongs to, which is the destination its deliveries are routed to. `OU41502` in these docs. |
| `sourceOuGuid` | `deliveries[]` on a simulate response | The org unit the simulated event was attributed to: your own org unit (`OU41502`) for the event PPS always fires, or a granting org unit such as `OU10432` for the others. |

> **API Reference:** [Getting Started](https://docs.pitchpointsolutions.com/developer_guides/webhooks/getting-started) walks through registering an endpoint end to end, including where your org unit GUID comes from.
