Skip to main content
POST
Error

Authorizations

Authorization
string
header
required

An organization API key, uk_sk_live_… or uk_sk_test_…. The environment is resolved from the stored key row on every request, never from a request parameter.

Every route behind this credential is rate limited per key — 1000 requests a minute — with a second ceiling of 3000 a minute across all the keys of one environment, so splitting your traffic across keys isolates it and minting more keys does not buy more of it. A test key can never spend a live key's allowance. Every response carries the current state in headers; see the rate-limits guide.

Headers

Idempotency-Key
string

A key you choose — a UUID, or your own identifier for the operation — that makes this write safe to retry. Send the same key again and you get the same response back: the same status, the same body, and Idempotent-Replay: true. The handler does not run a second time.

The key is scoped to your API key and the route, so keys never collide between environments or tenants. It is remembered for 24 hours. Reusing it for a different request is refused (422), and a second request arriving while the first is still running is refused too (409) — retry that one in a moment.

A request that failed with a 5xx or was rate limited leaves the key spendable: retry it with the same key.

Maximum string length: 255

Body

application/json
template
string
required

Required. The key of the template to send.

The words live in your Userkit templates — one row per channel under the same key — and this call names which message and supplies its values. That is the point of the arrangement: the sentence your customer reads sits where the people responsible for it can read, translate and fix it, instead of inside a deploy.

The template must be transactional. This endpoint sends receipts, and a category somebody can switch off belongs in a campaign — a product_news template here would be marketing that ignores an unsubscribe.

Example:

"fatura_vencendo"

contact_id
string<uuid>

Whose notification this is. Exactly one of contact_id or external_id.

external_id
string

The same person, named by your own primary key.

customer_id
string<uuid>

Which of your teams this message is about, named with our id. Optional, and at most one of customer_id / customer_external_id.

Absent means the message is the person's own, and their feed shows it inside every team of theirs — the right answer for "sua senha foi alterada" and for a product with no teams at all. Naming one scopes it: a person in two teams reads "a fatura da Acme falhou" only while acting inside Acme.

Membership is not checked — your key is the authority on which of your teams a message belongs to, exactly as it already is on who the recipient is. The environment is checked, because that is never a caller's to assert.

customer_external_id
string

The same team, named by your own primary key — the id you mirror it with. Both halves in one request is a 400 ambiguous_customer, and a team that does not resolve in the key's environment is a 400 unknown_customer: a caller who said which team meant it, and storing the message without one would file it under "the person's own", where every team of theirs would show it forever.

data
object

The values for the template's declared variables.

Every variable the template declares has to be here. A missing one is a 400 missing_template_data naming it — never a body with a hole where the invoice number was. Extra keys are ignored, so one payload can serve templates that read different subsets of it.

A value is a string, or — for a declared list — an array of rows whose values are strings. Strings and deliberately so: 1.4e+07 in an invoice is what happens when a platform formats somebody else's number, and only you know how your currency and your dates are supposed to read.

A row missing a declared column is a 400 naming it once (membros[0].pontos), not once per row.

Example:
url
string

Optional. Overrides the template's own destination for this one message — which is what an invoice link that differs per recipient needs.

Two shapes are accepted: a path rooted at / (/invoices/inv_123), which is the common case and cannot name a host, and an absolute http/https URL (https://app.example.com/invoices/inv_123), for a product that spans more than one domain. Anything else is a 400, including the protocol-relative //host/path — it reads as a path and is an absolute URL to another host.

Omit it for a notification that is an announcement rather than an errand; most are.

Maximum string length: 2048
channels
enum<string>[]

Where this message should go. Omitted is ["in_app"], which is what this endpoint did before the field existed — so a backend written against the old shape does not start sending mail.

The response carries a verdict per channel rather than one status code for all of them: a contact with no phone number is not a malformed request, and it must not cost them the in-app notification they could have had. Each channel answers created (the feed row, written before the response), queued (handed to the queue — the delivery log is where "did it arrive" is answered) or skipped with the reason to repair. Nothing leaving by any door is a 422.

email needs a contact with an email address. whatsapp needs three things: a server connected on this environment, a phone on the contact, and something to say.

Available options:
in_app,
email,
whatsapp
email
object

What only an inbox needs.

whatsapp
object

What only WhatsApp needs.

kind
enum<string>
default:update

Optional. action when the person has something to do — the card was declined, the document needs a signature — and update when you are telling them what already happened.

It defaults to update, and the asymmetry is deliberate: a notification wrongly filed as an update sits in a list, while one wrongly filed as an action puts a demand on somebody who has nothing to do. Say action when you know.

Available options:
action,
update

Response

At least one channel took the message. channels carries the verdict per channel; notification is present only when in_app was among them, since it is the one channel that writes a row.

channels
object

One entry per channel you asked for, keyed by its name. A channel that did not deliver is reported here rather than raised: a contact with no phone number must not cost them the in-app notification they could have had.

Example:
notification
object

One in-app message addressed to one person. Unlike an email's delivery record this row is the delivery — the body exists nowhere else, which is why it is here and why it dies with the contact.