> 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

> 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

Each event type has its own page with the conditions that fire it, its
fields, and an example payload.

* Model Sync: [`sync.running`](/guides/events/types/sync-running), [`sync.failed`](/guides/events/types/sync-failed), [`sync.canceled`](/guides/events/types/sync-canceled), [`sync.completed`](/guides/events/types/sync-completed), [`sync.completed_with_errors`](/guides/events/types/sync-completed-with-errors)
* Bulk Sync: [`bulk_sync.running`](/guides/events/types/bulk-sync-running), [`bulk_sync.completed`](/guides/events/types/bulk-sync-completed), [`bulk_sync.canceled`](/guides/events/types/bulk-sync-canceled), [`bulk_sync.failed`](/guides/events/types/bulk-sync-failed), [`bulk_sync.completed_with_errors`](/guides/events/types/bulk-sync-completed-with-errors)
* Ingestion: [`ingestion.stalled`](/guides/events/types/ingestion-stalled), [`ingestion.recovered`](/guides/events/types/ingestion-recovered)

## 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`](/guides/events/types/sync-completed) event includes `logs_url`, the
endpoint that returns an index of the record logs the execution produced.

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