Events

Events notify you when important actions happen inside an organization. For example, Polytomic emits a sync.completed event every time a sync finishes.

You can consume events in two ways:

  • Poll the events endpoint.
  • Register a webhook to receive events in real time.

⚠️ At-most-once delivery

Polytomic delivers each event at most once and does not re-send events after delivery. If you need guaranteed processing of every change, build a fallback that polls the events endpoint in addition to consuming the webhook.

Retention

Polytomic retains events for 48 hours. After that they are no longer available from the events endpoint.

Event types

The event types are as follows:

  • sync.running
  • sync.failed
  • sync.canceled
  • sync.completed
  • sync.completed_with_errors
  • bulk_sync.running
  • bulk_sync.completed
  • bulk_sync.canceled
  • bulk_sync.failed
  • bulk_sync.completed_with_error

Webhooks

Create and manage webhooks through the webhook API. A webhook fires events for the organization it belongs to. Each organization can have one webhook.

Each delivery is an HTTP POST with a JSON body. The top-level id is the event’s UUID and stays the same across delivery attempts. The top-level created_at is the event creation time in UTC, in RFC 3339 format. The type names the event, and event contains its type-specific fields.

Verify webhook signatures

When you create or update a webhook, you supply a secret. Polytomic sends these headers with each delivery:

HeaderValue
webhook-idThe event UUID, matching the body id.
webhook-timestampThe delivery attempt time as Unix seconds. This can change between attempts.
webhook-signaturev1, followed by a base64-encoded HMAC-SHA256 digest.

To verify webhook-signature, compute HMAC-SHA256 with the UTF-8 bytes of your webhook secret as the key. Sign the following bytes, using the unmodified request body:

webhook-id + "." + webhook-timestamp + "." + raw request body

Decode the base64 value after v1, and compare the digests in constant time. Check that the timestamp falls within a freshness window suitable for your endpoint before processing the event. Also check that the body id matches webhook-id. Use that ID to recognize repeat attempts; the timestamp and signature can differ for the same event.

Polytomic also sends Polytomic-Signature for existing consumers. Its value is the lowercase hex-encoded HMAC-SHA256 of the raw request body, using the same secret. You can continue verifying this header while migrating to webhook-signature.

Delivery

Your endpoint must return a 2xx status code. If it does not, Polytomic retries the delivery up to five times with exponential backoff. Event ordering is not guaranteed.

Record logs

The sync.completed event payload (see the example below) includes links to JSON logs of the records Polytomic inserted or updated. See the total_records, inserted_records, and updated_records fields.

Event payload examples

{
"id": "962b37a6-e644-4a13-bb5e-f5dc05c414a5",
"created_at": "2024-01-01T00:00:00Z",
"type": "bulk_sync.running",
"event": {
"name": "Asana to BigQuery sync",
"organization_id": "be80a27e-0e80-4dcb-bee9-1666f02eeb83",
"sync_id": "dcc891b3-4a25-4b8b-bba8-48104cd66525",
"execution_id": "8114c8cc-99fc-4fb4-ab72-28308762fa63",
"source_connection_id": "ad56197c-1bca-4256-a410-fb1ffde295c0",
"destination_connection_id": "318dba62-d875-11ed-b59b-ea7534cffcab"
}
}

{
"id": "8af2ae4e-ff93-4782-90f2-8af8e6797fd4",
"created_at": "2024-01-01T00:01:00Z",
"type": "bulk_sync.completed",
"event": {
"name": "Asana to BigQuery sync",
"organization_id": "be80a27e-0e80-4dcb-bee9-1666f02eeb83",
"sync_id": "dcc891b3-4a25-4b8b-bba8-48104cd66525",
"execution_id": "8114c8cc-99fc-4fb4-ab72-28308762fa63",
"source_connection_id": "ad56197c-1bca-4256-a410-fb1ffde295c0",
"destination_connection_id": "318dba62-d875-11ed-b59b-ea7534cffcab"
}
}

{
"id": "49f11fd6-23e3-40b6-81a8-0a0b896c9379",
"created_at": "2024-01-01T00:02:00Z",
"type": "sync.running",
"event": {
"name": "Salesforce Sync",
"organization_id": "be80a27e-0e80-4dcb-bee9-1666f02eeb83",
"execution_id": "2a42c650-b741-4282-a4c7-19de797aa18b",
"sync_id": "d09ceb2a-1641-4dc8-bdfe-d019d8964043",
"target_connection_id": "7c67e0b3-9759-44eb-b96c-2a7042b583f0"
}
}

{
"id": "d4f24a0b-bf94-49d0-98e8-3b7acdbce8df",
"created_at": "2024-01-01T00:03:00Z",
"type": "sync.completed",
"event": {
"name": "Salesforce Sync",
"organization_id": "be80a27e-0e80-4dcb-bee9-1666f02eeb83",
"sync_id": "d09ceb2a-1641-4dc8-bdfe-d019d8964043",
"execution_id": "2a42c650-b741-4282-a4c7-19de797aa18b",
"status": "completed",
"total_records": [
"https://app.polytomic.com/api/syncs/54d2d580-d910-4bc7-834b-92d57ca89762/executions/c9fd1b24-0b00-46e8-905b-839f919adffd/records/log1688189538-0d74ec71-4540-4706-867f-e6263070e058.json"
],
"inserted_records": null,
"updated_records": [
"https://app.polytomic.com/api/syncs/54d2d580-d910-4bc7-834b-92d57ca89762/executions/c9fd1b24-0b00-46e8-905b-839f919adffd/updates/log1688189542-05f04ffa-3c85-41eb-8333-9d951f93405b.json",
"https://app.polytomic.com/api/syncs/54d2d580-d910-4bc7-834b-92d57ca89762/executions/c9fd1b24-0b00-46e8-905b-839f919adffd/updates/log1688189552-6c0bcf4a-b1ae-4cf5-8ee2-dcb6d6fbb728.json"
],
"trigger": "manual",
"target_connection_id": "7c67e0b3-9759-44eb-b96c-2a7042b583f0"
}
}

Consuming events

The following Go example verifies a delivery’s signature, timestamp, and event ID before accepting it. Replace somepassword with your webhook secret and process the event after verification.

package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
)
var secret = []byte("somepassword")
func main() {
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
id := r.Header.Get("webhook-id")
timestamp := r.Header.Get("webhook-timestamp")
version, encoded, ok := strings.Cut(r.Header.Get("webhook-signature"), ",")
seconds, err := strconv.ParseInt(timestamp, 10, 64)
if id == "" || !ok || version != "v1" || err != nil {
http.Error(w, "invalid webhook headers", http.StatusUnauthorized)
return
}
age := time.Since(time.Unix(seconds, 0))
if age < -5*time.Minute || age > 5*time.Minute {
http.Error(w, "expired webhook", http.StatusUnauthorized)
return
}
given, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(id + "." + timestamp + "."))
mac.Write(body)
if !hmac.Equal(given, mac.Sum(nil)) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var event struct {
ID string `json:"id"`
}
if json.Unmarshal(body, &event) != nil || event.ID != id {
http.Error(w, "invalid event ID", http.StatusBadRequest)
return
}
// Use event.ID to make your event processing idempotent.
w.WriteHeader(http.StatusNoContent)
})
log.Fatal(http.ListenAndServe(":8000", nil))
}