An Elixir client for the Triple transaction data enrichment API: turn raw bank/card transaction strings into clean merchant names, logos, categories, locations, contact details, subscription detection, CO₂ estimates, fraud signals, and payment processor identification.
Installation
Add :triple to your mix.exs dependencies:
def deps do
[
{:triple, "~> 1.0.0"}
]
endQuick start
client = Triple.new(api_key: System.fetch_env!("TRIPLE_API_KEY"))
{:ok, enriched} =
Triple.enrich_transaction(client, %{
merchant_name: "AMZN MKTP UK",
transaction_type: :CARD_TRANSACTION,
transaction_id: Triple.Util.generate_transaction_id(),
transaction_amount: 24.99,
transaction_currency: "GBP",
channel_type: :ECOMMERCE
})
enriched.visual_enrichments.merchant_clean_name
#=> "Amazon"client is a plain %Triple.Config{} struct, not a process — pass it
explicitly to every call. That keeps the library safe to use with multiple
Triple accounts/environments (e.g. sandbox alongside production) in the
same application, with no global or process-dictionary state to worry
about.
API keys starting with tr_test_ are sandbox keys, tr_live_ are
production keys — the environment (and therefore which host gets called)
is inferred automatically from whichever you pass in.
Configuration
Every option can be passed to Triple.new/1 directly:
Triple.new(
api_key: "tr_live_xxx",
receive_timeout: 15_000,
max_retries: 5
)...or set application-wide, and used as a fallback whenever it isn't passed explicitly:
# config/runtime.exs
config :triple, api_key: System.fetch_env!("TRIPLE_API_KEY")See Triple.Config for the full list (timeouts, retry strategy, a custom
Req options passthrough, an optional client-side rate limiter, etc).
Enrichment
Two flavours, matching Triple's two enrichment endpoints:
# Structured — when you have discrete fields
Triple.enrich_transaction(client, %{
merchant_name: "AMZN MKTP UK",
transaction_type: :CARD_TRANSACTION,
transaction_id: Triple.Util.generate_transaction_id(),
merchant_country: "GBR",
transaction_amount: 24.99,
transaction_currency: "GBP"
})
# Unstructured — when all you have is a raw description string
Triple.enrich_unstructured_transaction(client, %{
transaction_id: Triple.Util.generate_transaction_id(),
text: "CRD PUR 4321 NETFLIX.COM 866-5797172 CA",
transaction_amount: 15.49,
transaction_currency: "USD"
})Every input is validated locally before any network call is made —
invalid input returns {:error, %Triple.Error{type: :validation}}
immediately, with the same field-level error shape Triple's own API would
return.
The two response shapes differ slightly, matching Triple's own OpenAPI
spec: the structured (v1) response wraps every enrichment feature
(location, subscriptions, CO₂, fraud, contact, payment processor) in an
enabled?-flagged struct, since not every transaction carries every kind
of signal — an online purchase, for instance, never has a
merchant_location. The unstructured (v2) response uses flat, simply
nullable structs instead. See Triple.Types.Enrich.V1.Response and
Triple.Types.Enrich.V2.Response for the exact shapes, including a couple
of small helpers like Triple.Types.Enrich.V1.Response.Subscriptions.recurring?/1.
Brands, feedback, stocks, cryptos, and TLS
# Look up a brand directly (e.g. to refresh a cached logo)
Triple.fetch_brand(client, "497f6eca-6276-4993-bfeb-53cbbbba6f08")
# Tell Triple when enrichment data is wrong or missing
Triple.report_feedback(client, %{
transaction_id: "txn_123",
report: :brand_name,
response_value: "AMZN MKTP UK",
feedback: "Should be Amazon"
})
# Brokerage data
Triple.fetch_stock(client, "LU1778762911", format: :svg_light)
Triple.fetch_crypto(client, "bitcoin")
# Issue an mTLS client certificate (hits Triple's control-plane host)
Triple.issue_tls_certificate(client, %{public_key: pem, lifetime: 365})Every function above also has a ! counterpart (fetch_brand!/2,
report_feedback!/2, ...) that raises Triple.Error instead of returning
{:error, _}.
Error handling
Every call returns {:ok, result} | {:error, %Triple.Error{}}:
case Triple.enrich_transaction(client, attrs) do
{:ok, enriched} ->
enriched
{:error, %Triple.Error{type: :validation, errors: errors}} ->
# local validation failure — `errors` is a `field => [messages]` map
Logger.warning("Bad enrich payload: #{inspect(errors)}")
{:error, %Triple.Error{type: :rate_limited, retry_after: seconds}} ->
# only seen after the client's own retries are exhausted
Logger.warning("Triple rate limit hit, retry after #{seconds}s")
{:error, error} ->
Logger.error(Exception.message(error))
endTriple.Error distinguishes :validation, :unauthenticated,
:forbidden, :not_found, :rate_limited, :server_error,
:unexpected_status, and :network_error — see the module docs for the
full field list.
Retries
408, 429, 500, 502, 503, and 504 responses (and transport
errors) are retried automatically with exponential backoff, honoring
Triple's retry-after header on 429s. Configure or disable this via
Triple.Config:
Triple.new(api_key: key, max_retries: 5)
Triple.new(api_key: key, retry: false)Telemetry
[:triple, :request, :start | :stop | :exception] events are emitted
around every call — see Triple.Telemetry for the full event/metadata
reference, handy for logging, metrics, or tracing.
Optional client-side rate limiting
For bulk workloads (e.g. backfilling historical transactions) where you'd
rather avoid 429s in the first place:
{:ok, _pid} = Triple.RateLimiter.start_link(name: MyApp.TripleLimiter, rate: 50, per: :second)
client = Triple.new(api_key: key, rate_limiter: MyApp.TripleLimiter)See Triple.RateLimiter for details and its limits (single-node only).
Testing code that calls Triple
This library is built on Req, so you can stub
responses in your own tests via Req's :adapter option — no extra test
dependency required:
adapter = fn req ->
{req, %Req.Response{status: 200, body: %{"transaction_id" => "txn_1"}}}
end
client = Triple.new(api_key: "tr_test_xxx", req_options: [adapter: adapter])The adapter function receives the fully-built %Req.Request{} (so you
can assert on req.method, req.url, req.options[:json], etc) and must
return {req, %Req.Response{}} or {req, exception}.
Sandbox vs. production
Triple provides fully isolated sandbox and production environments (API
hosts, dashboards, and databases). Pass a tr_test_* key to hit sandbox,
or tr_live_* for production — Triple.Config infers and warns on any
mismatch if you also pass environment: explicitly.
License
MIT. See LICENSE.
Disclaimer
This is a community-maintained client and is not officially affiliated with or endorsed by Triple Technologies. See jointriple.com for the official product and docs.triple.app for the official API reference.