Skip to content

Webhooks

Verify, save, and process Affinity events.

Updated View as Markdown

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

  1. Create a Test webhook endpoint.
  2. Subscribe only to the events that your service handles.
  3. Send a test event.
  4. Confirm signature failure behavior.
  5. Confirm deduplication and replay behavior.

Create the Live endpoint separately after Affinity approves Live access.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close