## Overview

StreamAPI webhooks push real-time **data change events** to your application when entities (e.g., operatories, patients, appointments) are created, updated, or deleted. Notifications are delivered over **Amazon SNS (Simple Notification Service)**. Your server receives an HTTP POST for each event and must handle both **subscription confirmation** and **notification** payloads.

This guide explains how to create and set up webhooks specifically for the StreamAPI: registering your endpoint, understanding the payload structure, confirming subscriptions, and verifying message signatures.

## Prerequisites

- **StreamAPI access** – Your application must be authorized to use the StreamAPI (see [Obtaining Production Access](/publicapi/obtaining-prod-access) if applicable).
- **Public endpoint** – Your webhook URL must be reachable from the internet over **HTTPS**. AWS SNS cannot deliver to private IPs or localhost unless you use a tunnel (e.g., ngrok) for development.
- **HTTP server** – A server that accepts `POST` requests and can respond quickly (e.g., return 2xx within a few seconds) so SNS does not retry unnecessarily.


## Creating and Registering Your Webhook

### 1. Expose a webhook endpoint

Your application must expose an HTTP endpoint that:

- Accepts **POST** requests.
- Accepts a **JSON** or **text/plain** body (SNS may send JSON as `text/plain`).
- Returns a **2xx** status code after processing (or immediately after receiving) the request so SNS considers the delivery successful.


Example URL form:

```text
https://your-domain.com/webhook
```

For local development, use a tunnel (e.g., ngrok) so SNS can reach your machine:

```text
https://abc123.ngrok.io/webhook
```

### 2. Register the endpoint with the platform

Register your webhook URL with the StreamAPI by submitting a support ticket.

- Provide the full webhook URL (HTTPS).


After registration, the platform creates an SNS subscription for your endpoint.

### 3. Confirm the subscription

When you first register, AWS SNS sends a **SubscriptionConfirmation** message to your URL. You **must** confirm the subscription or you will not receive future notifications.

- **Type:** `SubscriptionConfirmation`
- **SubscribeURL:** A one-time URL you must **GET** (or **POST**) from your server. When SNS receives this request, the subscription is confirmed.
- Your endpoint should:
  1. Parse the incoming POST body as JSON.
  2. If `Type === "SubscriptionConfirmation"`, call the `SubscribeURL` (e.g., with an HTTP GET).
  3. Return 2xx to SNS.


If you do not confirm, the subscription stays pending and no data change notifications will be delivered.

## Payload Structure

StreamAPI webhooks use the standard **AWS SNS** message envelope. The body of the POST is a single JSON object.

### Top-level SNS envelope

| Field | Description |
|  --- | --- |
| **Type** | `Notification`, `SubscriptionConfirmation`, or `UnsubscribeConfirmation` |
| **MessageId** | Unique ID for the message (use for idempotency). |
| **TopicArn** | SNS topic ARN (e.g., `arn:aws:sns:us-east-1:…:Org_<orgId>`). |
| **Message** | For `Type === "Notification"`, a **string** containing a JSON object with the StreamAPI event. |
| **Timestamp** | ISO 8601 timestamp when SNS sent the message. |
| **SignatureVersion** | Signature version (e.g., `1`). |
| **Signature** | Base64-encoded signature for verification. |
| **SigningCertURL** | HTTPS URL to the AWS certificate used to sign the message. |
| **UnsubscribeURL** | URL to unsubscribe (for `Notification`). |
| **MessageAttributes** | Optional attributes (e.g., `RoutingKey`, `organizationId`, `MessageType`). |


For **SubscriptionConfirmation**, the body also includes:

- **SubscribeURL** – URL to call to confirm.
- **Token** – Subscription token.


### Inner message (StreamAPI event)

When `Type` is `Notification`, the **Message** field is a **stringified JSON** object. Parse it to get the StreamAPI payload:

| Field | Description |
|  --- | --- |
| **messageType** | e.g., `DataChange`. |
| **applicationId** | Application identifier. |
| **clientName** | Client name (e.g., `Developer`). |
| **type** | Entity type (e.g., `OperatoryV1`, `PatientV1`). |
| **id** | Entity ID. |
| **operation** | `CREATE`, `UPDATE`, or `DELETE`. |
| **payload** | Entity payload (varies by type). |
| **transactionId** | Transaction ID. |
| **correlationId** | Correlation ID for tracing. |
| **sendTime** | Epoch milliseconds when the event was sent. |


### MessageAttributes (StreamAPI)

`MessageAttributes` on the SNS envelope often include:

| Attribute | Description |
|  --- | --- |
| **RoutingKey** | Format: `{organizationId}.{locationId}.{EntityType}.{OPERATION}` (e.g., `5d111c20ca7b487854a79d0d.-1.OperatoryV1.UPDATE`). |
| **organizationId** / **OrganizationId** | Organization ID. |
| **locationId** | Location ID (`-1` if org-level). |
| **MessageType** | e.g., `StreamAPI-Internal`. |


You can use **RoutingKey** or **MessageAttributes** to filter or route events without parsing the full `Message` body.

## Example notification payload

Below is an example of a **Notification** payload for a StreamAPI data change event.

```json
{
  "Type": "Notification",
  "MessageId": "1899eea0-81b3-5112-88dd-8cae7f6399b5",
  "TopicArn": "arn:aws:sns:us-east-1:393668382772:Org_5d111c20ca7b487854a79d0d",
  "Message": "{\"messageType\":\"DataChange\",\"applicationId\":\"hAnIzDLqlKBpPz2hscufe4HpuGmmkpl4\",\"clientName\":\"Developer\",\"type\":\"OperatoryV1\",\"id\":\"64000000000005\",\"operation\":\"UPDATE\",\"payload\":{\"shortName\":\"testop14\",\"id\":\"64000000000005\",\"type\":\"OperatoryV1\"},\"transactionId\":\"116c84a5-33ba-4d4c-b46c-3ebce5d506e0\",\"correlationId\":\"116c84a5-33ba-4d4c-b46c-3ebce5d506e0\",\"sendTime\":1771371664352}",
  "Timestamp": "2026-02-17T23:41:04.426Z",
  "SignatureVersion": "1",
  "Signature": "fANin6ysjQQknuP7tRSjvDrnWacHtcVrFleUWizAQ6lp1xMATOuVzDpnAEcTkAYdb7TqmeFUQXRP4DYOaRgQ6KMnvApgS1axLMI5nBEe/AdjFBeRR9GJwBNwX/0PcIDlySY+MMwU2eXg8V9ud+/RvkQPlECxm3CAJv+9ItdxXSd2jGqfQ+eVJyyLBhqnBXjwpyh7d5evwks3Dv9pB+4Mu717KpT9QAR/yCc3LXWUh4DgeCTlCZYS8xcgr0LgwVck2WIQnDBLeDkJgwYvNJLfh6Mwq2q+HaErDnccRygMsRky7Csfy41EypirEFZllSOIHAzK/oPSNtL/ehod3mlzDw==",
  "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-7506a1e35b36ef5a444dd1a8e7cc3ed8.pem",
  "UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:393668382772:Org_5d111c20ca7b487854a79d0d:f47e759a-a606-4302-82ba-f2b1bedd6618",
  "MessageAttributes": {
    "RoutingKey": {"Type":"String","Value":"5d111c20ca7b487854a79d0d.-1.OperatoryV1.UPDATE"},
    "organizationId": {"Type":"String","Value":"5d111c20ca7b487854a79d0d"},
    "locationId": {"Type":"String","Value":"-1"},
    "OrganizationId": {"Type":"String","Value":"5d111c20ca7b487854a79d0d"},
    "MessageType": {"Type":"String","Value":"StreamAPI-Internal"}
  }
}
```

Parsed **Message** (inner event):

```json
{
  "messageType": "DataChange",
  "applicationId": "hAnIzDLqlKBpPz2hscufe4HpuGmmkpl4",
  "clientName": "Developer",
  "type": "OperatoryV1",
  "id": "64000000000005",
  "operation": "UPDATE",
  "payload": {
    "shortName": "testop14",
    "id": "64000000000005",
    "type": "OperatoryV1"
  },
  "transactionId": "116c84a5-33ba-4d4c-b46c-3ebce5d506e0",
  "correlationId": "116c84a5-33ba-4d4c-b46c-3ebce5d506e0",
  "sendTime": 1771371664352
}
```

## Verifying message signatures

To ensure messages are from AWS SNS and have not been altered, verify the **Signature** using the certificate at **SigningCertURL**.

1. **Validate SigningCertURL** – Allow only URLs from `https://sns.<region>.amazonaws.com/` (or `https://sns.amazonaws.com/`). Reject any other host.
2. **Download the certificate** – Fetch the PEM from `SigningCertURL` over HTTPS (cache it by URL for a short TTL to avoid repeated downloads).
3. **Build the string to sign** – For `Notification`, AWS documents the canonical form using the fields: `Message`, `MessageId`, `Subject`, `Timestamp`, `TopicArn`, `Type`, in that order, with each key and value followed by a newline. For `SubscriptionConfirmation` and `UnsubscribeConfirmation`, use the field set that includes `SubscribeURL` or `UnsubscribeURL` and `Token`. Follow [AWS SNS Verify Signature](https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html).
4. **Verify** – Use the certificate’s public key and the algorithm (e.g., RSA-SHA1 for SignatureVersion 1) to verify the **Signature** (Base64-decoded) against the string to sign.


Reject any message that fails verification.

## Handling duplicate deliveries

SNS may deliver the same notification more than once. Use **MessageId** (and optionally **transactionId** / **correlationId** inside **Message**) to deduplicate:

- Store the last N **MessageId** values (or use a short-lived cache).
- If you have already processed a given **MessageId**, return 2xx and skip processing again.


This keeps your handling idempotent and avoids duplicate side effects.

## Summary checklist

| Step | Action |
|  --- | --- |
| 1 | Expose an HTTPS endpoint that accepts POST and returns 2xx. |
| 2 | Register that URL as your StreamAPI webhook in the platform. |
| 3 | On first request, if `Type === "SubscriptionConfirmation"`, call `SubscribeURL` to confirm. |
| 4 | For `Type === "Notification"`, parse `Message` as JSON and process the StreamAPI event. |
| 5 | Verify the SNS signature using `SigningCertURL` and reject invalid messages. |
| 6 | Use `MessageId` (and optionally routing attributes) for idempotency and filtering. |


For more on the StreamAPI and authentication, see the [API Consumer Guide](/publicapi/api-consumer-guide) and [API Best Practices](/publicapi/api-best-practices).