Developers

Guides · PNE

Webhooks

Webhooks let your integration be notified the moment a record is created or updated in PatientNow Essentials — no polling required. You register a URL for the events you care about; when a matching event fires, PatientNow sends an HTTP POST to that URL with a small JSON notification.

ℹ️ PatientNow Essentials only

Webhooks are a PNE feature. They are not part of the VISH or PatientNow Pro surfaces. Everything on this page targets the PNE base URL https://api.envisiongo.com/api/v1.

Overview

A webhook is a registration that pairs an event name (such as Appointment.Created) with a receiver URL you control. Register one webhook per event you want to receive. When that event occurs, PatientNow delivers a compact notification — event metadata plus a URL that points at the affected record — to your receiver. The full record is not embedded in the notification; you fetch it from the provided URL using your normal API credentials.

📎 Customer records use Patient.* event names

The API resource is customers, but the webhook events for it are named Patient.Created and Patient.Updated. There is no Customer.Created event — see Supported event names.

How delivery works

  1. You register a receiver URL for an event.
  2. Something happens in PatientNow — a customer, appointment, or order is created or updated.
  3. PatientNow sends an HTTP POST with a JSON notification to your URL.
  4. Your receiver returns a 2xx quickly to acknowledge receipt.
  5. You use the Url from the notification to fetch the full record from the API.
⚠️ There is no sandbox — writes are live

PatientNow has no test tenant. Any record you create to trigger a test webhook is created in real data. Use a disposable receiver (see Testing), but create test records with care.

Supported event names

These are the only event names accepted when registering a webhook. They are matched case-insensitively but are shown here in their canonical form. Register the specific events you want to receive.

Event nameFires when…Record the Url points to
Patient.CreatedA customer is addedcustomers/{id}
Patient.UpdatedA customer is updatedcustomers/{id}
Appointment.CreatedAn appointment is addedappointments/{id}/details
Appointment.UpdatedAn appointment is updatedappointments/{id}/details
Order.CreatedAn order is addedorders/{id}
Order.UpdatedAn order is updatedorders/{id}
📎 For create-notification workflows

If you only need to know when records first appear, register the three *.Created events: Patient.Created, Appointment.Created, and Order.Created.

Register a webhook

POST /api/v1/webhooks

Send a JSON body pairing an event name with your receiver URL. Repeat the call once per event you want to subscribe to.

Request body

FieldTypeDescription
EventName string required One of the supported event names. An unrecognized value is rejected with 400.
WebhookUrl string required Absolute http or https URL that will receive the notification. Must be publicly reachable.
curl -X POST \
  "https://api.envisiongo.com/api/v1/webhooks?apikey=YOUR_API_KEY" \
  -H "Authorization: Basic $(printf 'USERNAME:PASSWORD' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "EventName": "Appointment.Created",
    "WebhookUrl": "https://example.com/webhooks/patientnow"
  }'
const res = await fetch(
  "https://api.envisiongo.com/api/v1/webhooks?apikey=YOUR_API_KEY",
  {
    method: "POST",
    headers: {
      "Authorization": "Basic " + btoa("USERNAME:PASSWORD"),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      EventName: "Appointment.Created",
      WebhookUrl: "https://example.com/webhooks/patientnow",
    }),
  }
);
const webhook = await res.json();
import base64, requests

token = base64.b64encode(b"USERNAME:PASSWORD").decode()
res = requests.post(
    "https://api.envisiongo.com/api/v1/webhooks",
    params={"apikey": "YOUR_API_KEY"},
    headers={"Authorization": f"Basic {token}"},
    json={
        "EventName": "Appointment.Created",
        "WebhookUrl": "https://example.com/webhooks/patientnow",
    },
)
webhook = res.json()
using var client = new HttpClient();
var token = Convert.ToBase64String(
    Encoding.UTF8.GetBytes("USERNAME:PASSWORD"));
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Basic", token);

var body = new StringContent(
    "{\"EventName\":\"Appointment.Created\"," +
    "\"WebhookUrl\":\"https://example.com/webhooks/patientnow\"}",
    Encoding.UTF8, "application/json");

var res = await client.PostAsync(
    "https://api.envisiongo.com/api/v1/webhooks?apikey=YOUR_API_KEY",
    body);

Response

A successful registration returns the stored webhook record.

{
  "Id": 4271,
  "WebhookUrl": "https://example.com/webhooks/patientnow",
  "EventType": "Appointment.Created"
}
📎 Webhook Id is an integer

Unlike PNE record IDs — which are opaque encrypted strings — a webhook registration's Id is a plain integer. You use it to fetch or delete the registration.

List registered webhooks

GET /api/v1/webhooks

Returns every webhook registered for your tenant. Two optional query filters narrow the list.

ParameterTypeDescription
EventType string optional Return only registrations for this event. Must be a supported event name; an invalid value returns 400.
WebhookUrl string optional Return only registrations for this receiver URL. Must be an absolute http/https URL.
curl -X GET \
  "https://api.envisiongo.com/api/v1/webhooks?apikey=YOUR_API_KEY&EventType=Appointment.Created" \
  -H "Authorization: Basic $(printf 'USERNAME:PASSWORD' | base64)"
const res = await fetch(
  "https://api.envisiongo.com/api/v1/webhooks?apikey=YOUR_API_KEY",
  { headers: { "Authorization": "Basic " + btoa("USERNAME:PASSWORD") } }
);
const webhooks = await res.json();
import base64, requests

token = base64.b64encode(b"USERNAME:PASSWORD").decode()
res = requests.get(
    "https://api.envisiongo.com/api/v1/webhooks",
    params={"apikey": "YOUR_API_KEY"},
    headers={"Authorization": f"Basic {token}"},
)
webhooks = res.json()
using var client = new HttpClient();
var token = Convert.ToBase64String(
    Encoding.UTF8.GetBytes("USERNAME:PASSWORD"));
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Basic", token);

var res = await client.GetAsync(
    "https://api.envisiongo.com/api/v1/webhooks?apikey=YOUR_API_KEY");

Response

[
  {
    "Id": 4271,
    "WebhookUrl": "https://example.com/webhooks/patientnow",
    "EventType": "Appointment.Created"
  },
  {
    "Id": 4272,
    "WebhookUrl": "https://example.com/webhooks/patientnow",
    "EventType": "Patient.Created"
  }
]

Get a single webhook

GET /api/v1/webhooks/{id}

Retrieve one registration by its integer Id. A non-integer (or non-positive) id returns 400; an id that isn't registered to your tenant returns 404.

curl -X GET \
  "https://api.envisiongo.com/api/v1/webhooks/4271?apikey=YOUR_API_KEY" \
  -H "Authorization: Basic $(printf 'USERNAME:PASSWORD' | base64)"
{
  "Id": 4271,
  "WebhookUrl": "https://example.com/webhooks/patientnow",
  "EventType": "Appointment.Created"
}

Delete a webhook

DELETE /api/v1/webhooks/{id}

Remove a registration by its integer Id. The same validation applies: a bad id returns 400, an unknown one 404. A successful delete returns 204 No Content.

curl -X DELETE \
  "https://api.envisiongo.com/api/v1/webhooks/4271?apikey=YOUR_API_KEY" \
  -H "Authorization: Basic $(printf 'USERNAME:PASSWORD' | base64)"

The delivery payload

When an event fires, your receiver gets an HTTP POST with a JSON body like this:

{
  "Application": "PNE",
  "TenantId": "NA01_12345",
  "EventId": "0c6f94f8-4d14-41c0-b8f8-8e52d7d8c111",
  "EventType": "Appointment.Created",
  "EventTimestamp": "2026-06-02T15:30:00Z",
  "Url": "https://api.envisiongo.com/api/v1/appointments/{id}/details"
}
FieldTypeDescription
Application string Source application identifier — PNE.
TenantId string Your tenant, in server-node-prefix_tenant-id form (e.g. NA01_12345).
EventId string Unique GUID for this notification. Store it to deduplicate.
EventType string The event that occurred, e.g. Appointment.Created.
EventTimestamp string UTC timestamp (ISO 8601) when the notification was created.
Url string The API URL to call to fetch the full affected record.

Retrieving the full record

The notification carries metadata and a pointer, not the record itself. Call the Url from the payload with your normal PNE credentials — remember to append the gateway apikey, which is not part of the delivered Url.

curl -X GET \
  "https://api.envisiongo.com/api/v1/appointments/{id}/details?apikey=YOUR_API_KEY" \
  -H "Authorization: Basic $(printf 'USERNAME:PASSWORD' | base64)"
📎 IDs in the Url are opaque

The record identifiers embedded in the Url (the {id} segment) are opaque encrypted strings — pass them back exactly as received. See Guides: IDs.

Testing webhooks

To confirm end-to-end delivery without standing up a server, point a webhook at a disposable receiver such as webhook.site, then create a matching record.

  1. Open webhook.site and copy the unique URL it generates for you.
  2. Register that URL for the event you want to test — for example Patient.Created.
  3. Confirm it appears in your webhook list.
  4. Create the matching record in PatientNow (a customer for Patient.Created, an appointment for Appointment.Created, an order for Order.Created).
  5. Watch webhook.site for the incoming POST. Confirm the body is JSON, EventType matches, and EventId, EventTimestamp, and Url are present.
  6. Copy the Url and call it with your API credentials to confirm it returns the record you created.
⚠️ Disposable receivers are for testing only

webhook.site URLs are public and temporary. Never register one for a production integration, and remember there is no sandbox — the record you create to trigger the test is real.

Receiver requirements

Your webhook endpoint should:

⚠️ Respond within 15 seconds

Each delivery attempt has a 15-second timeout. If your endpoint hasn't returned a 2xx within that window, the attempt is treated as failed. Acknowledge fast and defer heavy processing — including the follow-up record fetch — to a background job.

📎 Retries on failure

A delivery that times out or returns a non-2xx response is retried. PatientNow waits 30 seconds between attempts and makes up to 10 attempts in total before giving up. Because a retried notification carries the same EventId, an idempotent receiver will safely ignore the duplicate.

A dependable receiver typically: parses the payload → validates EventType → checks whether EventId was already seen → stores the event → returns 200 OK → then fetches and processes the full record from Url.

Troubleshooting

Registered, but no event arrives

The event type is rejected at registration

Use one of the exact supported event names. In particular, there is no Customer.Created — use Patient.Created.

The payload doesn't contain the full record

That's expected. The notification carries metadata and a Url; call the Url with your credentials to retrieve the record.

Duplicate notifications

Receivers should be idempotent. Store the EventId and skip any you've already handled.

Quick reference

TaskMethodEndpoint
Register webhookPOST/api/v1/webhooks
List webhooksGET/api/v1/webhooks
Get webhook by IDGET/api/v1/webhooks/{id}
Delete webhookDELETE/api/v1/webhooks/{id}