Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

50 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tipalti-go

A production-grade Go 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.

Zero external dependencies. go.mod declares none — the entire module is built on the standard library (net/http, encoding/json, encoding/xml, crypto/hmac, context, generics). Nothing to vendor, nothing to audit upstream.

Architecture: Domain-Driven Design

tipalti-go/
  tipalti.go                        # the ONLY .go file at the module root:
                                     # public Client facade + re-exports
  internal/
    domain/                         # entities, value objects, Port interfaces
      shared/                       #   — no HTTP/XML/JSON knowledge at all
      payee/  invoice/  payment/
      soap/   purchaseorder/  employee/
    application/                    # use-case services orchestrating a
      payeeapp/  invoiceapp/        #   domain Port (e.g. adding pagination
      paymentapp/                   #   streaming) — still transport-agnostic
      soappayeeapp/  soappayerapp/
      procurementapp/
    infrastructure/                 # adapters implementing each domain Port
      httptransport/                #   against Tipalti's actual wire format
      oauth2/
      restadapter/
      soapadapter/
      procurementadapter/
    config/  apperrors/  pagination/
    webhook/  ratelimit/  telemetry/

The dependency direction is strictly one-way and acyclic: domain ← application ← infrastructure ← root. Domain packages import nothing from this module except other domain packages; verified by grep in CI (see below) as well as go vet.

Since Go's internal/ convention makes those packages unimportable outside this module, tipalti.go re-exports every public type via type aliases (type Resource = shared.Resource, etc.) so external callers get a clean, flat API without ever touching the internal layout.

Installation

go get github.com/iamkanishka/tipalti-go

Quick start

client, err := tipalti.New(
	tipalti.WithMode(tipalti.ModeSandbox),
	tipalti.WithREST("client-id", "client-secret"),
	tipalti.WithSOAP("payer-name", "api-key"),
	tipalti.WithProcurement("procurement-api-key"),
)
if err != nil {
	log.Fatal(err)
}

// Modern REST API
page, err := client.Payees.List(ctx, tipalti.PayeeListParams{})

stream := client.PayeeStream(ctx, tipalti.PayeeListParams{})
for payee := range stream.Items() {
	fmt.Println(payee.ID(), payee.String("name"))
}
if err := stream.Err(); err != nil {
	log.Fatal(err)
}

// Legacy SOAP API
result, err := client.SOAP.Payer.ProcessPayments(ctx, []map[string]any{
	{"idap": "vendor-123", "amount": 100.00, "currency": "USD", "refCode": "pay-1"},
}, tipalti.ProcessPaymentsOptions{PaymentGroupTitle: "Weekly payout"})

// Procurement REST API
pos, err := client.Procurement.PurchaseOrders.List(ctx, tipalti.PurchaseOrderListParams{})

Only populate the credential options for the API families you actually use — a Client built with just WithSOAP is fine as long as you only call client.SOAP.* methods.

Error handling

Every error is one of six typed errors; use errors.As:

_, err := client.Payees.Get(ctx, "p_123")

var authErr *tipalti.AuthenticationError
var rlErr *tipalti.RateLimitError
var valErr *tipalti.ValidationError
var faultErr *tipalti.SOAPFaultError

switch {
case errors.As(err, &authErr):
	// bad/expired credentials
case errors.As(err, &rlErr):
	// rate limited; rlErr.RetryAfter has the hint, if any
case errors.As(err, &valErr):
	// malformed request; valErr.Errors has field-level details
case errors.As(err, &faultErr):
	// SOAP <soap:Fault>; faultErr.FaultCode / FaultString
}

Pagination

Every REST list use case has a matching client.<X>Stream method returning a channel-based Stream[Resource] (channel-based rather than iter.Seq since this module targets Go 1.22):

stream := client.InvoiceStream(ctx, tipalti.InvoiceListParams{Status: "pending"})
items, err := tipalti.Collect(stream) // or range over stream.Items()

SOAP signing

HMAC-SHA256 request signing is handled automatically — every client.SOAP.Payee/client.SOAP.Payer method knows its operation's EAT (Encryption Additional Terms) parameter and folds it into the signed request for you. All 45 legacy operations (21 Payee + 24 Payer) are covered.

Procurement employee import

csv, _ := os.ReadFile("employees.csv")
_, err := client.Procurement.Employees.ImportEmployees(ctx, csv)

IPN webhooks

func handleTipaltiWebhook(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	event, err := tipalti.ParseWebhook(string(body))
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	handleEvent(event.EventType(), event)
}

Telemetry

client, _ := tipalti.New(
	tipalti.WithREST(id, secret),
	tipalti.WithTelemetryHook(tipalti.TelemetryFunc(func(e tipalti.TelemetryEvent) {
		log.Printf("%s.%s -> status=%d err=%v in %s", e.API, e.Operation, e.Status, e.Err, e.Duration)
	})),
)

Rate limiting

limiter := tipalti.NewRateLimiter(5, time.Minute) // Procurement API's documented PO-update limit
if err := limiter.Wait(ctx); err != nil {
	return err
}
client.Procurement.PurchaseOrders.Update(ctx, attrs)

Design notes

  • Concurrency: the OAuth2 TokenManager is safe for concurrent use — concurrent callers during a token refresh share the single in-flight request rather than each firing their own. Verified with go test -race.
  • Generics: Stream[T] (pagination) and a couple of small internal helpers use generics; no code generation.
  • REST endpoint shapes (Payees/Invoices/Payments) follow Tipalti's documented conventions for the modern REST API — see the doc comment on internal/infrastructure/restadapter/client.go if your instance's exact response envelope differs; the request/auth/error-handling machinery there is meant to be reused as-is.

Quality

gofmt -l .                    # clean
go vet ./...                  # clean
go build ./...                # clean
go test ./... -race -count=1  # all passing
golangci-lint run ./...       # 0 issues

.golangci.yml enables a production-grade linter set well beyond the tool's 6-linter default — including gosec, errorlint, gocyclo, bodyclose, contextcheck, exhaustive, prealloc, unparam, revive, and more (see the file for the full list). All clean, verified in a fresh checkout.

golangci-lint itself is installed from a GitHub release binary rather than go install, and dependencies are fetched via GOPROXY=direct — see comments in this repo's build notes if proxy.golang.org isn't reachable in your environment either; none of that affects consuming this module, since it has zero dependencies of its own.

License

MIT

About

A production-grade Go 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.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages