Production-grade Go client for the Railsr Embedded Finance API.
| Feature | Detail |
|---|---|
| Full API coverage | All Railsr v1/v2 endpoints — 10 resource groups, 86 endpoints |
| OAuth 2.0 token management | In-memory cache with automatic pre-expiry refresh, mutex-safe |
| Full-jitter exponential backoff | Configurable max retries and base backoff |
| Circuit breaker | Three-state (closed/open/half-open), thread-safe |
| Client-side rate limiter | Token-bucket, mutex-safe |
| Idempotency keys | Auto-generated (128-bit random hex) on all POST/PUT/PATCH |
| Telemetry hooks | Pluggable Hook func called after every request |
| HMAC-SHA256 webhook verification | Constant-time comparison |
| Functional options | Clean, extensible configuration |
| Race-safe | All state uses sync primitives; tested with -race |
| Zero non-stdlib dependencies | Only golang.org/x/time and github.com/google/uuid |
| Polling helpers | WaitForActive, WaitForStatus, WaitForTerminal with context support |
go get github.com/iamkanishka/railsr-goRequires Go 1.25.
package main
import (
"context"
"log"
"os"
railsgo "github.com/iamkanishka/railsr-go"
)
func main() {
r, err := railsgo.New(
os.Getenv("RAILSR_CLIENT_ID"),
os.Getenv("RAILSR_CLIENT_SECRET"),
railsgo.WithEnvironment(railsgo.EnvironmentLive),
)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Create an enduser
eu, err := r.Endusers.Create(ctx, &railsgo.CreateEnduserParams{
Person: &railsgo.Person{
Name: &railsgo.PersonName{FamilyName: "Smith", GivenName: "Alice"},
Email: "alice@example.com",
DateOfBirth: "1990-01-15",
Nationality: "GB",
CountryOfResidence: []string{"GB"},
Address: &railsgo.Address{
Number: "14",
Street: "High Street",
City: "London",
PostalCode: "EC1A 1BB",
ISOCountry: "GB",
},
},
})
if err != nil {
log.Fatal(err)
}
// Trigger KYC
_, err = r.Endusers.CreateKYCCheck(ctx, eu.EnduserID, nil)
if err != nil {
log.Fatal(err)
}
// Wait for KYC to pass (webhook-driven in production)
eu, err = r.Endusers.WaitForStatus(ctx, eu.EnduserID, railsgo.WaitForStatusOpts{
TargetStatuses: []string{"active"},
})
if err != nil {
log.Fatal(err)
}
// Create a GBP ledger
ledger, err := r.Ledgers.Create(ctx, &railsgo.CreateLedgerParams{
HolderID: eu.EnduserID,
LedgerType: "standard-gbp",
AssetClass: "currency",
AssetType: "gbp",
})
if err != nil {
log.Fatal(err)
}
log.Printf("Ledger created: %s sort=%s account=%s",
ledger.LedgerID, ledger.UKSortCode, ledger.UKAccountNumber)
}r, err := railsgo.New(clientID, clientSecret,
railsgo.WithEnvironment(railsgo.EnvironmentLive), // :play | :play_live | :live
railsgo.WithTimeout(30*time.Second),
railsgo.WithMaxRetries(3),
railsgo.WithBaseBackoff(200*time.Millisecond),
railsgo.WithRateLimitRPS(50),
)All options have sensible defaults. Never hard-code credentials — use environment variables or a secrets manager.
// Create a beneficiary
ben, err := r.Beneficiaries.Create(ctx, &railsgo.CreateBeneficiaryParams{
Name: "Bob Jones",
UKAccountNumber: "87654321",
UKSortCode: "204514",
Currency: "GBP",
EnduserID: eu.EnduserID,
})
// Run Confirmation of Payee (CoP)
ben, err = r.Beneficiaries.Verify(ctx, ben.BeneficiaryID, &railsgo.VerifyBeneficiaryParams{
PaymentType: "faster-payment",
})
// ben.COPResult: "matched" | "close_match" | "no_match"
// Send £10.00
tx, err := r.Transactions.SendMoney(ctx, &railsgo.SendMoneyParams{
LedgerID: ledger.LedgerID,
BeneficiaryID: ben.BeneficiaryID,
Amount: 1000, // pence
Currency: "GBP",
PaymentType: "faster-payment",
Reason: "Invoice #42",
})
// Poll for terminal status
tx, err = r.Transactions.WaitForTerminal(ctx, tx.TransactionID, railsgo.WaitForTerminalOpts{})// Issue a virtual card
card, err := r.Cards.Create(ctx, &railsgo.CreateCardParams{
LedgerID: ledger.LedgerID,
CardType: "virtual",
CardProgrammeID: "cp_xxx",
})
// Freeze / unfreeze
r.Cards.Freeze(ctx, card.CardID)
r.Cards.Unfreeze(ctx, card.CardID)
// Daily spend limit £100
r.Cards.CreateRule(ctx, card.CardID, &railsgo.CreateCardRuleParams{
RuleType: "amount_limit",
LimitAmount: 10_000,
LimitCurrency: "GBP",
LimitInterval: "daily",
})
// Block gambling MCCs
r.Cards.CreateRule(ctx, card.CardID, &railsgo.CreateCardRuleParams{
RuleType: "mcc_block",
MCCList: []string{"7995", "7801"},
})// Create mandate
mandate, err := r.Mandates.Create(ctx, &railsgo.CreateMandateParams{
EnduserID: eu.EnduserID,
LedgerID: ledger.LedgerID,
AccountNumber: "12345678",
SortCode: "040004",
AccountHolderName: "Alice Smith",
})
// Wait for BACS activation
mandate, err = r.Mandates.WaitForActive(ctx, mandate.MandateID, railsgo.WaitForMandateActiveOpts{})
// Collect £50
payment, err := r.Payments.Create(ctx, &railsgo.CreatePaymentParams{
MandateID: mandate.MandateID,
Amount: 5_000,
Reason: "Wallet top-up",
})// Set rules
r.Firewall.SetRules(ctx, &railsgo.SetRulesParams{
Rules: []railsgo.FirewallRule{
{
Name: "Quarantine large international",
Rule: `(and (> (transaction.amount) 500000) (not (= (beneficiary.country) "GB")))`,
Action: "quarantine",
Priority: 10,
},
},
})
// Review quarantined transactions
quarantined, _ := r.Transactions.ListQuarantined(ctx)
for _, tx := range quarantined {
if approve(tx) {
r.Transactions.Approve(ctx, tx.TransactionID)
} else {
r.Transactions.Reject(ctx, tx.TransactionID, "Policy violation")
}
}// Configure endpoint
r.Webhooks.Configure(ctx, &railsgo.ConfigureParams{
URL: "https://myapp.com/webhooks/railsr",
Secret: os.Getenv("RAILSR_WEBHOOK_SECRET"),
})
// Verify incoming payloads (in your HTTP handler)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-Railsr-Signature")
secret := os.Getenv("RAILSR_WEBHOOK_SECRET")
if err := railsgo.VerifySignature(body, sig, secret); err != nil {
http.Error(w, "invalid signature", 401)
return
}
// process event...
}import (
"log/slog"
railsgo "github.com/iamkanishka/railsr-go"
"github.com/iamkanishka/railsr-go/telemetry"
)
// Log every request at DEBUG level
hook := telemetry.SlogHook(slog.LevelDebug)
r, err := railsgo.NewWithTelemetry(clientID, clientSecret, hook,
railsgo.WithEnvironment(railsgo.EnvironmentLive),
)
// Or compose multiple hooks
hook := telemetry.Multi(
telemetry.SlogHook(slog.LevelDebug),
myPrometheusHook,
myTracingHook,
)tx, err := r.Transactions.SendMoney(ctx, params)
if err != nil {
var apiErr *railsgo.APIError
if errors.As(err, &apiErr) {
switch apiErr.Type {
case "not_found":
// ledger or beneficiary missing
case "rate_limited":
// back off and retry
case "circuit_open":
// Railsr API temporarily unavailable
case "unauthorized":
// credentials invalid
}
log.Printf("Railsr error: %s [%s] request_id=%s",
apiErr.Message, apiErr.Code, apiErr.RequestID)
}
}
// Or use sentinel errors
if errors.Is(err, railsgo.ErrNotFound) { ... }
if errors.Is(err, railsgo.ErrRateLimited) { ... }| Resource | Endpoints |
|---|---|
| Endusers (v2) | Create, Get, List, Update, Patch, KYC ×3, FirewallRecalc, WaitForStatus |
| Ledgers | Create, Get, List, Update, FindByUKAccount, FindByIBAN, ListEntries, CreditVirtual, DebitVirtual, DevCredit, WaitForActive |
| Transactions | SendMoney, InterLedger, FX, Get, List, ListQuarantined, ResolveQuarantine, Approve, Reject, Retry, WaitForTerminal |
| Beneficiaries | Create, Get, List, Update, Verify (CoP), RecalculateFirewall |
| Cards | Create, Get, List, Activate, Freeze, Unfreeze, Cancel, Suspend, UpdateStatus, Replace, GetPAN, ResetPINAttempts, ListTransactions, CreateRule, ListRules, GetRule, DeleteRule, ListProgrammes, GetProgramme, CreatePaymentToken (Labs), ListPaymentTokens (Labs) |
| Mandates | Create, Get, List, Cancel, WaitForActive |
| Payments | Create, Get, List |
| Firewall | SetRules, GetRules, CreateDataset, ListDatasets, UpdateDataset, DeleteDataset, GetFunctions |
| Webhooks | Configure, GetConfig, ListHistory, Retry, VerifySignature, EventTypes (34 events) |
| Customer | Get, Update, ListProducts, ListLedgers |
# Run all tests with race detector
go test -race ./...
# Run tests with coverage
go test -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Lint
golangci-lint run
# Vet
go vet ./...MIT — see LICENSE.