Triple

Hex.pmDocsLicense

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"}
]
end

Quick 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))
end

Triple.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.