adinize
Elixir SDK for the adinize server events API. Your server reports conversions to adinize, which attributes them to the ad click and forwards them to Meta, TikTok and Google Ads.
Personal data leaves your server only as SHA-256 hashes.
Install
def deps do
[{:adinize, "~> 0.1"}]
end
Create a secret key on your pixel's page in adinize, and keep it on your server:
# config/runtime.exs
config :adinize,
secret_key: System.fetch_env!("ADINIZE_SECRET_KEY"),
default_country: "EG"
Send an event
Adinize.track("Purchase",
event_id: "order_10482",
user: [email: " Jane@Example.com ", phone: "0100 123 4567"],
data: [value: 129.5, currency: "EGP", order_id: "10482"]
)
#=> {:ok, %Adinize.Result{event_id: "order_10482", status: :accepted, errors: []}}
email,phone,first_name,last_nameandstreet_addressare hashed before sending.data:goes as given, so any key in it, at any depth, named*email*,*phone*,first_name,last_nameorstreet_addressis refused with the key's path.page_urlgoes without its query string, which can hold an email or a token. Passquery_string: trueto keep it.event_iddefaults to a UUIDv4 andevent_timeto now. Send your order number asevent_idso a retry never counts twice: the server answers:duplicatefor anevent_idit already holds.- Phones become E.164 before hashing. A local number takes
default_country's code; one without a+, a00prefix or a default country is left out. Arabic-Indic digits work, and for EG, SA, AE, GB and JO a0written after the country code is dropped. phone:sends two hashes of the same E.164 number:phone_hash(with the+, read by Google Ads and TikTok) andphone_digits_hash(digits only, the form Meta matches). A number with no E.164 form sends neither.- Pre-hashed keys (
email_hash:,phone_hash:,phone_digits_hash:,first_name_hash:,last_name_hash:,street_address_hash:) take 64 lowercase hex characters. The SDK cannot derive one phone hash from the other, so pass bothphone_hash:andphone_digits_hash:. With onlyphone_hash:, Google and TikTok match the phone and Meta does not.phone:beside either pre-hashed phone key isINVALID_OPTION. Adinize.track_many/2sends up to 100 events in one request.
From a Phoenix controller, add the visitor cookie, Meta's cookies, the IP and the User-Agent:
Adinize.track("Purchase", Adinize.Plug.context(conn) ++ [event_id: order.id])
Hashing
The SDK hashes these fields with SHA-256 before the request leaves your server. It trims and lowercases text first, as Meta's customer information rules say.
| Field | Hashed from |
|---|---|
email |
the trimmed, lowercased address |
phone |
the E.164 digits (see the phone rule above); Meta gets them without the + |
first_name, last_name |
the trimmed, lowercased name |
street_address |
the trimmed, lowercased text |
A field that is empty after trimming, or a phone that cannot become E.164,
is left out of the request. IP address, User-Agent and the _fbc and
_fbp cookies go as given: Meta and TikTok match them unhashed.
Adinize.Hash exposes each function if you need a hash elsewhere.
Deduplicate with the browser pixel
When the adinize pixel in the browser and your server both report the same
purchase, give both the same event_id, and use the same event name. The
platforms keep one of the two: Meta and TikTok merge a browser and a server
event that share event_id within 48 hours.
Your server call carries the order number the browser event carries as
its event_id:
Adinize.track("Purchase", event_id: "order_10482", data: [value: 129.5, currency: "EGP"])
Your pixel's page in adinize shows how to send an event_id from the
browser. A retry from your server is safe for the same reason: adinize answers
:duplicate for an event_id its pixel already holds.
Results and errors
Every call returns a value and never raises.
| You get | When |
|---|---|
{:ok, %Adinize.Result{status: :accepted}} |
stored |
{:ok, %Adinize.Result{status: :duplicate}} |
the pixel already held this event_id |
{:ok, %Adinize.Result{status: :rejected, errors: [...]}} |
the server refused this event, with reasons |
{:error, %Adinize.Error{status: 401, code: "INVALID_KEY"}} |
the key is missing, unknown or revoked |
{:error, %Adinize.Error{status: 429, retry_after: 12}} |
wait 12 seconds, then send the same events again |
{:error, %Adinize.Error{code: "INVALID_OPTION"}} |
a malformed option; nothing was sent |
Adinize.Error lists every code. track/2 does not retry unless you pass
retry: true (see below).
Sending in the background
A developer who does not want a web request to wait on adinize adds a batcher to the supervision tree and sends asynchronously:
children = [{Adinize.Batcher, flush_interval: 1_000, max_batch: 100}]
Adinize.track_async("Purchase",
event_id: "order_10482",
data: [value: 129.5, currency: "EGP"]
)
#=> :ok
- Sends every
flush_intervalms (default 1,000) or atmax_batchevents (default 100, capped at 100). Pastmax_queue(default 10,000) it drops the newest event and emits telemetry, rather than growing memory without bound. - On a 429 it waits
Retry-After(falling back to the same backoff as a 5xx when the server gives none); on a 5xx, a timeout or a transport error it backs off with jitter, up to 5 attempts total, the sameevent_ideach time — the server answers a retried event:duplicate, never twice. A 400 or 401 is never retried. - On a supervised stop it flushes what it holds, best effort, within
shutdownms (default 5,000). track/2(sync) takes the same retry policy withretry: true(defaultfalse).- Telemetry:
[:adinize, :request, :start | :stop | :exception]around every send,[:adinize, :event, :rejected]for a rejected result, and[:adinize, :event, :dropped]when an event is dropped (queue full, retries exhausted, a non-retryable error, or shutdown). Metadata never holds the secret key, a raw personal field or the request/response — only ids, names and the server's own field/code/message strings.
The secret key
The SDK never logs the key and never puts it in an error or in its own
telemetry. Finch's telemetry events carry the request headers, key
included: if your app logs Finch telemetry metadata, filter the
authorization header first.
Contract
The API behind this SDK is described by one OpenAPI file:
https://adinize.ai/api/server/v1/openapi.yaml. spec/server-events-v1.yaml
holds a copy that CI compares with the live file. Docs: https://hexdocs.pm/adinize.
License
MIT