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.
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 /destinationsanswers400 failed_precondition. - A vendor, a pinned catalog version and its configuration.
vendormust name a packaged catalog (posthog). An omittedcatalog_versionpins 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_keyis envelope-encrypted when you send it and is never returned, logged or passed through job arguments. Reads show onlycredentials_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:
| Property | Value |
|---|---|
tessera_band | the 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_churn | the label's score: p_positive for a binary label, the predicted value for a regression label |
tessera_slot | the slot id |
tessera_computed_at | when the state row was written, in UTC |
tessera_banding_policy_fingerprint | the 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_changedevent. 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}:backfillqueues one sweep over every state row of the slot's active bundle. It is400 failed_preconditionwhile another backfill of the same destination is still running, or while the destination is disabled. It honorsIdempotency-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:
| Limit | posthog.v1 default | Hard bound |
|---|---|---|
max_events_per_batch | 100 | 1 to 1000 |
requests_per_minute | 60 | 1 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.