A modern Ruby client library for the Lightspeed Retail (X-Series/Vend) API.
This gem was entirely based on Vend Ruby v2 by Yurkiv Misha.
📦 Migrating from vend-ruby-v2? This gem was previously called
vend-ruby-v2. See MIGRATION.md for upgrade instructions. Full backward compatibility is maintained in v0.3.0.
- ✅ Automatic Pagination - Efficiently handle large datasets with cursor-based pagination
- ✅ Automatic Retry Logic - Built-in handling for rate limits and transient failures
- ✅ OAuth Scope Support - Declare required scopes on resources; get early, clear errors for missing scopes
- ✅ Modern Ruby 3+ - Leverages latest Ruby features and idioms
- ✅ Thread-Safe - Support for concurrent API requests
- ✅ Comprehensive - Full coverage of Lightspeed Retail API v2.0 resources
Add this line to your application's Gemfile:
gem 'lightspeed-retail-ruby'And then execute:
$ bundle installOr install it yourself as:
$ gem install lightspeed-retail-ruby# Configure the client
Lightspeed.configure do |config|
config.domain_prefix = ENV['LIGHTSPEED_DOMAIN_PREFIX'] # Your store name
config.access_token = ENV['LIGHTSPEED_ACCESS_TOKEN'] # OAuth token
config.scopes = ["products:read", "sales:read"] # OAuth scopes (recommended)
end
# Fetch a single page of products
products = Lightspeed::Product.all
# => {:data=>[{:id=>"...", :name=>"Product 1", ...}], :version=>{:min=>1, :max=>100}}
# Fetch all products automatically (handles pagination)
all_products = Lightspeed::Product.auto_paginate_v2
# => [{:id=>"...", :name=>"Product 1"}, {:id=>"...", :name=>"Product 2"}, ...]
# Fetch a specific product
product = Lightspeed::Product.find("product-id-here")
# => {:data=>{:id=>"...", :name=>"Product 1", ...}}The gem provides powerful pagination support for v2.0 API endpoints that prevents memory issues when working with large datasets.
Automatically fetch all pages and return a flat array:
# Fetch all products across all pages
all_products = Lightspeed::Product.auto_paginate_v2
# => [{...}, {...}, ...] (all products)
# With filters
deleted_products = Lightspeed::Product.auto_paginate_v2(deleted: true)
# Works with all v2.0 resources
all_customers = Lightspeed::Customer.auto_paginate_v2
all_sales = Lightspeed::Sale.auto_paginate_v2Process pages one at a time for better memory efficiency:
Lightspeed::Product.each_page_v2 do |products|
products.each do |product|
puts "Processing: #{product[:name]}"
# Process each product without loading everything into memory
end
end
# With filters
Lightspeed::Product.each_page_v2(deleted: true) do |products|
# Process deleted products page by page
endFor backward compatibility, .all returns only the first page:
response = Lightspeed::Product.all
products = response[:data] # First page of products
version = response[:version][:max] # Cursor for next page
# Manual pagination
next_page = Lightspeed::Product.all(after: version)The gem automatically handles rate limits and transient failures with exponential backoff.
Requests automatically retry on:
- 429 Too Many Requests (rate limiting)
- 503 Service Unavailable (transient failures)
Configuration:
- Max retries: 3
- Backoff: 0.5s → 1s → 2s (with randomization)
- Respects
Retry-Afterheader from API
# This will automatically retry if rate limited
products = Lightspeed::Product.auto_paginate_v2
# No additional code needed - retry happens automatically!All API errors raise specific exceptions:
begin
product = Lightspeed::Product.find("invalid-id")
rescue Lightspeed::NotFound => e
puts "Product not found"
rescue Lightspeed::TooManyRequests => e
# Already retried 3 times, still rate limited
puts "Rate limit exceeded, retry after: #{e.response_headers[:retry_after]}"
rescue Lightspeed::Unauthorized => e
puts "Invalid access token"
rescue Lightspeed::HttpError => e
# Catch-all for any HTTP error
puts "API error: #{e.message}"
endAvailable exceptions:
Lightspeed::BadRequest(400)Lightspeed::Unauthorized(401)Lightspeed::Forbidden(403)Lightspeed::NotFound(404)Lightspeed::TooManyRequests(429)Lightspeed::InternalServerError(500)Lightspeed::ServiceUnavailable(503)- And more... (see lib/lightspeed/exception.rb)
All standard Lightspeed Retail API v2.0 resources are supported:
# Products
Lightspeed::Product.all
Lightspeed::Product.find(id)
Lightspeed::Product.create(params)
Lightspeed::Product.update(id, params)
# Customers
Lightspeed::Customer.all
Lightspeed::Customer.find(id)
# Sales
Lightspeed::Sale.all
Lightspeed::Sale.find(id)
Lightspeed::Sale.create(params)
# Other resources
Lightspeed::Brand
Lightspeed::Consignment
Lightspeed::CustomerGroup
Lightspeed::Inventory
Lightspeed::Outlet
Lightspeed::PaymentType
Lightspeed::PriceBook
Lightspeed::Register
Lightspeed::Supplier
Lightspeed::Tax
Lightspeed::User
# ... and moreSee lib/lightspeed/resources/ for the complete list.
For single-threaded applications or CLIs:
Lightspeed.configure do |config|
config.domain_prefix = 'your-store'
config.access_token = 'your-token'
end
# Use resources normally
products = Lightspeed::Product.allFor multi-threaded applications, create separate connections per thread:
# Create a connection
config = Lightspeed::Config.new(
domain_prefix: ENV['LIGHTSPEED_DOMAIN_PREFIX'],
access_token: ENV['LIGHTSPEED_ACCESS_TOKEN']
)
connection = Lightspeed::Connection.build(config)
# Pass connection to requests
products = Lightspeed::Product.all(connection: connection)
customer = Lightspeed::Customer.find(id, connection: connection)
# Example: Thread pool
connections = ThreadLocal.new do
Lightspeed::Connection.build(Lightspeed::Config.new(
domain_prefix: ENV['LIGHTSPEED_DOMAIN_PREFIX'],
access_token: ENV['LIGHTSPEED_ACCESS_TOKEN']
))
end
threads = 10.times.map do |i|
Thread.new do
conn = connections.value
Lightspeed::Product.all(connection: conn)
end
end
threads.each(&:join)# Initialize OAuth2 client
auth = Lightspeed::Oauth2::AuthCode.new(
'your-store', # domain prefix
'your-client-id', # OAuth client ID
'your-client-secret', # OAuth client secret
'your-redirect-uri' # OAuth redirect URI
)
# Get authorization URL — scopes are required from 1 June 2026
# Option 1: pass scopes explicitly
auth_url = auth.authorize_url(scopes: ["products:read", "sales:read", "customers:read"])
# Option 2: configure scopes globally and they'll be picked up automatically
Lightspeed.configure { |c| c.scopes = ["products:read", "sales:read"] }
auth_url = auth.authorize_url # scope parameter included automatically
# Exchange authorization code for token
token = auth.token_from_code(params[:code])
access_token = token.token
refresh_token = token.refresh_token
# Refresh an expired token
new_token = auth.refresh_token(access_token, refresh_token)Lightspeed requires the scope parameter in all OAuth authorization requests from 1 June 2026. This gem provides full scope support including early detection of scope mismatches before an API call is made.
Declare the scopes your application needs in Lightspeed.configure. Use only the scopes you actually need:
Lightspeed.configure do |config|
config.domain_prefix = 'your-store'
config.access_token = 'your-token'
config.scopes = [
"products:read",
"sales:read",
"customers:read",
"customers:write"
]
endScopes can also be passed as a space-delimited string: config.scopes = "products:read sales:read".
When scopes are configured, calling a resource method that requires a scope not in the configured list raises Lightspeed::ScopeError immediately — before any HTTP request is made:
Lightspeed.configure do |config|
config.scopes = ["products:read"] # no sales scope
end
Lightspeed::Sale.all
# => Lightspeed::ScopeError: Lightspeed::Sale.all requires OAuth scope 'sales:read'.
# Add it to your Lightspeed.configure block: config.scopes = ["sales:read"]If no scopes are configured the check is skipped entirely, so existing code continues to work without changes.
Use .scope_required to inspect what scope a particular action requires — useful when assembling your authorization URL:
Lightspeed::Sale.scope_required(:all) #=> "sales:read"
Lightspeed::Sale.scope_required(:create) #=> "sales:write"
Lightspeed::Sale.scope_required(:return) #=> "sales:write"
Lightspeed::Product.scope_required(:find) #=> "products:read"
Lightspeed::Product.scope_required(:inventory) #=> "inventory:read"
# Some resources require any one of several write scopes
Lightspeed::Consignment.scope_required(:create)
#=> ["consignments:write:inventory_count",
# "consignments:write:stock_order",
# "consignments:write:stock_transfer"]| Resource | Read scope | Write scope(s) |
|---|---|---|
| Brand, Tag, ProductType, ProductImage | products:read |
products:write |
| Consignment | consignments:read |
consignments:write:inventory_count / consignments:write:stock_order / consignments:write:stock_transfer |
| Customer, CustomerGroup | customers:read |
customers:write |
| Inventory | inventory:read |
— |
| Outlet | outlets:read |
— |
| OutletProductTax, Tax | taxes:read |
taxes:write |
| PaymentType | payment_types:read |
— |
| PriceBook, PriceBookProduct | products:read:price_books |
products:write:price_books |
| Product | products:read |
products:write |
| Register | registers:read |
— |
| Sale | sales:read |
sales:write |
| Supplier | suppliers:read |
suppliers:write |
| User | users:read |
users:write |
| Webhook | webhooks |
webhooks |
See Lightspeed scope documentation for the full list.
All methods accept a params hash:
# Filter products
active_products = Lightspeed::Product.all(active: true)
# Pagination with filters
deleted_products = Lightspeed::Product.auto_paginate_v2(deleted: true)
# Custom connection
products = Lightspeed::Product.all(connection: custom_connection)Resources automatically use the correct API version:
# Most resources use v2.0 API
Lightspeed::Product.all # => GET /api/2.0/products
# Some resources use v0.9 for backward compatibility
Lightspeed::Webhook.all # => GET /api/webhooks (v0.9)
# Explicit version methods when needed
Lightspeed::Product.find_v0_9(id) # Use v0.9 endpointThis gem includes several performance optimizations:
- Frozen string literals - Reduces memory allocations by ~5-10%
- Efficient pagination - Cursor-based pagination prevents loading large datasets into memory
- Connection reuse - HTTP connection pooling via Faraday
- Automatic retry - Exponential backoff prevents wasted requests
Lightspeed.configure do |config|
# Required
config.domain_prefix = 'your-store' # Your Lightspeed store name
config.access_token = 'your-token' # OAuth access token
# Recommended — required for new OAuth connections from 1 June 2026
config.scopes = ["products:read", "sales:read"] # Array or space-delimited string
# Optional
config.request_type = :json # Request format (default: :json)
# config.request_type = :url_encoded # For webhooks
# Note: Timeout, retry, and other connection settings are
# configured automatically with sensible defaults
endDefault connection settings:
- Timeout: 120 seconds
- Retry: 3 attempts with exponential backoff
- Rate limit handling: Automatic with
Retry-Afterheader support
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install.
bundle exec rspecbundle exec rspec
# See coverage report in coverage/index.htmlBug reports and pull requests are welcome on GitHub at https://github.com/coaxsoft/lightspeed-retail-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.