Beta
Webhooks API
Register and configure webhook delivery endpoints, set their signing secrets, enable and disable them, and inspect or replay individual deliveries.
If you are integrating for the first time, start with the Webhooks Developer Guide.
Browse every operation, its schemas and code samples in the API Explorer, or download the OpenAPI specification for your own tools.
Every path below is relative to the base URL:
https://api.pointservices.com/riskinsight-services-ws/resources
| Operation | Method and path | Reference |
|---|---|---|
| Register an endpoint | POST /v1/webhooks/endpoints | Endpoints |
| List endpoints | GET /v1/webhooks/endpoints | Endpoints |
| Get one endpoint | GET /v1/webhooks/endpoints/{id} | Endpoints |
| Edit an endpoint | POST /v1/webhooks/endpoints/{id} | Endpoints |
| Query deliveries | POST /v1/webhooks/deliveries/{endpointId}/query | Deliveries |
| Get one delivery | GET /v1/webhooks/deliveries/{endpointId}/{deliveryId} | Deliveries |
| Replay one delivery | POST /v1/webhooks/deliveries/{endpointId}/{deliveryId}/replay | Deliveries |
| Replay in bulk | POST /v1/webhooks/deliveries/{endpointId}/replay | Deliveries |
| Get a bulk replay task | GET /v1/webhooks/deliveries/{endpointId}/replay/{taskId} | Deliveries |
| Send a test delivery | POST /v1/webhooks/endpoints/{id}/test | Events |
| Simulate an event | POST /v1/webhooks/events/simulate | Events |
Getting Started walks through registering an endpoint and receiving a first verified delivery, including where your org unit GUID comes from — you obtain it from your PPS account manager before any call here will succeed.
Authentication
Every request must be authenticated with a bearer token in the standard Authorization header, written as Authorization: Bearer <token>. That is the mechanism this API supports; send your credential this way and no other. Authentication is checked before the request body is read, so an unauthenticated request never reveals whether its body would have been accepted.
The scheme name is matched without regard to case, but the header must carry the scheme and the token separated by a single space. A bare token with no Bearer prefix is rejected.
Obtaining An Access Token — sign in with the credentials PPS issued you. An access token expires one hour after it is issued, so refresh it. The refresh token expires too, on its own schedule, so an unattended integration must be able to sign in again on its own.
Conventions
Every timestamp in this API, sent or received, is an RFC 3339 instant in UTC with a trailing Z, for example "2026-08-28T14:26:03Z". Fractional seconds are emitted only when they are not zero, so do not assume a fixed field width, and parse with a library rather than by slicing the string.
Unknown JSON keys in a request body are ignored rather than rejected, so a client written against a later version of this API can safely call an earlier deployment. The cost is that a misspelled property name is silently dropped: compare what comes back against what you sent.
Errors
Every error response the service sends is an RFC 9457 problem document, served as application/problem+json. One shape covers every status, so you parse errors the same way whatever went wrong. The one exception comes from in front of the service: the platform edge can refuse a request with a 403 and an HTML body, for example when it omits Content-Type or carries a value that resembles SQL or script. Check Content-Type before you parse an error body.
{
"type": "https://pointservices.com/problems/invalid-token",
"title": "The supplied credential was rejected",
"status": 401,
"detail": "The bearer token in the `Authorization` header was rejected. Obtain a fresh token rather than inspecting the one you sent.",
"instance": "urn:pps:request:4b8c2d90-7f31-4a6e-95b2-0c3e8a7d1f45"
}
Problem Properties
| Property | Description | Type | Required |
|---|---|---|---|
| type | An absolute URI identifying the problem type. This is the stable value your program matches on. about:blank means the status code alone describes the problem. | string | yes |
| title | A short summary of the problem type, written for a person. Stable for a given type, but written for a reader rather than for matching. | string | yes |
| status | The HTTP status code, repeated in the body so a logged problem document stands on its own. | number | yes |
| detail | An explanation of this occurrence, naming the offending field where one is known. Written for your logs, not a stable code. | string | no |
| instance | A URI identifying this occurrence, written as urn:pps:request:<id>. Quote it when you raise a support ticket. | string | no |
Branch on type where there is one, and compare it as a string — a type URI identifies a problem, it is not a document you fetch. Never branch on the text of title or detail: both are written for a person to read and neither is a stable code.
Your handler must tolerate about:blank: most 400s carry it, as do a 401 with no credential, a 403 refused before the operation ran, and every 5xx. A 401 with a rejected token, a 403 decided by the operation, and every 404, 409 and 429, carries a dedicated type, and so do two kinds of 400. Branch on type where there is one, fall back to the status otherwise — a client that switches on type alone cannot tell two different 400s apart — and keep a default for anything you do not recognize.
Problem types
This is the complete set of types this API emits.
| type | Status | What it means | What to do |
|---|---|---|---|
about:blank | 400 | The request body was malformed, carried a value of the wrong type, or carried a value outside this API’s vocabulary. These 400s are not typed, so detail is the only thing that distinguishes them. | Read detail and fix the request. Do not pattern-match its text. |
https://pointservices.com/problems/invalid-request | 400 | The Content-Type header names a character set this service cannot use. | Correct the charset parameter of your Content-Type header. |
https://iana.org/assignments/http-problem-types#digest-mismatched-values | 400 | The body did not match the digest sent with it. | Send the request again. This is IANA’s registered type rather than a PPS one. |
https://pointservices.com/problems/invalid-token | 401 | A credential was presented and rejected. | Obtain a fresh token. |
about:blank | 400, 401 with no credential, 403 refused before the operation ran, and any status with no dedicated type of its own, including every 5xx | No credential was presented, the request was refused before the operation ran, or the status carries no dedicated type. | For a 401, send an Authorization header. For a 5xx, retry. |
https://pointservices.com/problems/not-authorized | 403 | You are authenticated, but you may not act on the org unit involved: the one you named when registering or moving an endpoint, or your own when simulating. | Name an org unit you hold rights over, or ask your PPS account manager to grant them. |
https://pointservices.com/problems/not-found | 404 | The resource is missing, out of your tenancy, or one you may not act on. | Check the id you sent. |
https://pointservices.com/problems/conflict | 409 | The resource’s current state refuses the request. | Change the state first, then retry. |
https://pointservices.com/problems/concurrent-modification | 409 | Another write landed first, so nothing was applied. | Read the resource again, and resubmit if your change still applies. |
https://pointservices.com/problems/too-many-requests | 429 | The endpoint’s delivery rate budget for the current window is spent. | Try again in about a second. |
Status by status
400
The request body was malformed, carried a value of the wrong type, or carried a value outside this API’s vocabulary: an unknown topic, an unrecognized delivery status, an implausible contact address, or a marker that is not a delivery id. These 400s carry about:blank, so detail is the only thing that tells two of them apart.
Two 400s carry a dedicated type. https://pointservices.com/problems/invalid-request means the Content-Type header names a character set this service cannot use. https://iana.org/assignments/http-problem-types#digest-mismatched-values means the body did not match the digest sent with it.
Unknown keys are not an error. They are dropped, so a client written against a later version keeps working against an earlier deployment.
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "`acceptedEventTypes[0].eventTypes[1]` carries `workflow.done`, which is not a published topic.",
"instance": "urn:pps:request:1e5f9a24-3b70-4c82-a6d1-9f4b7e0c3852"
}
401
The request carried no valid authentication. Two cases produce this status, and type tells them apart. invalid-token means a credential was presented and rejected.
{
"type": "https://pointservices.com/problems/invalid-token",
"title": "The supplied credential was rejected",
"status": 401,
"detail": "The bearer token in the `Authorization` header was rejected. Obtain a fresh token rather than inspecting the one you sent.",
"instance": "urn:pps:request:4b8c2d90-7f31-4a6e-95b2-0c3e8a7d1f45"
}
invalid-token does not say why the credential was rejected. PPS cannot distinguish an expired token from a malformed one from a badly signed one. Treat every one the same way: obtain a fresh token rather than inspecting the one you sent.
about:blank means no credential was presented at all.
{
"type": "about:blank",
"title": "Unauthorized",
"status": 401,
"detail": "No credential was presented. Send an `Authorization: Bearer <token>` header.",
"instance": "urn:pps:request:a71f3c28-5d06-4b94-8e2a-1f5c7d0b3946"
}
403
You are authenticated, and the answer is still no. A 403 is an authorization decision, never a statement about your credential, so refreshing your token will not turn it into a 200. A request refused with a 403 changes nothing.
Any operation can return one. Two types tell the cases apart:
https://pointservices.com/problems/not-authorized: the operation refused the org unit involved. Registering an endpoint in, or moving one to, an org unit you hold no rights over returns it, and so does simulating an event when your credential may not manage webhooks in its own org unit.about:blank: the request was refused before the operation ran.
The platform edge can also answer 403 before the request reaches the service, with an HTML body rather than a problem document.
Naming an endpoint id that is not yours is a 404, not a 403, so ids cannot be probed by watching the status change.
{
"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:2f7e4c83-b019-4d56-8a3f-6e1d0b9c5a27"
}
404
The resource you named is missing, out of your tenancy, or one you may not act on. The three are deliberately indistinguishable, so endpoint ids cannot be enumerated: an endpoint that exists but is not yours answers 404, never 403.
{
"type": "https://pointservices.com/problems/not-found",
"title": "No such resource",
"status": 404,
"detail": "No endpoint 4711 is available to this request.",
"instance": "urn:pps:request:8d3a1f56-6c94-4e20-b7f8-0a5e9c2d4b73"
}
409
Two different types share this status, and they call for different handling.
conflict means the resource’s current state refuses the request. Two conditions produce it: a test to an endpoint whose destination domain is no longer authorized, and a bulk replay into an endpoint whose circuit is OPEN. Both need the endpoint’s state to change first.
{
"type": "https://pointservices.com/problems/conflict",
"title": "The resource's current state refuses this request",
"status": 409,
"detail": "Endpoint 4711 has an OPEN circuit, so a bulk replay is refused until it closes.",
"instance": "urn:pps:request:5c2b9e47-0a38-4f15-9d6e-2b7c4a1f8035"
}
concurrent-modification means another write landed first, so your write was not applied. It is not retried for you: read the resource again, decide whether your change still applies to what you now see, and send the edit again if it does.
{
"type": "https://pointservices.com/problems/concurrent-modification",
"title": "The resource was modified concurrently",
"status": 409,
"detail": "Endpoint 4711 changed while your write was in flight, so nothing was applied. Read it again and retry.",
"instance": "urn:pps:request:3a6f80d2-1e74-4b93-a5c0-8d2b7e4f16a9"
}
The replay of a single delivery never returns 409. It returns 200 and silently does nothing when the delivery is PENDING or RETRYING or the endpoint’s circuit is OPEN. A client branching on 409 to detect a refused replay will swallow every one of them. See Deliveries.
429
The endpoint’s delivery rate budget for the current window is spent. The budget is shared with that endpoint’s real deliveries rather than being a separate allowance for testing, so a test can never starve the production traffic for the endpoint it is meant to be validating. Try again in about a second.
{
"type": "https://pointservices.com/problems/too-many-requests",
"title": "The delivery rate budget is spent",
"status": 429,
"detail": "This endpoint's per second delivery budget for the current window is spent. The budget is shared with its real deliveries. Try again in about a second.",
"instance": "urn:pps:request:7e19c3b8-4d06-4a2f-b95c-1c8e6f30d472"
}
503
A fault on the PPS side, never a statement about your endpoint. Most concretely, the PPS signer could not produce a signature. Your endpoint was not contacted, so nothing was delivered in part and the request is safe to retry as it stands. This status has no dedicated problem type, so it carries about:blank.
The test operation is the only one that returns it. A real delivery that cannot be signed is not sent and is retried on the ordinary schedule instead, which you observe on the delivery row rather than as an HTTP status.
{
"type": "about:blank",
"title": "Service Unavailable",
"status": 503,
"detail": "PPS could not produce a signature for this delivery, so nothing was sent to your endpoint. Retry the request.",
"instance": "urn:pps:request:0b4d7f31-9a52-4c68-8e10-3f7b2d5c9a84"
}
Reporting a problem
instance is written as urn:pps:request:<id>, and the id inside it is the request correlation id. It is the same value returned in the x-pps-request-id and X-PPS-CorrelationID response headers. Quote it when you raise a support ticket; it is what lets PPS find that exact request.