> This page is for version 2025-09-18 (default).
> For other versions, use one of these documentation indexes:
> - 2025-09-18 (default): https://apidocs.polytomic.com/2025-09-18/llms.txt
> - 2024-02-08: https://apidocs.polytomic.com/2024-02-08/llms.txt
> - 2023-04-25: https://apidocs.polytomic.com/2023-04-25/llms.txt
> - 2022-12-12: https://apidocs.polytomic.com/2022-12-12/llms.txt
> - 2021-05-23: https://apidocs.polytomic.com/2021-05-23/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://apidocs.polytomic.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://apidocs.polytomic.com/_mcp/server.

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](/api-reference/events/list).
* 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](/api-reference/webhooks/list). 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:

| Header              | Value                                                                        |
| ------------------- | ---------------------------------------------------------------------------- |
| `webhook-id`        | The event UUID, matching the body `id`.                                      |
| `webhook-timestamp` | The delivery attempt time as Unix seconds. This can change between attempts. |
| `webhook-signature` | `v1,` 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**:

```text
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

```json
{
	"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"
   }
}
```

\


```json
{
	"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"
  }
}
```

\


```json
{
	"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"
	}
}
```

\


```json
{
	"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.

```go
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))
}
```