clickSomo

Contacts & consent

A list you can defend,not just a list you have.

Anyone can hold ten thousand e-mail addresses. The question that matters — the one a regulator, a provider and your own conscience all ask in slightly different words — is where each of them came from and on what basis you may write to it. ClickSomo makes that a stored fact rather than something you would have to reconstruct.

Identity is the e-mail address.

One contact per address per project, enforced by the database rather than by hope. Addresses are normalised when they are written, never when they are read — normalise on read and the constraint is a decoration that lets Anna@ and anna@ both in.

Phone numbers are stored in E.164 where they exist, but a phone number is an attribute, not an identity. There is no identity-resolution graph, no merge protocol and no audit trail of merges, because every contact here arrives through a channel that already has an address: an import, a form, a subscribe, or your own code.

The name is stored whole

display_name is kept exactly as it was given and is never split into parts. Name splitting is a bug factory — "van der Berg", "Kim Min-jun", mononyms, titles, and every ordering convention that is not Anglo — and a wrong split surfaces in a merge tag as "Dear van,", which is worse than no personalisation at all.

Structured parts exist only for sources that supply them already separated. Nothing derives them. And a name somebody typed themselves is never overwritten by a later CSV that happened to observe something different, because last-write-wins lets the sloppiest import win.

One contactWhat is held
  • 1IdentityE-mail, normalised. Unique within the project.
  • 2NameStored whole. first_name is derived at render time, never stored.
  • 3AttributesAnything you like, as JSON. Yours, and interpolatable as attrs.*.
  • 4Tags and listsTags are first class and live in one store; lists are explicit membership.
  • 5ConsentOne row per channel, with a lawful basis and its evidence.
  • 6EventsAn append-only log of everything this person did. The table the product hangs off.
  • ×NeverNo password, no hash, no session, no API key. A column that authenticates somebody would be an incident, not a feature.

Scoped to tenant and project, both

The consent ledger

Absence is not permission.

One row per contact per channel — e-mail, SMS, letter, push — each carrying a state, a lawful basis, a source, the evidence, the timestamps and the jurisdiction. A contact with no basis row for a channel cannot be sent anything on that channel. Not "is flagged". Cannot.

consent

Somebody actively asked. A form submission with the evidence stored, a double opt-in, a checkbox they ticked themselves. This is the strongest basis and it is the only one a form can produce.

legitimate_interest

The best a CSV import can land as. A spreadsheet cannot remember somebody agreeing to something, so it is never allowed to claim they did — and re-uploading the same file does not upgrade it.

contract

You are writing to them because of something you are both party to. The service reminder, the renewal notice, the thing they would be annoyed not to receive.

Before a campaign sends, you see the basis mix of its audience: how many of these people asked, how many you are reaching under legitimate interest, and how many are there because of a contract. It is one number that has stopped more bad sends than any warning banner ever written.

Why this is stricter than it needs to be for you: it is not really about you. Bulk sending reputation is a shared resource, and one careless list poisons the well for everyone sending from the same infrastructure. Per-tenant complaint and bounce rates are watched, and an account whose rates breach the threshold is throttled here — before a provider does it to everybody at once.

Behind every row in the list,
somebody said yes.

Four ways in

Every route into your list is you,
acting on your own data.

There is no service-token ingest lane and no sibling product quietly writing into your contacts. If you want somebody on a list, one of these four put them there.

CSV import

Mapped column by column, normalised on write, MX-checked so dead and typo'd domains are caught before they hard-bounce, and landed with a basis you set explicitly. The import gets an id, and every contact it created points back at it.

A ClickSomo form

Rendered from the same document as everything else, with validation, a honeypot and a rate limit. A submission is the one route that can legitimately produce consent, because it is the only one where the person is present.

A page you built in dash-b

The dashboard designer publishes pages; a form on one of them posts here with your own credentials. Same validation, same consent handling, no second integration to maintain.

The public API

POST /v1/contacts with your own token, upsert semantics, idempotency key honoured. Your app already knows who signed up; this is the shortest path from that fact to a funnel.

Segments are a predicate, compiled.

A segment is a stored predicate tree over contact fields, attributes, tags, list membership and behaviour, compiled to SQL and cached with a count. The operators are the ordinary ones — equals, not equals, in, not in, starts with, ends with, contains, is set, is not set — combined with and, or and not to whatever depth you need.

Preview it before you use it. The endpoint returns real contacts, so "who is actually in this" is a question you answer by looking rather than by sending and finding out.

Segments then do double duty: as an audience for a campaign, as the condition on a funnel branch, and as a trigger in their own right — segment_enter starts a run the moment somebody becomes eligible.

Quiet regularsSegment preview · 412
  • 1Tagtags contains "regular"
  • 2Attributeattrs.total_orders >= 3 — your field, your name for it.
  • 3BehaviourNo order.paid event in the last 90 days.
  • 4ConsentA usable e-mail basis. Contacts without one are not in the count, because they could not be sent to anyway.
  • iPreview412 contacts, listed. Counted again at send, because a segment is a question and not a snapshot.

Compiled to SQLCount cached, answer live

Suppression, and when it is checked.

Suppressions are project-scoped and global: hard bounces, complaints, unsubscribes, manual entries and role addresses. They are checked at send time, and then again inside the sending service itself.

That second word does the work. A funnel with a three-day wait in it has a three-day window in which somebody can unsubscribe, and a system that checked suppression when the message was queued will mail them anyway. Checking at send is the difference between honouring an unsubscribe and recording one.

Erasure works the same way: a deletion request removes the contact and their message history here, and reaches every sibling service that held a copy through the same internal lane the rest of the family uses. There is one place to ask and one thing that happens.

Tell us what you send today,
and to how many.

We will show you what it looks like here — usually with the funnel drawn, because that is faster than a paragraph about it.

Open ClickSomo Talk to a person

Illustrations by Pixeltrue (Minimalistic pack).