Skip to content

Contacts, attributes and consent

A contact is one person in one workspace. You write contacts by upsert, keyed on your own id for the person, so a sync makes the same call whether or not we have seen them before. Attributes carry the facts your templates read, and each needs a definition first. Consent is something you assert, with the time you collected it: we never mint it for you, so a contact you send no consent for gets no marketing email.

Create or update a contact with a PUT to ref: plus your id for the person:

Terminal window
curl -X PUT https://api.sendtruss.com/v1/workspaces/ref:cust_4821/contacts/ref:guest_998 \
-H "Authorization: Bearer $SENDTRUSS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "ana@example.com",
"locale": "pt-BR",
"attributes": {"first_name": "Ana"},
"tags": ["returning"],
"marketing": {"state": "subscribed", "at": "2026-10-01T09:30:00Z"}
}'

The first_name attribute needs a definition before a contact can carry it; see Attributes and their definitions below.

A new reference creates the contact and answers 201; a known one updates it and answers 200. By our id the call only updates, and an unknown id is a 404, since there is no reference to create it under.

The rules you can’t guess from the field names:

  • A field you leave out is left alone. A sync can send only what changed. email is required only when the contact is created.
  • References follow the workspace rule: 1 to 128 letters, digits, ., _ and -, unique in the workspace, and no URL encoding needed.
  • Email is unique in the workspace. An upsert that would give a second contact an address another holds is refused with email_taken, and the answer’s contact names the one that has it.
  • tags is the whole set. Sending it replaces the contact’s tags. To add or remove one at a time, use Add tags to a contact and Remove a tag from a contact. A tag name new to the workspace creates the tag. Tags belong to the workspace: they are how you express one customer’s own segments.
  • locale is a language tag such as en or pt-BR. It picks the translation a template sends, ahead of the workspace’s locale.

To write many contacts at once, such as a customer’s existing list, use the batch upsert. See Sync your contacts.

You can get a contact by either address, and list a workspace’s contacts newest first, filtered by search (part of the address), status or tag (a tag’s key).

An attribute is a stable fact about a person: their first name, their plan, their home campground. A contact can only carry attributes that have a definition, and definitions belong to your platform, shared by every workspace. So {{ attributes.first_name }} means the same thing in every workspace’s email.

Create an attribute definition once, before the first contact that uses it:

Terminal window
curl -X POST https://api.sendtruss.com/v1/attribute-definitions \
-H "Authorization: Bearer $SENDTRUSS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"key": "first_name",
"name": "First name",
"type": "string"
}'
  • key is 1 to 64 lowercase letters, digits and _, unique on your platform. A taken key is refused with key_taken.
  • type is string, number, boolean or date.
  • required (default false) says every new contact must have a value, and that the value can’t be cleared later.
  • Only the name can change afterwards. The key, the type and whether it’s required are fixed, and there is no delete, because changing them would silently change what every existing contact’s value means.

In an upsert, only the attribute keys you send change, and null clears one. A key with no definition is refused.

Facts that change with each event, such as a booking’s dates, don’t belong in attributes. Send them in the event’s payload instead (Emitting events).

A contact’s status is its marketing state:

  • pending: you never sent consent, so the contact gets no marketing email. Every new contact starts here unless you send marketing.
  • subscribed: you asserted that the person agreed to marketing.
  • unsubscribed: the person said no, through you or through the link in one of our marketing emails.

You assert consent with marketing: a state of subscribed or unsubscribed, and at, the time the person gave or withdrew it. at can’t be in the future. The contact keeps when the latest accepted subscribed was given and which of your keys sent it, as marketing.consented_at and marketing.consented_by_key, so you can always answer when and how a person agreed.

The rule that protects people from stale syncs: a subscribed dated at or before the contact’s latest opt-out is refused with consent_older_than_opt_out, and nothing changes. Say your nightly sync still carries yesterday’s “yes”, and the person unsubscribed through our link this morning. The sync’s consent is older, so it can’t undo their choice. A person who opted out and wants back in rejoins through your own sign-up flow, which sends a subscribed dated after their opt-out.

When a person leaves marketing through our link, they get ten minutes to undo it, and your backend hears about both through the contact.left_marketing and contact.rejoined_marketing webhooks. An unsubscribed you send through the API takes effect at once, with no undo window and no webhook.

Consent never lifts a suppression. If an address hard-bounced, or the person marked an email as spam, a later subscribed doesn’t make us market to it again. See Transactional and marketing streams.

Transactional email doesn’t depend on marketing consent. A pending or unsubscribed contact still gets the email your emits send.

pixel_consent_at is when the person agreed to open tracking, or null to withdraw it. Send it when your platform collects that agreement.

The two mean different things:

  • Delete a contact when you no longer need the record. The contact and its tags are gone, and email already sent to it stays until it ages out. If the person comes back, you can create them again and market to them once you send their consent.
  • Erase a person when they ask to be forgotten. We remove everything we hold about them. See Privacy and erasure.