Packages
tipalti_client
1.0.0
A complete, production-grade Elixir client for the Tipalti API surface: the modern OAuth2 REST API, the legacy HMAC-signed SOAP Payee/Payer API, and the Procurement REST API, with pagination, retries, telemetry, and IPN webhook support.
Current section
Files
Jump to
Current section
Files
tipalti_client
README.md
README.md
# Tipalti
A production-grade Elixir client for the full Tipalti API surface: the
modern OAuth2 REST API, the legacy HMAC-signed SOAP Payee/Payer API, and
the Procurement REST API.
Built deliberately with a **minimal dependency footprint** — the HTTP
transport runs on OTP's bundled `:httpc`/`:ssl`/`:xmerl` instead of pulling
in a full HTTP client stack, so this package brings in only two small,
dependency-free libraries (`jason`, `telemetry`).
## Why three APIs?
Tipalti's product grew over time, and it shows in the API surface:
| API | Style | Auth | Covered by |
|---|---|---|---|
| Modern REST | JSON over HTTPS | OAuth 2.0 client credentials | `Tipalti.Payees`, `Tipalti.Invoices`, `Tipalti.Payments` |
| Legacy SOAP | XML over HTTPS | HMAC-SHA256 signed requests | `Tipalti.SOAP.Payee`, `Tipalti.SOAP.Payer` |
| Procurement REST | JSON over HTTPS | Static `x-api-key` | `Tipalti.Procurement.PurchaseOrders`, `Tipalti.Procurement.Employees` |
This package speaks all three through one consistent interface, sharing a
single HTTP transport, retry policy, and telemetry layer underneath.
## Installation
```elixir
def deps do
[
{:tipalti, "~> 1.0.0"}
]
end
```
## Quick start
```elixir
config = Tipalti.Config.new(
mode: :sandbox,
rest: [client_id: "...", client_secret: "..."],
soap: [payer_name: "...", api_key: "..."],
procurement: [api_key: "..."]
)
# Modern REST API
{:ok, page} = Tipalti.Payees.list(config)
for payee <- Tipalti.Payees.stream(config) do
IO.inspect(payee)
end
# Legacy SOAP API
{:ok, result} =
Tipalti.SOAP.Payer.process_payments(config, [
%{idap: "vendor-123", amount: 100.00, currency: "USD", refCode: "pay-1"}
])
# Procurement REST API
{:ok, pos} = Tipalti.Procurement.PurchaseOrders.list(config)
```
Only populate the `Tipalti.Config` sections you actually use — a config
built with just `soap:` opts is fine as long as you only call
`Tipalti.SOAP.*` functions with it.
## Error handling
Every function has a non-bang variant returning `{:ok, result}` /
`{:error, exception}`, and a `!` variant that raises instead:
```elixir
case Tipalti.Payees.get(config, "p_123") do
{:ok, payee} -> payee
{:error, %Tipalti.AuthenticationError{}} -> :bad_credentials
{:error, %Tipalti.RateLimitError{retry_after: ms}} -> :retry_later
{:error, %Tipalti.ValidationError{errors: errors}} -> :bad_input
{:error, %Tipalti.SOAPFaultError{fault_string: msg}} -> :soap_fault
{:error, error} -> {:other_error, error}
end
payee = Tipalti.Payees.get!(config, "p_123")
```
See `Tipalti.Error` for the full exception hierarchy.
## Pagination
Every REST list endpoint has a `stream/2` variant that lazily walks every
page:
```elixir
Tipalti.Invoices.stream(config, status: "pending")
|> Stream.filter(&(&1.amount > 1000))
|> Enum.to_list()
```
## SOAP signing
The legacy API's HMAC-SHA256 request signing (`Tipalti.SOAP.Signature`) is
handled automatically — every `Tipalti.SOAP.Payee`/`Tipalti.SOAP.Payer`
function knows its operation's EAT (Encryption Additional Terms) parameter
and folds it into the signature for you.
## Procurement employee import
The three-step CSV upload flow (get a signed URL, upload the CSV, trigger
the import) is wrapped in one call:
```elixir
csv = File.read!("employees.csv")
{:ok, _result} = Tipalti.Procurement.Employees.import_employees(config, csv)
```
## IPN webhooks
```elixir
def handle_tipalti_webhook(conn) do
{:ok, body, conn} = Plug.Conn.read_body(conn)
case Tipalti.Webhook.parse(body) do
{:ok, event} ->
handle_event(Tipalti.Webhook.event_type(event), event)
Plug.Conn.send_resp(conn, 200, "")
{:error, _reason} ->
Plug.Conn.send_resp(conn, 400, "")
end
end
```
## Telemetry
Every request emits a `:telemetry.span/3` under `[:tipalti, :request]` by
default (configurable via `Tipalti.Config` `:telemetry_prefix`). See
`Tipalti.Telemetry` for the full event/metadata reference.
## Rate limiting
The Procurement API documents explicit rate limits. `Tipalti.HTTP` already
retries a `429` with backoff, but for high-throughput integrations
`Tipalti.RateLimiter` (an optional token-bucket limiter) lets you avoid
tripping the limit client-side in the first place.
## Design notes
- **HTTP transport**: `Tipalti.HTTP` is built on OTP's `:httpc`, with
exponential backoff + full jitter on retries, TLS verification via
`:public_key.cacerts_get/0` (OTP 25+), and connection pooling disabled
(each request opens its own connection — simpler and more predictable
across the range of environments this library runs in, e.g. serverless).
- **OAuth2 tokens**: `Tipalti.Auth.TokenServer` is a GenServer, one per
distinct `client_id`, started on demand and supervised under
Tipalti.Application. Tokens are cached and refreshed 60 seconds before
expiry.
- **REST endpoint shapes** (`Tipalti.Payees`/`Invoices`/`Payments`) follow
Tipalti's documented conventions for the modern REST API — see the
moduledoc caveat in `Tipalti.Payees` if your instance's exact response
envelope differs; the request/auth/pagination/error-handling machinery
underneath is meant to be reused as-is.
## Quality
```
mix format --check-formatted # clean
mix compile --warnings-as-errors # clean
mix test # 43 tests, 0 failures
mix credo --strict # 0 issues (312 functions)
mix dialyzer # 0 errors
```
`credo` and `dialyxir` are dev/test-only dependencies, pinned to git tags
alongside their own small transitive deps (`bunt`, `file_system`, `erlex`)
for environments where the hex.pm registry isn't reachable — see `mix.exs`.
The Dialyzer PLT takes a few minutes to build the first time
(`mix dialyzer --plt`); subsequent runs are fast.
## Testing this package
The test suite uses a small `:gen_tcp`-based mock HTTP server
(`test/support/mock_server.ex`) instead of Bypass/Plug, keeping the
dependency list minimal even for development/test.
```
mix test
```
## License
MIT