API REFERENCE
Beta
Event Topics
A topic names what happened. PPS publishes two, and an endpoint may accept either or both.
| Topic | Fires when | data.status |
|---|---|---|
| workflow.updated | New data becomes available for the workflow. | ready |
| workflow.failed | A run fails. | failed |
Matching is exact and sensitive to case. Workflow.Updated is not workflow.updated, and any value outside the two is rejected with a 400. The topic and data.status always agree, on a real event, a test envelope and a simulated one alike.
There is no completion event. Neither topic implies the other follows, and neither is terminal.
Receiving Deliveries 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
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. It must be one this endpoint has declared for that product; any other value is rejected with a 400, checked after authorization. | string | yes |
Both halves of consent must already be in place. The eventType must be one this endpoint has declared for this product, and a live subscription grant must entitle your tenant to that product and event type. A missing half is a 400. Both are checked after authorization, so a caller who may not see this endpoint cannot use either error to learn what it accepts or what it is entitled to, and both answer with the same status so the error does not reveal which half is missing. Read detail to tell them apart; only the first is one you can fix yourself.
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 shares the endpoint’s live per-second delivery budget, though it consumes none of that budget itself.
Responses
200
The test ran. Read success to learn whether your endpoint accepted it.
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 KB — 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:
{
"eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
"topic": "workflow.updated",
"url": "https://hooks.example.com/pps/orders",
"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",
"status": "ready"
}
},
"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.
{
"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",
"status": "ready"
}
}
Example, your server answered and rejected it. sent: true with success: false.
{
"eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
"topic": "workflow.updated",
"url": "https://hooks.example.com/pps/orders",
"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",
"status": "ready"
}
},
"warnings": []
}
Example, the request never left PPS. sent: false and statusCode is an explicit null.
{
"eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
"topic": "workflow.updated",
"url": "https://hooks.example.com/pps/orders",
"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",
"status": "ready"
}
},
"warnings": []
}
Example, a success on a disabled endpoint. The condition is a warning, not a refusal.
{
"eventId": "msg_5c7e1a02-9d64-4f13-b8a7-6e0d3f2c8451",
"topic": "workflow.updated",
"url": "https://hooks.example.com/pps/orders",
"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",
"status": "ready"
}
},
"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, or no live grant entitles your tenant to that product and event type. Both answer with this status and this wording.
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Endpoint 4711 has not declared `workflow.failed` for ADV/ADV-120, so no test of that shape can be sent.",
"instance": "urn:pps:request:24f80b6e-91a3-4c57-bd08-7e5f3a2c169d"
}
Subscription grants are cross-tenant entitlements written by PPS, never by you, so there is no request you can make that creates one. If a test is refused for a missing grant, contact your PPS account manager to have it created, then test again.
409
The endpoint’s destination domain is no longer authorized, so nothing can be sent to it.
{
"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.
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, and none of them answers 403.
See Errors.
Simulate An Event
Fire one synthetic event of the type you name and report every delivery it produced.
POST /v1/webhooks/events/simulate
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.
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, not to one you choose. There is no endpoint id in the request. If three endpoints in your tenant accept this product and topic, all three receive it. To exercise a single endpoint, use the test operation.
It answers the receiver’s question and only that one. A subscription grant is directed: a data owner entitles a different tenant to receive event data originating in the data owner’s tenant, and this operation always puts you on the receiving end. The event is attributed to an org unit in the data owner’s tenant, never to one of yours. There is deliberately no producer-side mode and no field to request one.
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, from the published vocabulary. It also decides the envelope’s 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, present in both the envelope and the returned rows, so the key is never absent. | 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 tenant whose data owner has entitled yours to this product and topic, and this field carries the first of them. | 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, in the data owner’s tenant. Never one of yours. | string |
Example, one entitling tenant and two entitled endpoints of yours. Every row repeats the one eventId at the top of the response.
{
"eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
"simulated": true,
"deliveries": [
{
"deliveryId": 80531,
"endpointId": 4711,
"subscriptionId": 620,
"eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
"sourceOuGuid": "OU10432"
},
{
"deliveryId": 80532,
"endpointId": 4712,
"subscriptionId": 620,
"eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
"sourceOuGuid": "OU10432"
}
],
"warnings": []
}
When more than one tenant has entitled yours to this product and topic, each 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, two entitling tenants. Each fired its own event, so eventId, sourceOuGuid and subscriptionId differ per row while endpointId does not — both reach the same endpoint of yours.
{
"eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
"simulated": true,
"deliveries": [
{
"deliveryId": 80533,
"endpointId": 4711,
"subscriptionId": 620,
"eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
"sourceOuGuid": "OU10432"
},
{
"deliveryId": 80534,
"endpointId": 4711,
"subscriptionId": 621,
"eventId": "msg_6e3f8b41-7c02-4a95-8d16-b504e9a2f731",
"sourceOuGuid": "OU10433"
}
],
"warnings": []
}
Example, nothing was entitled to receive an event of this shape. An empty deliveries array is a real answer rather than a failure, and warnings says which condition was missing.
{
"eventId": "msg_1d4b7e02-5a93-4c68-b7f1-0e2a9c6d5384",
"simulated": true,
"deliveries": [],
"warnings": [
"No live subscription grant entitles your tenant to ADV/ADV-120 workflow.updated, so no endpoint was entitled to receive this event."
]
}
Take a deliveryId from the response and fetch it to see the envelope that was built.
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
You named an org unit you hold no rights over. Simulate takes your org unit from your authenticated identity, so this is refused before any event is fired.
{
"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, and none of them answers 403.
See 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 for another tenant’s data | Yes; a missing one is a 400 | Yes; without one it reports zero deliveries rather than an error |
| How the payload is marked | data.test: true plus a webhook-test: true header | simulated: true on the delivery row |
Getting Started uses the test operation for a first end-to-end check.