Project

airwallex

0.0
The project is in a healthy, maintained state
A comprehensive Ruby gem for integrating with Airwallex's global payment infrastructure, including payment acceptance, payouts, foreign exchange (FX rates, quotes, conversions), and multi-currency balance management. Features automatic authentication, idempotency guarantees, webhook verification, and unified pagination across all resources.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

 Project Readme

Airwallex Ruby Gem

A Ruby client library for the Airwallex API, providing access to payment acceptance and payout capabilities.

Overview

This gem provides a Ruby interface to Airwallex's payment infrastructure, designed for Ruby 3.1+ applications. It includes core functionality for authentication management, idempotency guarantees, webhook verification, and multi-environment support.

Current Features (v0.1.0):

  • Authentication: Bearer token authentication with automatic refresh
  • Payment Acceptance: Payment intent creation, confirmation, and management
  • Payouts: Transfer creation and beneficiary management
  • Idempotency: Automatic request deduplication for safe retries
  • Pagination: Unified interface over cursor-based and offset-based pagination
  • Webhook Security: HMAC-SHA256 signature verification with replay protection
  • Sandbox Support: Full testing environment for development

Note: This is an initial MVP release. Additional resources (FX, cards, refunds, etc.) will be added in future versions.

Installation

Add this line to your application's Gemfile:

gem 'airwallex'

And then execute:

bundle install

Or install it yourself as:

gem install airwallex

Quick Start

Configuration

require 'airwallex'

Airwallex.configure do |config|
  config.api_key = 'your_api_key'
  config.client_id = 'your_client_id'
  config.environment = :sandbox # or :production
end

Creating a Payment Intent

# Create a payment intent
payment_intent = Airwallex::PaymentIntent.create(
  amount: 100.00,
  currency: 'USD',
  merchant_order_id: 'order_123',
  return_url: 'https://yoursite.com/return'
)

# Confirm with card details
payment_intent.confirm(
  payment_method: {
    type: 'card',
    card: {
      number: '4242424242424242',
      expiry_month: '12',
      expiry_year: '2025',
      cvc: '123'
    }
  }
)

Creating a Payout

# Create a beneficiary
beneficiary = Airwallex::Beneficiary.create(
  bank_details: {
    account_number: '123456789',
    account_routing_type1: 'aba',
    account_routing_value1: '026009593',
    bank_country_code: 'US'
  },
  beneficiary_type: 'BUSINESS',
  company_name: 'Acme Corp'
)

# Execute transfer
transfer = Airwallex::Transfer.create(
  beneficiary_id: beneficiary.id,
  source_currency: 'USD',
  transfer_method: 'LOCAL',
  amount: 1000.00,
  reason: 'Payment for services'
)

Processing Refunds

# Create a full refund
refund = Airwallex::Refund.create(
  payment_intent_id: payment_intent.id,
  amount: 100.00,
  reason: 'requested_by_customer'
)

# Create a partial refund
partial_refund = Airwallex::Refund.create(
  payment_intent_id: payment_intent.id,
  amount: 50.00
)

# List all refunds for a payment
refunds = Airwallex::Refund.list(payment_intent_id: payment_intent.id)

Managing Payment Methods

# Create a customer
customer = Airwallex::Customer.create(
  email: 'customer@example.com',
  first_name: 'John',
  last_name: 'Doe'
)

# Save a payment method
payment_method = Airwallex::PaymentMethod.create(
  type: 'card',
  card: {
    number: '4242424242424242',
    expiry_month: '12',
    expiry_year: '2025',
    cvc: '123'
  },
  billing: {
    first_name: 'John',
    email: 'customer@example.com'
  }
)

# Use saved payment method
payment_intent.confirm(payment_method_id: payment_method.id)

# List customer's payment methods
methods = customer.payment_methods

Batch Transfers

# Create a batch of transfers for bulk payouts
batch = Airwallex::BatchTransfer.create(
  request_id: "batch_#{Time.now.to_i}",
  source_currency: 'USD',
  transfers: [
    { beneficiary_id: 'ben_001', amount: 100.00, reason: 'Seller payout' },
    { beneficiary_id: 'ben_002', amount: 250.00, reason: 'Affiliate payment' },
    { beneficiary_id: 'ben_003', amount: 500.00, reason: 'Vendor payment' }
  ]
)

# Check batch status
batch = Airwallex::BatchTransfer.retrieve(batch.id)
puts "Completed: #{batch.success_count}/#{batch.total_count}"

# Check individual transfer statuses
batch.transfers.each do |transfer|
  puts "#{transfer.id}: #{transfer.status}"
end

Managing Disputes

# List all open disputes
disputes = Airwallex::Dispute.list(status: 'OPEN')

# Get specific dispute
dispute = Airwallex::Dispute.retrieve('dis_123')
puts "Dispute amount: #{dispute.amount} #{dispute.currency}"
puts "Reason: #{dispute.reason}"
puts "Evidence due: #{dispute.evidence_due_by}"

# Submit evidence to challenge
dispute.submit_evidence(
  customer_communication: 'Email showing delivery confirmation',
  shipping_tracking_number: '1Z999AA10123456784',
  shipping_documentation: 'Proof of delivery with signature'
)

# Or accept dispute without challenging
dispute.accept

Foreign Exchange & Multi-Currency

# Get real-time exchange rate
rate = Airwallex::Rate.retrieve(
  buy_currency: 'EUR',
  sell_currency: 'USD'
)
puts "Current rate: #{rate.client_rate}"

# Lock in a rate with a quote (valid for 24 hours)
quote = Airwallex::Quote.create(
  buy_currency: 'EUR',
  sell_currency: 'USD',
  sell_amount: 10000.00,
  validity: 'HR_24'
)

puts "Locked rate: #{quote.client_rate}"
puts "Expires in: #{quote.seconds_until_expiration} seconds"
puts "Is expired? #{quote.expired?}"

# Execute conversion using locked quote
conversion = Airwallex::Conversion.create(
  quote_id: quote.id,
  reason: 'Multi-currency settlement'
)

# Or convert at current market rate
conversion = Airwallex::Conversion.create(
  buy_currency: 'EUR',
  sell_currency: 'USD',
  sell_amount: 5000.00,
  reason: 'Currency exchange'
)

# Check account balances
balances = Airwallex::Balance.list
balances.each do |balance|
  next if balance.available_amount <= 0
  puts "#{balance.currency}: #{balance.available_amount} available"
end

# Get specific currency balance
usd_balance = Airwallex::Balance.retrieve('USD')
puts "USD Available: #{usd_balance.available_amount}"
puts "USD Total: #{usd_balance.total_amount}"

Usage

Authentication

The gem uses Bearer token authentication with automatic token refresh:

Airwallex.configure do |config|
  config.api_key = 'your_api_key'
  config.client_id = 'your_client_id'
  config.environment = :sandbox # or :production
end

Tokens are automatically refreshed when they expire, and the gem handles thread-safe token management.

Idempotency

The gem automatically handles idempotency for safe retries:

# Automatic request_id generation
transfer = Airwallex::Transfer.create(
  amount: 500.00,
  beneficiary_id: 'ben_123'
  # request_id automatically generated
)

# Or provide your own for reconciliation
transfer = Airwallex::Transfer.create(
  amount: 500.00,
  beneficiary_id: 'ben_123',
  request_id: 'my_internal_id_789'
)

Pagination

Unified interface across both cursor-based and offset-based endpoints:

# Auto-pagination with enumerable
Airwallex::Transfer.list.auto_paging_each do |transfer|
  puts transfer.id
end

# Manual pagination
transfers = Airwallex::Transfer.list(page_size: 50)
while transfers.has_more?
  transfers.each { |t| process(t) }
  transfers = transfers.next_page
end

Webhook Handling

# In your webhook controller
payload = request.body.read
signature = request.headers['x-signature']
timestamp = request.headers['x-timestamp']

begin
  event = Airwallex::Webhook.construct_event(
    payload,
    signature,
    timestamp,
    tolerance: 300 # 5 minutes
  )
  
  case event.type
  when 'payment_intent.succeeded'
    handle_successful_payment(event.data)
  when 'payout.transfer.failed'
    handle_failed_payout(event.data)
  end
rescue Airwallex::SignatureVerificationError => e
  # Invalid signature
  head :bad_request
end

Error Handling

begin
  transfer = Airwallex::Transfer.create(params)
rescue Airwallex::InsufficientFundsError => e
  # Handle insufficient balance
  notify_user("Insufficient funds: #{e.message}")
rescue Airwallex::RateLimitError => e
  # Rate limit hit - automatic retry with backoff
  retry_with_backoff
rescue Airwallex::AuthenticationError => e
  # Invalid credentials
  log_error("Auth failed: #{e.message}")
rescue Airwallex::APIError => e
  # General API error
  log_error("API error: #{e.code} - #{e.message}")
end

Architecture

Design Principles

  • Correctness First: Automatic idempotency and type safety prevent duplicate transactions
  • Fail-Safe Defaults: Sandbox environment default, automatic token refresh
  • Developer Experience: Auto-pagination, dynamic schema validation, structured errors
  • Security: HMAC webhook verification, constant-time signature comparison, SCA support
  • Resilience: Exponential backoff, jittered retries, concurrent request limits

Core Components

lib/airwallex/
├── api_operations/        # CRUD operation mixins (Create, Retrieve, List, Update, Delete)
├── resources/             # Implemented resources
│   ├── payment_intent.rb  # Payment acceptance
│   ├── transfer.rb        # Payouts
│   └── beneficiary.rb     # Payout beneficiaries
├── api_resource.rb        # Base resource class with dynamic attributes
├── list_object.rb         # Pagination wrapper
├── errors.rb              # Exception hierarchy
├── client.rb              # HTTP client with authentication
├── configuration.rb       # Environment and credentials
├── webhook.rb             # Signature verification
├── util.rb                # Helper methods
└── middleware/            # Faraday middleware
    └── idempotency.rb     # Automatic request_id injection

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rspec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

Running Tests

bundle exec rspec

Code Style

bundle exec rubocop

Local Development

# In bin/console or irb
require 'airwallex'

Airwallex.configure do |config|
  config.environment = :sandbox
  config.api_key = ENV['AIRWALLEX_API_KEY']
  config.client_id = ENV['AIRWALLEX_CLIENT_ID']
end

API Coverage

Currently Implemented Resources

  • Payment Acceptance:
    • PaymentIntent (create, retrieve, list, update, confirm, cancel, capture)
    • Refund (create, retrieve, list)
    • PaymentMethod (create, retrieve, list, update, delete, detach)
    • Customer (create, retrieve, list, update, delete)
    • Dispute (retrieve, list, accept, submit_evidence)
  • Payouts:
    • Transfer (create, retrieve, list, cancel)
    • Beneficiary (create, retrieve, list, delete)
    • BatchTransfer (create, retrieve, list)
  • Foreign Exchange & Multi-Currency:
    • Rate (retrieve, list) - Real-time exchange rate queries
    • Quote (create, retrieve) - Lock exchange rates with expiration tracking
    • Conversion (create, retrieve, list) - Execute currency conversions
    • Balance (list, retrieve) - Query account balances across currencies
  • Webhooks: Event handling, HMAC-SHA256 signature verification

Coming in Future Versions

  • Global accounts
  • Card issuing
  • Subscriptions and billing
  • Virtual account numbers

Environment Support

Sandbox

Testing environment for development:

Airwallex.configure do |config|
  config.environment = :sandbox
  config.api_key = ENV['AIRWALLEX_SANDBOX_API_KEY']
  config.client_id = ENV['AIRWALLEX_SANDBOX_CLIENT_ID']
end

Production

Live environment for real financial transactions:

Airwallex.configure do |config|
  config.environment = :production
  config.api_key = ENV['AIRWALLEX_API_KEY']
  config.client_id = ENV['AIRWALLEX_CLIENT_ID']
end

Rate Limits

The gem respects Airwallex API rate limits. If you encounter Airwallex::RateLimitError, implement retry logic with exponential backoff:

begin
  transfer = Airwallex::Transfer.create(params)
rescue Airwallex::RateLimitError => e
  sleep(2 ** retry_count)
  retry_count += 1
  retry if retry_count < 3
end

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/Sentia/airwallex.

Development Setup

  1. Fork and clone the repository
  2. Run bin/setup to install dependencies
  3. Create a .env file with sandbox credentials
  4. Run tests: bundle exec rspec
  5. Check style: bundle exec rubocop

Guidelines

  • Write tests for new features
  • Follow existing code style (enforced by Rubocop)
  • Update documentation for API changes
  • Ensure all tests pass before submitting PR

Versioning

This gem follows Semantic Versioning. The Airwallex API uses date-based versioning, which is handled internally by the gem.

Security

If you discover a security vulnerability, please email security@sentia.com instead of using the issue tracker.

Documentation

Requirements

  • Ruby 3.1 or higher
  • Bundler 2.0 or higher

Dependencies

  • faraday (~> 2.0) - HTTP client
  • faraday-retry - Request retry logic
  • faraday-multipart - File upload support

License

The gem is available as open source under the terms of the MIT License.

Support

Acknowledgments

Built with comprehensive analysis of the Airwallex API ecosystem. Special thanks to the Airwallex team for their extensive documentation and developer resources.