Skip to content

fintech-sdk/railsr-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

railsr-go

CI Go Reference Go Report Card License: MIT

Production-grade Go client for the Railsr Embedded Finance API.


Features

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

Installation

go get github.com/iamkanishka/railsr-go

Requires Go 1.25.


Quick Start

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)
}

Configuration

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.


Sending Money

// 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{})

Cards

// 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"},
})

Direct Debit

// 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",
})

Compliance Firewall

// 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")
    }
}

Webhooks

// 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...
}

Telemetry

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,
)

Error Handling

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) { ... }

API Coverage

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

Development

# 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 ./...

License

MIT — see LICENSE.

Packages

 
 
 

Contributors

Languages