smartbill-sdk
A Ruby SDK for the SmartBill Cloud REST API,
offering a synchronous client with typed request/response models covering
every endpoint in the official openapi.json spec.
This is a Ruby port of the Python
smartbill-rest-sdk.
Features
- Synchronous
Smartbill::Sdk::Client. - Typed request/response models built on dry-struct (type coercion, required-attribute presence, snake_case ⇄ camelCase aliasing) with dry-validation contracts enforcing semantic rules (date formats, payment-type enum, positive amounts, recipient e-mail shape) before every request is sent.
- snake_case Ruby attributes aliased to camelCase JSON automatically
(
company_vat_code↔companyVatCode). - Permissive parsing — unknown API fields are ignored, so new fields don't break parsing.
- Helper exception hierarchy with the API
errorTextsurfaced. - Optional client-side rate limiter (SmartBill allows 30 calls / 10s, then blocks for 10 minutes).
- Runtime dependencies:
dry-struct,dry-validation,dry-types,dry-inflector,zeitwerk(autoloading), and the stdlibbase64gem (usesNet::HTTPunder the hood).
Installation
Add to your application's Gemfile:
gem "smartbill-sdk"
Or install manually:
gem install smartbill-sdk
From source (development):
bundle install
bundle exec rake test # run the test suite
bundle exec rake rubocop # lint
bundle exec rake rbs:validate # validate the RBS type signatures
The gem ships RBS type signatures in
sig/smartbill/sdk.rbs covering the full public API (client, services,
models, contracts, transport, exceptions). Opt-in typechecking with
Steep is configured via the Steepfile,
though a fully clean steep check currently requires RBS signatures for
dry-struct / dry-validation, which are not bundled with those gems.
Authentication
SmartBill uses HTTP Basic Auth with username:token:
username— the e-mail you log in with in SmartBill Cloud.token— found in SmartBill Cloud > Contul Meu > Integrari > API.
require "smartbill/sdk"
client = Smartbill::Sdk::Client.new(username: "you@example.com", token: "abc123...")
Quick start
require "smartbill/sdk"
include Smartbill::Sdk
client = Client.new(username: "you@example.com", token: "...")
invoice = Models::Invoice.new(
company_vat_code: "RO12345678",
client: Models::Client.new(name: "Intelligent IT", vat_code: "RO12345678",
city: "Sibiu", country: "Romania"),
series_name: "FCT",
is_draft: false,
products: [
Models::Product.new(name: "Produs 1", measuring_unit_name: "buc", currency: "RON",
quantity: 2, price: 10, is_tax_included: true,
tax_name: "Redusa", tax_percentage: 9)
]
)
resp = client.invoices.create(invoice)
puts "Factura emisa: seria #{resp.series}, numarul #{resp.number}"
The client can also be used with a block that closes it automatically:
Client.new(username: "...", token: "...").with_client do |c|
c.invoices.create(invoice)
end
Services
| Attribute | Service | Covers |
|---|---|---|
client.invoices |
InvoicesService |
create, delete, reverse (storno), cancel, restore, payment status, PDF |
client.estimates |
EstimatesService |
create, delete, cancel, restore, PDF, invoices-status |
client.payments |
PaymentsService |
create (general / chitanta / bon fiscal), delete, fiscal-receipt text |
client.email |
EmailService |
POST /document/send |
client.taxes |
ConfigurationService |
GET /tax (taxes), GET /series (series) |
client.series |
ConfigurationService |
alias of client.taxes — same instance |
client.stocks |
StocksService |
GET /stocks |
Lifecycle: storno / cancel / restore / PDF
storno = Models::StornoRequest.new(company_vat_code: cif, series_name: "FCT", number: "0040")
st = client.invoices.reverse(storno)
puts st.document_url
client.invoices.cancel(cif, "FCT", "0040") # PUT /invoice/cancel
client.invoices.restore(cif, "FCT", "0040") # PUT /invoice/restore
pdf_bytes = client.invoices.pdf(cif, "FCT", "0040") # raw binary String
File.binwrite("factura.pdf", pdf_bytes)
Taxes, series and stocks
taxes = client.taxes.taxes("RO12345678")
taxes.taxes.each { |t| puts "#{t.name}: #{t.percentage}%" }
series = client.series.series("RO12345678", type: "f") # type: f/c/p/i/n
series.list.each { |s| puts "#{s.name}: #{s.next_number}" }
stocks = client.stocks.get("RO12345678", "2024-05-01", warehouse_name: "Depozit")
stocks.list.each { |entry| entry.products.each { |p| puts p.product_name } }
subject and body_text must be Base64-encoded by the caller, as the
SmartBill API requires.
email = Models::EmailDocument.new(
company_vat_code: "RO12345678", series_name: "FCT", number: "0040",
type: Models::DocumentType::INVOICE, to: "client@example.ro",
subject: Base64.strict_encode64("Factura FCT0040"),
body_text: Base64.strict_encode64("Va trimitem Factura FCT0040.")
)
resp = client.email.send(email)
puts resp.status.code, resp.status.
Errors
All errors descend from Smartbill::Sdk::Error:
AuthError— HTTP 401 (bad username/token/company CIF).RateLimitError— HTTP 403 (rate-limited, blocked 10 min).APIError— has.error_text,.message_field,.status_code(the API'serrorTextis surfaced in.error_text).TransportError— network-level failure.ValidationError— a model is missing required fields or fails its validation contract (bad date format, unknown payment type, non-positive amount, etc.). Raised before any HTTP call is made.
rescue Smartbill::Sdk::AuthError => e
# ...
rescue Smartbill::Sdk::APIError => e
puts e.error_text, e.status_code
end
Validation
Request models are checked against a dry-validation contract before being
sent, so malformed requests raise ValidationError locally instead of
round-tripping to the SmartBill API. The contracts enforce:
- date fields match
YYYY-MM-DD; Payment#typeis one of the SmartBill payment types;EmailDocument#typeisfactura/proformaand recipients look like e-mail addresses;- numeric amounts are positive;
precisionis a non-negative integer; - nested payment-at-issuance blocks (
Invoice#payment) are validated too.
You can also run a contract explicitly:
Smartbill::Sdk::Contracts::InvoiceContract.validate!(invoice) # raises ValidationError
result = Smartbill::Sdk::Contracts::InvoiceContract.new.call(invoice.to_attributes)
result.success? # => true / false
result.errors.to_h # => { issue_date: ["is in invalid format"] }
Rate limiting
SmartBill allows 30 calls / 10 seconds; exceeding it triggers a server-side
403 that blocks access for 10 minutes. Opt into a client-side preemptive
limiter with enforce_rate_limit::
client = Smartbill::Sdk::Client.new(username: "...", token: "...", enforce_rate_limit: true)
Notes
- The SDK talks JSON only (
format="json"); XML is not supported. - All date fields use
YYYY-MM-DDstrings, matching the API. - Only a synchronous client is provided. For concurrency, run
independent requests on separate threads (each
Net::HTTPrequest opens its own connection — seeexamples/taxes_and_stocks_sync.rb).
Examples
Runnable scripts live in examples/:
| Script | Demonstrates |
|---|---|
create_invoice_sync.rb |
Issuing an invoice |
create_estimate_sync.rb |
Issuing a proforma + invoices-status |
create_payment_sync.rb |
Registering a Chitanta payment |
invoice_lifecycle_sync.rb |
Storno / cancel / restore / PDF |
list_series_sync.rb |
GET /series |
send_email_sync.rb |
POST /document/send with Base64 |
fiscal_receipt_sync.rb |
Bon fiscal with mixed cash/card payment |
taxes_and_stocks_sync.rb |
Concurrent GET /tax + GET /stocks via threads |
Agent skills
This repo ships ready-to-import pi
skills under skills/ that teach coding agents how to use
the SDK. Each SKILL.md is a self-contained, copy-pasteable guide for one
area of the API:
| Skill | Covers |
|---|---|
smartbill-invoices |
Invoices & proformas/estimates: create, storno, cancel, restore, PDF, payment status |
smartbill-payments |
Payments & fiscal receipts (bon fiscal): POST /payment, payment types, mixed cash/card, fiscal-printer text, delete |
smartbill-email |
Emailing a document (POST /document/send): base64 subject/body, invoice/proforma |
See skills/README.md for how to import them into a pi
agent. The runnable scripts in examples/ accompany these
skills.
Disclaimer
This SDK was written by an AI agent (pi) as a Ruby port of the Python
smartbill-rest-sdk, which was itself generated from the official
openapi.json spec. The Ruby port is verified with a suite of 60 mocked
tests (using WebMock). Please have a human review it before issuing real
invoices — accountants work hard enough as it is.
License
The gem is available as open source under the terms of the MIT License.