EposNowClient

Gem Version CI License: MIT

A lightweight, fully-tested Ruby client for the Epos Now REST API (V4).

131 specs | 100% line coverage | 100% branch coverage | 0 RuboCop offenses

Features

  • Basic Auth with API Key + Secret (Base64-encoded)
  • 10 resource classes: Products, Categories, Transactions, TenderTypes, TaxGroups, Customers, Staff, Devices, Brands, Locations
  • Automatic pagination (200 items/page, fetches all pages)
  • Retry with exponential backoff on transient network errors
  • Structured error hierarchy for clean exception handling
  • Search & filtering using Epos Now query syntax
  • Zero runtime dependencies beyond HTTParty

Installation

Add to your Gemfile:

gem 'epos_now_client'

Then run:

bundle install

Or install directly:

gem install epos_now_client

Quick Start

require 'epos_now_client'

client = EposNowClient::Client.new(
  api_key: 'your_api_key',
  api_secret: 'your_api_secret'
)

# Fetch all products
products = client.products.all

# Create a category
client.categories.create_category({ 'Name' => 'Drinks' })

# Search products
client.products.search('(Name|contains|Burger)')

Authentication

Epos Now uses Basic Auth with an API Key and Secret. Get your credentials from the Epos Now Backoffice:

  1. Navigate to Web Integrations > REST API
  2. Click Add Device
  3. Copy the API Key and API Secret

Note: Epos Now uses a single API endpoint (https://api.eposnowhq.com) for all environments. There is no separate sandbox URL. Developer accounts use the same API with test credentials.

Configuration

EposNowClient.configure do |config|
  config.api_key = ENV['EPOS_NOW_API_KEY']
  config.api_secret = ENV['EPOS_NOW_API_SECRET']
  config.base_url = 'https://api.eposnowhq.com'  # default
  config.timeout = 30                              # default (seconds)
  config.open_timeout = 10                         # default (seconds)
  config.max_retries = 3                           # default
  config.retry_delay = 1.0                         # default (seconds, doubles each retry)
  config.per_page = 200                            # default
  config.logger = Logger.new($stdout)              # optional
end

client = EposNowClient::Client.new

Per-client (for multi-account apps)

client = EposNowClient::Client.new(
  api_key: 'merchant_api_key',
  api_secret: 'merchant_api_secret',
  timeout: 60
)

Rails initializer

# config/initializers/epos_now.rb
EposNowClient.configure do |config|
  config.api_key = Rails.application.credentials.dig(:epos_now, :api_key)
  config.api_secret = Rails.application.credentials.dig(:epos_now, :api_secret)
  config.logger = Rails.logger
end

Usage

Categories

client.categories.all                                         # List all (paginated)
client.categories.find(42)                                    # Get by ID
client.categories.create_category({ 'Name' => 'Drinks' })    # Create
client.categories.update_category(42, { 'Name' => 'Bev' })   # Update
client.categories.delete_category(42)                         # Delete

Products

client.products.all                                           # List all
client.products.find(10)                                      # Get by ID
client.products.create_product({                              # Create
  'Name' => 'Burger',
  'SalePrice' => 9.99,
  'CategoryId' => 1
})
client.products.update_product(10, { 'SalePrice' => 11.99 }) # Update
client.products.delete_product(10)                            # Delete
client.products.search('(Name|contains|Burg)')                # Search

Transactions

client.transactions.all                                       # List all
client.transactions.find(100)                                 # Get by ID
client.transactions.by_date(                                  # By date range
  start_date: '2026-03-01',
  end_date: '2026-03-13'
)
client.transactions.latest                                    # Latest
client.transactions.create_transaction({                      # Create
  'TransactionItems' => [{ 'ProductId' => 1, 'Quantity' => 2 }],
  'Tenders' => [{ 'TenderTypeId' => 1, 'Amount' => 25.50 }],
  'ServiceType' => 0  # 0=EatIn, 1=Takeaway, 2=Delivery
})
client.transactions.delete_transaction(200)                   # Delete
client.transactions.validate(payload)                         # Validate

Tender Types

client.tender_types.all
client.tender_types.find(1)
client.tender_types.create_tender_type({ 'Name' => 'Gift Card' })

Tax Groups

client.tax_groups.all
client.tax_groups.find(1)

Customers

client.customers.all
client.customers.find(1)
client.customers.create_customer({ 'FirstName' => 'Jane', 'LastName' => 'Doe' })
client.customers.update_customer(1, { 'FirstName' => 'Janet' })
client.customers.delete_customer(1)
client.customers.search('(FirstName|contains|Jane)')

Staff

client.staff.all
client.staff.find(1)
client.staff.create_staff({ 'FirstName' => 'Bob', 'Pin' => '1234' })
client.staff.update_staff(1, { 'FirstName' => 'Robert' })
client.staff.delete_staff(1)

Devices

client.devices.all
client.devices.find(1)

Brands

client.brands.all
client.brands.find(1)
client.brands.create_brand({ 'Name' => 'Coca-Cola' })
client.brands.update_brand(1, { 'Name' => 'PepsiCo' })
client.brands.delete_brand(1)

Locations

client.locations.all
client.locations.find(1)

Pagination

All .all methods automatically paginate through every page (200 items per page):

# Fetches ALL products, regardless of how many pages exist
all_products = client.products.all

Search & Filtering

Epos Now supports search filters using the syntax (PropertyName|Operator|Value):

# Available operators: contains, StartsWith, EndsWith, >, >=, <, <=, like
client.products.search('(Name|contains|Burger)')
client.customers.search('(FirstName|StartsWith|J)')
client.products.search('(SalePrice|>|10.00)')

Error Handling

All errors inherit from EposNowClient::Error and include status and body attributes:

begin
  client.products.find(999)
rescue EposNowClient::AuthenticationError => e
  # 401 - Invalid API key or secret
rescue EposNowClient::NotFoundError => e
  # 404 - Resource not found
rescue EposNowClient::ValidationError => e
  # 422 - Invalid request payload
rescue EposNowClient::RateLimitError => e
  # 429 - API rate limit exceeded
rescue EposNowClient::ServerError => e
  # 500-599 - Epos Now server error
rescue EposNowClient::ConnectionError => e
  # Network errors (after all retries exhausted)
rescue EposNowClient::Error => e
  # Catch-all
  e.status  # HTTP status code (Integer or nil)
  e.body    # Parsed response body (Hash, String, or nil)
  e.message # Human-readable error message
end

Retry Logic

The client automatically retries on transient network errors with exponential backoff:

  • Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ETIMEDOUT
  • Net::OpenTimeout, Net::ReadTimeout
  • SocketError
EposNowClient.configure do |config|
  config.max_retries = 3    # Retry up to 3 times (default)
  config.retry_delay = 1.0  # 1s, 2s, 4s backoff (default)
end

API Reference

Resource Methods
client.categories all, find, create_category, update_category, delete_category
client.products all, find, create_product, update_product, delete_product, search
client.transactions all, find, create_transaction, delete_transaction, by_date, latest, validate
client.tender_types all, find, create_tender_type
client.tax_groups all, find
client.customers all, find, create_customer, update_customer, delete_customer, search
client.staff all, find, create_staff, update_staff, delete_staff
client.devices all, find
client.brands all, find, create_brand, update_brand, delete_brand
client.locations all, find

Development

git clone https://github.com/dan1d/epos_now_client.git
cd epos_now_client
bundle install

# Run the full suite
bundle exec rake            # RuboCop + RSpec

# Or individually
bundle exec rspec           # 131 specs, 100% line + branch coverage
bundle exec rubocop         # 0 offenses

# View coverage report
open coverage/index.html

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b feature/my-feature)
  3. Write tests first (TDD)
  4. Ensure 100% coverage (bundle exec rspec)
  5. Ensure 0 RuboCop offenses (bundle exec rubocop)
  6. Commit your changes (git commit -m 'Add my feature')
  7. Push to the branch (git push origin feature/my-feature)
  8. Create a Pull Request

License

MIT License. See LICENSE.txt for details.