Affinity sends signed webhook events for asynchronous changes. Endpoint URLs must use public HTTPS and contain at most 2,048 characters. This service limit keeps endpoint requests bounded; it is not a general URL standard. Configure separate endpoints in Test mode and Live mode.
Choose the endpoint owner
Practice, pharmacy, and platform API keys manage their own endpoints at /v1/webhook-endpoints.
Without an organization header, the API uses the key’s organization.
Each endpoint response includes its owning organizationId.
The owner determines which endpoints and event history you can manage.
The subscribedEvents and practiceIds fields determine which authorized events an endpoint receives.
Receive practice events as a platform
Use your platform key without an organization header to create a platform-owned endpoint.
Set practiceIds to select connected practices in the key’s mode:
{
"url": "https://your-service.example/affinity/events",
"subscribedEvents": ["order.created", "order.signed"],
"practiceIds": ["prac_..."]
}This filter narrows delivery. It does not grant access to another practice’s events. For orders, the platform receives events for orders attributed to that platform. Connecting a practice does not subscribe the platform to all orders in that practice.
An empty practiceIds array receives all otherwise-authorized events.
A nonempty filter excludes events without a matching practice.
Only platform-owned endpoints accept a nonempty practice filter.
On updates, omitted practiceIds preserves the filter; an empty array removes it.
Subscription changes apply to newly generated events.
Delegate endpoint management
A practice or pharmacy can grant a platform permission to manage its endpoints. The endpoint remains owned by the practice or pharmacy.
First, use the owner’s API key to grant webhook access:
PUT /v1/webhook-grants/acct_<platform-id>
Authorization: Bearer <owner-api-key>
Idempotency-Key: <unique-request-key>
Content-Type: application/json
{"scopes":["webhooks:read","webhooks:write"]}The owner key requires webhooks:write.
A practice must already be connected to the platform in the selected mode.
A platform cannot grant itself access.
Grants apply only to webhook resources and only in the owner’s key mode.
Then use the platform key with the owner’s public organization ID:
GET /v1/webhook-endpoints
Authorization: Bearer <platform-api-key>
X-Affinity-Organization-Id: <owner-organization-id>Use the same header when updating, disabling, testing, rotating secrets, or reading and replaying the owner’s events.
The platform key and the grant must both permit the requested action.
An unauthorized organization selection returns 403; the API does not fall back to the key’s organization.
The header does not change the caller’s identity or the key’s Test or Live mode.
Use List access grants to inspect grants with the owner’s key. Use Revoke webhook access to remove a platform’s access. Revocation also prevents access to saved mutation responses. Existing endpoints remain owned by the practice or pharmacy and continue operating after revocation.
All API-key mutations require Idempotency-Key.
Use a different idempotency key when changing the selected organization.
Follow signing and fulfillment
New orders are unsigned drafts. A clinician opens the order, reviews its prescriptions, and signs or rejects it. There is no separate review-request or approval step.
Use order.updated for draft edits, order.signed for the signature, and order.rejected for a clinician’s rejection. Signing does not confirm pharmacy submission. Follow order.submitted, order.accepted, and shipment events for fulfillment.
A rejection closes the complete unsigned order permanently. Snapshot events include the decision reason, time, clinician identity, and prescribing provider. Thin events contain the order ID; retrieve the authenticated order to read the decision.
Historical order.review_requested and order.changes_requested events remain readable and replayable. New orders do not emit them.
Verify the signature
Pass the unmodified request bytes to the TypeScript SDK. Verify the signature before you parse the JSON body.
import { AffinityWebhookVerificationError, verifyAffinityWebhook } from "@affinity-health/sdk";
export async function handleAffinityWebhook(request: Request) {
try {
const event = await verifyAffinityWebhook({
body: await request.arrayBuffer(),
secret: process.env.AFFINITY_WEBHOOK_SECRET!,
signature: request.headers.get("affinity-signature"),
});
await saveEvent(event);
return new Response(null, { status: 204 });
} catch (error) {
if (error instanceof AffinityWebhookVerificationError) {
return Response.json({ error: error.code }, { status: 400 });
}
throw error;
}
}The signature header uses a timestamp and an HMAC-SHA256 digest. The SDK checks both values.
Process each event once
Affinity can deliver the same event more than once. Store the event ID in the same transaction as your state change.
Save the event durably, then return 2xx. Process it asynchronously after acknowledgment.
Retrieve the current API resource in that background job when event order matters.
Do not wait for order retrieval or other downstream work before responding.
Delivery order and current state
Affinity does not guarantee event delivery order. For example, order.submitted can arrive before
order.created or order.signed. Retries and manual replay can deliver older events after newer ones.
This follows Stripe’s event-ordering guidance.
Verify the signature, durably deduplicate by event ID, and acknowledge with 2xx. In your background
job, retrieve the current order with the same organization and mode before updating its displayed
state. Do not overwrite current state with an older snapshot. Event timestamps describe event
creation, not delivery sequence, and two events can share a timestamp.
The 2026-09-28 webhook contract uses the following order status values. An unsigned order has
draft in a webhook snapshot and requires_provider_signature in an order API response. A signed
order has ready in both. A partially submitted batch has submitted, processing, shipped, or
blocked in a webhook snapshot, based on its fulfillment progress. Its order API response can use
partially_submitted. The order.accepted event name does not create a new status. The Sign order
response returns ready.
These webhook values also apply to retries, replays, event-detail payloads, and
data.previous_attributes. Affinity keeps the stored event snapshot unchanged. Retrieve the order
for its current status.
Timeouts, retries, and suspension
Affinity allows three seconds for DNS resolution and the TCP/TLS connection and 15 seconds for the HTTP response.
A connected receiver has the remaining response time to save and acknowledge the event.
DNS failures report dns_error; the connection deadline reports connect_timeout; an overdue response reports response_timeout.
Network failures, 408, 409, 425, 429, and 5xx responses use the full retry schedule.
After the first attempt, the delays between attempts are:
| Mode | Retry delays |
|---|---|
| Test | 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours |
| Live | 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours, 6 hours, 18 hours, 2 days, 3 days |
Other 4xx responses retry after 2 minutes and 30 minutes. Each delay varies by up to 20 percent.
Redirects fail without a retry. Affinity never follows the redirect.
consecutiveFailures counts deliveries that have exhausted their retries, including failures with no retry.
It does not count individual failed attempts while a delivery is retrying.
A successful delivery resets the counter. Three consecutive exhausted deliveries suspend the endpoint.
Use the attempt records to inspect failures that are still retrying.
After correcting the receiver, reactivate the endpoint with a partial update:
PATCH /v1/webhook-endpoints/whe_<endpoint-id>
Authorization: Bearer <api-key>
Idempotency-Key: <unique-request-key>
Content-Type: application/json
{"status":"active"}Reactivation resets the failure counter. Omitted fields retain their existing values. Optional fields do not accept null: clear a description with "", or clear subscriptions and practice filters with [].
Replay failed events after reactivation to recover missed updates.
Event names
An empty subscribedEvents array subscribes to all supported events. Unknown names return 400.
| Events | Meaning |
|---|---|
webhook_endpoint.test |
Endpoint test |
order.created, order.updated |
Draft creation and order changes |
order.signed, order.rejected |
Clinician decision |
order.submitted, order.accepted, order.processing |
Submission and pharmacy progress |
order.shipped, order.delivered |
Shipment progress |
order.blocked, order.cancelled |
Fulfillment exception or confirmed cancellation |
cancellation.requested, cancellation.sent |
Cancellation is pending |
cancellation.confirmed |
Pharmacy cancellation is confirmed |
cancellation.rejected, cancellation.failed, cancellation.too_late |
Cancellation did not complete |
order.review_requested, order.changes_requested |
Historical review events, available for replay |
Protect webhook data
Thin events contain a resource ID and object type. Read the authenticated resource when you need current or sensitive data.
Do not write request bodies, secrets, patient data, or prescription data to application logs.
Test the endpoint
- Create a Test webhook endpoint.
- Subscribe only to the events that your service handles.
- Send a test event.
- Confirm signature failure behavior.
- Confirm deduplication and replay behavior.
Create the Live endpoint separately after Affinity approves Live access.