Skip to main content

Destinations

A destination writes a slot's band and score as person properties to a tool your teams already use, so that tool can target on them. Today the one packaged vendor is PostHog: Tessera sets person properties such as tessera_band and tessera_score_churn on the PostHog person whose distinct_id is the entity id you already hold, and PostHog feature flags and cohorts can target those properties.

Verification status

PostHog is the only destination vendor. Its delivery evidence is the production destination-writer image run against a recorded PostHog fake (batch endpoint, $set person events, deterministic event UUIDs, pacing, suppression and 401/403 handling). A delivery to a live PostHog project is unverified until a PostHog sandbox account is available. The catalog's batch size and rate defaults are conservative values, not confirmed PostHog limits.

A destination is a record in your project, created and managed through the /destinations operations. It reads a slot's stored state; it never writes model state.

What a destination needs​

  • A slot with a band binding. Band values are derived at delivery time under the slot's banding policy, bound per label with PUT /slots/{slot}/bands. A slot with no binding cannot have a destination: POST /destinations answers 400 failed_precondition.
  • A vendor, a pinned catalog version and its configuration. vendor must name a packaged catalog (posthog). An omitted catalog_version pins the latest packaged version, and catalog versions are immutable once released, so a destination's property names and limits never change under it.
  • A write-only API key. credentials.api_key is envelope-encrypted when you send it and is never returned, logged or passed through job arguments. Reads show only credentials_set: true.

A project can hold at most 8 destinations that are not deleted. Creating, updating, deleting and the actions below require the admin scope; reads require read.

Create a destination​

{
"vendor": "posthog",
"slot_id": "slot_01j9v11destinationslot0000",
"config": {
"host": "https://eu.posthog.com",
"project_id": 12345,
"property_prefix": "tessera"
},
"credentials": {
"api_key": "<your PostHog project API key>"
}
}

POST /destinations answers 201 with the destination record. A config the pinned catalog's schema does not accept, an unknown vendor or an unknown catalog_version is 400 invalid_argument. For posthog.v1 the configuration is closed: host (an https:// origin), an integer project_id and a property_prefix (lowercase letters, digits and _, default tessera).

POST /destinations/{destination_id}:test-connection sends an empty batch to the vendor with the stored key and answers 200 with status and a list of checks. A failed check is still a 200; the status says whether the key and host worked. It does not write any person property.

The properties it writes​

GET /destinations/{destination_id} returns declared_property_names: the exact names deliveries write, resolved by the same code that builds every delivery. With the default prefix and one bound label called churn:

PropertyValue
tessera_bandthe band the label's score falls in under the bound banding policy; null when the score is below every band, which clears a stale band
tessera_score_churnthe label's score: p_positive for a binary label, the predicted value for a regression label
tessera_slotthe slot id
tessera_computed_atwhen the state row was written, in UTC
tessera_banding_policy_fingerprintthe fingerprint of the banding policy the band was derived under

With more than one bound label the band property is named per label, tessera_band_churn, tessera_band_ltv and so on. A multiclass label delivers no property: it is listed in skipped_properties with the reason no_per_class_template, and Tessera never invents a single score for it. Changing property_prefix changes every name above, and the record's declared_property_names shows the result before anything is sent.

Each person update is one PostHog $set event whose distinct_id is the raw entity id and whose timestamp is computed_at.

When it delivers​

  • On a band change. When an entity crosses into another band, Tessera emits a score.band_changed event. The destination's worker receives it, reads the entity's current stored state (the event only identifies which entity to look at) and sends the update. Changes to one entity inside a short batching window collapse to the latest state.
  • On a backfill. POST /destinations/{destination_id}:backfill queues one sweep over every state row of the slot's active bundle. It is 400 failed_precondition while another backfill of the same destination is still running, or while the destination is disabled. It honors Idempotency-Key. Use it after you create a destination, and after re-enabling one, because band changes that happened while a destination was disabled are not replayed.

Suppression and holdouts​

Every batch, trigger-driven or backfill, passes through the delivery suppression filter inside the delivery worker, under the destination's effective_purpose: the purpose you declare, else the slot's purpose, else general. Suppressed entities and members of active holdout experiments on the slot are left out of the batch and counted on the receipt. If the filter cannot decide, nothing is sent.

Limits, batching and retries​

limits overrides the catalog defaults, and each override is clamped to the catalog's hard bounds:

Limitposthog.v1 defaultHard bound
max_events_per_batch1001 to 1000
requests_per_minute601 to 600

Every event carries a deterministic uuid derived from the destination, the entity, the banding policy fingerprint and computed_at. A retried or replayed batch re-sends byte-identical events with identical uuids, which lets the vendor deduplicate them.

A batch that fails is retried at +1 minute, +5 minutes, +30 minutes, +2 hours and +21 hours, and is failed after six attempts. After 20 consecutive failed deliveries the destination is disabled with disabled_reason: "sustained_failure" and its pending deliveries are marked abandoned. A 401 or 403 from the vendor stops the ladder at once: the destination is disabled with disabled_reason: "revoked_credentials".

A destination acts for the credential that created it (or last enabled it). If that credential may no longer use the destination's effective purpose, the destination is disabled with disabled_reason: null and its queued and pending deliveries are dropped. The organization audit log names the reason: a destination.auto_disable event on the destination with details {"reason": "purpose_not_allowed", "purpose": "<purpose>"}. In an organization that uses purpose allow-lists, this also happens when the credential is revoked or its member is removed or deactivated (see Delivery integrity).

To recover a disabled destination, fix the cause (for a revoked key, send a new credentials.api_key with PATCH /destinations/{destination_id}), then call POST /destinations/{destination_id}:enable, which resets the failure count, and run :backfill to catch up. For a purpose disable, call :enable with a credential that may use the purpose; it then acts for the destination. :enable on an active destination is 400 failed_precondition.

Delivery receipts​

GET /destinations/{destination_id}/deliveries lists one receipt per delivery cycle, newest first, filterable by status (pending, retrying, succeeded, failed, abandoned). A receipt records the per-attempt typed outcomes, the event uuids, the number of persons updated, the banding policy fingerprint and the suppression manifest of that cycle. It never contains person payloads, entity ids, credentials or vendor response bodies.

When a cycle reaches a terminal status, Tessera emits one destination.delivery.completed webhook whose data holds destination_id, slot_id, vendor, trigger (score.band_changed or backfill), entities_delivered, suppressed_count, withheld_count, banding_policy_fingerprint and completed_at. Persons updated are metered under export_rows with the dimensions slot_id and destination_kind: "posthog".

Deleting a destination​

DELETE /destinations/{destination_id} destroys the vaulted credentials, drops the queued entities and marks pending deliveries abandoned. No further write is made, and later reads answer 404 not_found. Person properties already set in the vendor stay there; remove them in the vendor if you no longer want them.

Data-subject deletion​

Deleting an entity (POST /entities/{entity_type}/{entity_id}:delete) removes it from the destination's queue and from deliveries still waiting to be retried. Once the deletion job completes, the entity is never sent again; an attempt already in flight when the job runs may still complete. Properties PostHog already received stay in your PostHog project: delete the person there as well.

Before you rely on it​

  • Tessera writes the property values; deciding what they trigger (flags, cohorts, campaigns) is configured in PostHog on the names listed in declared_property_names.
  • The vendor limits above are the catalog's conservative defaults, not a statement of the vendor's own current limits. Check them with the vendor if you raise a limit.

Add a destination in the console​

Open Settings, then Destinations, and choose Add destination. Pick the slot, the PostHog host and project id, and paste the project API key. An optional property prefix changes the names of the written properties (the default is tessera, giving names such as tessera_band), and an optional purpose sets the suppression purpose the destination delivers under.

Review opens a confirmation listing every value, with the key shown only as "Provided". Nothing is saved until you confirm. The key is sent once, stored encrypted and write-only, and never shown again: afterwards the console shows only that credentials are stored.

Status and the delivery log​

Open a destination to see its status, the exact property names it writes, and its delivery cycles with their typed outcomes. Tessera disables a destination after repeated delivery failures, and immediately when the vendor rejects the stored key. A disabled destination shows why and delivers nothing.

  • Test connection runs the vendor checks and shows each result.
  • Enable turns a disabled destination back on after you confirm. It does not fix the cause, so run the connection test first.
  • Backfill sends the current band and score of every entity in the slot, after you confirm. Suppression sets, the purpose and holdouts apply to every batch, as for any delivery. Only one backfill can run per destination, and a disabled destination cannot be backfilled. The API describes the sweep only when it accepts the request, so the console shows that record (queued, with the time it was requested) and says it does not update; the delivery cycles the sweep cuts appear in the delivery log, usually with the trigger backfill. A cycle whose entities are all suppressed or held out sends nothing to the vendor. A backfill runs only where the destination delivery worker is released. In an environment without it, the console says that backfill isn't available there yet instead of queuing a sweep that nothing would run.
  • Delete removes the destination and its stored credentials after you type its id.
  • Edit changes the PostHog host, project id and property prefix, the purpose and the batching and rate limits, and can replace the stored key. The vendor, slot and catalog version cannot change. Review changes lists each changed field with its current and new value, and nothing is saved until you confirm. A new key is sent once, stored encrypted and write-only, and shown only as "Replaced".

Rotate a key after the vendor rejects it​

When the vendor answers 401 or 403, Tessera disables the destination at once. To bring it back, choose Edit and enter a new project API key, run Test connection, and then choose Enable. Enabling does not replay entities that changed band while the destination was off.

API​

The console uses the public operations createDestination, listDestinations, getDestination, updateDestination, enableDestination, backfillDestination, testDestinationConnection, listDestinationDeliveries and deleteDestination; see the destinations reference.