Spruce Webhooks Overview

Getting started with Spruce Webhooks

What are Spruce Webhooks?

Spruce Webhooks is a mechanism for notifying an application of changes to resources (contacts, conversations, messages, etc.) in an organization. Rather than polling the API for changes, a user can subscribe to changes by specifying a destination endpoint where the notification events should be delivered. Spruce will send an HTTP POST request to a configured endpoint when a create, update or delete event occurs in their organization. This "push" rather than "pull" model allows for an application to receive efficient and timely updates.

Why should I use Webhooks?

Webhooks are a powerful way to integrate Spruce with your application. They allow you to receive near real-time updates about changes in your organization, which can be used to trigger actions in your application. Taking action on individual events such as a new contact being created can help you keep your application in sync with Spruce.

Requesting API Access

Complete this form to request access to the API. The API is currently the only way to access Spruce Webhooks.

Registering an Endpoint for Events

After API access has been granted, an endpoint can be created using the corresponding Public API endpoint. After successful creation, the endpoint will begin receiving events. The dispatch of events to a registered endpoint can be paused or resumed using the corresponding Public API endpoint.

At create time you will receive a secret key that will be used to verify the signature of the payload. This key should be kept secure and not shared with anyone. Future listing of existing endpoints will not return the secret key.

curl --request POST \
--url https://api.sprucehealth.com/v1/webhooks/endpoints \
--header 'accept: application/json' \
--header 'authorization: Bearer <api-token>' \
--header 'content-type: application/json' \
--data '
{
  "name": "Events Endpoint",
  "url": "https://example.com/webhooks"
}
'

Types of events

All events published to an endpoint are scoped to the context of their owning organization.

Contact Events

Event TypeDescription
contact.createdOccurs when a new contact is created.
contact.updatedOccurs when a contact is modified.
contact.deletedOccurs when a contact is deleted.
{
    "eventTime": "2024-05-15T00:00:00Z",
    "object": "event",
    "type": "contact.created",
    "data": {
        "object": {
            "object": "contact",
            "apiURL": "https://api.sprucehealth.com/v1/contacts/entity_28V8BV463XXXX",
            "appURL": "https://api.sprucehealth.com/org/entity_28QNVEPK2XXXX/contact/entity_28V8BV463XXXX",
            "canDelete": false,
            "canEdit": true,
            "category": "patient",
            "created": "2024-05-15T00:00:00Z",
            "customContactFields": [],
            "displayName": "Patient Contact",
            "emailAddresses": [],
            "familyName": "Contact",
            "gender": "unknown",
            "givenName": "Patient",
            "hasAccount": false,
            "hasPendingInvite": true,
            "id": "entity_28V8BV463XXXX",
            "integrationLinks": [],
            "organizationContactFields": [],
            "phoneNumbers": [
                {
                    "displayValue": "(555) 555-5555",
                    "label": "mobile",
                    "value": "+15555555555"
                }
            ],
            "tags": []
        }
    }
}

Conversation Events

Event TypeDescription
conversation.createdOccurs when a new conversation is created.
conversation.updatedOccurs when a conversation is modified.
conversation.deletedOccurs when a conversation is deleted.
{
  "eventTime": "2024-05-15T00:00:00Z",
  "object": "event",
  "type": "conversation.created",
  "data": {
    "object": {
      "apiURL": "https://api.sprucehealth.com/v1/conversations/t_29S7786ETXXXX",
      "appURL": "https://api.sprucehealth.com/org/entity_28QNVEPK2XXXX/thread/t_29S7786ETXXXX",
      "archived": false,
      "associatedContactIds": null,
      "createdAt": "2024-05-15T00:00:00Z",
      "externalParticipants": [
        {
          "contact": "entity_28SQM2TF8XXXX",
          "displayName": "Patient Contact"
        }
      ],
      "id": "t_29S7786ETXXXX",
      "internalEndpoint": {
        "channel": "secure",
        "displayValue": "https://api.sprucehealth.com/organizationendpoint",
        "id": "ZW50aXR5XzI4UU5WRVBLMk9PMDI6XXXXYW5pemF0aW9uQ29kZV8yOFFOVkVRUElPTzAwOnNlY3XXXX==",
        "isInternal": true,
        "object": "endpoint",
        "rawValue": "organizationendpoint"
      },
      "internalMemberIds": [],
      "isReadOnly": false,
      "lastMessageAt": "2024-05-15T00:00:00Z",
      "object": "conversation",
      "tags": [],
      "title": "Example Conversation",
      "type": "secure"
    }
  }
}

Conversation Item Events

Event TypeDescription
conversationItem.createdOccurs when a new conversation item is created.
conversationItem.updatedOccurs when a conversation item is modified.
conversationItem.deletedOccurs when a conversation item is deleted.
conversationItem.restoredOccurs when a conversation item is restored.
{
  "eventTime": "2024-05-15T00:00:00Z",
  "object": "event",
  "type": "conversationItem.created",
  "data": {
    "object": {
      "apiURL": "https://api.sprucehealth.com/v1/conversationItems/ti_29PN3P8GEXXXX",
      "appURL": "https://api.sprucehealth.com/org/entity_28QNVEPK2XXXX/thread/t_29BJ656OKLG00/message/ti_29PN3P8GEXXXX",
      "attachments": [],
      "author": {
        "displayName": "Example Teammate"
      },
      "buttons": null,
      "conversationId": "t_29S7786ETXXXX",
      "createdAt": "2024-05-15T00:00:00Z",
      "direction": "outbound",
      "id": "ti_29PN3P8GEXXXX",
      "isInternalNote": false,
      "modifiedAt": "2024-05-15T00:00:00Z",
      "object": "conversationItem",
      "pages": [],
      "text": "Hello!"
    }
  }
}

Mapping Conversation Item Events to Contacts

When a conversation item event is received, the field containing the conversation id can be used in conjunction with the conversation endpoint to retrieve information about the containing thread.

After fetching the containing conversation, the externalParticipants field of the conversation structure can be used to determine what (if any) contacts are participating in the conversation. The structure of the externalParticipants field is dependent on the conversation type.

Mapping Conversations With Saved Contacts

For conversations with saved contacts, the list of externalParticipants will contain a contact field. The contact field of each externalParticipant will contain the id of the corresponding Spruce contact. The id can be used to fetch the relevant contact information from the contact endpoint.

Example Secure Conversation with Saved Contacts

{
  "conversation": {
    "type": "secure",
    "externalParticipants": [
      {
        "displayName": "Patient Contact",
        "contact": "entity_28SQM2TF8XXXX"
      }
    ],
    ...
  }
}

Example SMS Conversation with Saved Contacts

{
  "conversation": {
    "type": "phone",
    "externalParticipants": [
      {
        "displayName": "Patient Contact",
        "contact": "entity_28SQM2TF8XXXX",
        "endpoint": {
          "channel": "phone",
          "displayValue": "(555) 555-5555",
          ...
        }
      }
    ],
    ...
  }
}

Mapping Conversations With Unsaved or Multiple Matching Contacts

For conversations with unsaved contacts, or conversations with multiple matching contact, the externalParticipants field will contain information about the external endpoint that the conversation is associated with, but the contact field will be missing or empty.

Example SMS conversation with Unsaved Contact or Multiple Matching Contacts

{
  "conversation": {
    "type": "phone",
    "externalParticipants": [
      {
        "displayName": "(555) 555-5555",
        "endpoint": {
          "channel": "phone",
          "rawValue": "+15555555555",
          ...
        }
      }
    ],
    ...
  }
}

For conversations with multiple matching contacts the rawValue field of the endpoint returned in the externalParticipant can be used to perform a contact search to retrieve the set of relevant potential contacts.

Unsaved contacts cannot be fetched from the contact search endpoint. In this case an empty result set will be returned.

Endpoint Requirements

Spruce will only send events to endpoints that support HTTPS. The endpoint must be able to respond with a 2XX status code within 5 seconds.

Event Ordering

Events are sent in the order they occur. However, events may not always be delivered in the order they were created. Event recipients should be able to handle events that arrive out of order.

Retry Behavior

If your endpoint does not respond with a 2XX, we will continue to retry the event every 2 minutes for a maximum of 10 attempts.

Signature Verification

Spruce will sign the payload with a secret key that is provided to you at the time of registering an endpoint for events. The signature will be included in the X-Spruce-Signature header. You can verify the signature by hashing the payload with the secret key using the SHA256 algorithm and comparing it to the Base64 decoded signature.

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
	"net/http"
    "io"
)

func verifySignature(endpointSecret []byte, r *http.Request) (bool, error) {
    signature, err := base64.StdEncoding.DecodeString(r.Header.Get("X-Spruce-Signature"))
    if err != nil {
        return false, err
    }
    body, err := io.ReadAll(r.Body)
    if err != nil {
        return false, err
    }
    h := hmac.New(sha256.New, endpointSecret)
    h.Write(body)
    return hmac.Equal(h.Sum(nil), signature), nil
}

Rate Limiting

Endpoints are limited to receiving 1000 events per minute. If this limit is exceeded events will resume sending after the rate limit window has passed.