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.


Copyright © Pitchpoint Solutions. All rights reserved.