A framework-agnostic Ruby SDK for the Printavo GraphQL API (v2).
I use Printavo every day at Texas Embroidery Ranch. This gem was created to bridge Printavo with other operational systems—CRM, marketing, finance, and automation—so that print shops can build integrated workflows without writing raw GraphQL by hand.
Features
- Full Printavo v2 GraphQL API support
- Resource-oriented interface:
client.customers.all,client.orders.find(id) - Raw GraphQL access:
client.graphql.query("{ ... }") - Sanitized GraphQL response envelopes that preserve partial data and errors
- Rich domain models:
order.status,order.status?(:in_production),order.customer - Rack-compatible webhook signature verification
- Multi-client support — no globals required
- Ruby 3.0+ required
Installation
Add to your Gemfile:
gem "printavo-ruby"or
bundle add printavo-rubyor install directly:
gem install printavo-rubyAuthentication
Printavo authenticates via your account email and API token (found at My Account → API Token on printavo.com).
require "printavo"
client = Printavo::Client.new(
email: ENV["PRINTAVO_EMAIL"],
token: ENV["PRINTAVO_TOKEN"]
)Rails Initializer
# config/initializers/printavo.rb
PRINTAVO = Printavo::Client.new(
email: ENV["PRINTAVO_EMAIL"],
token: ENV["PRINTAVO_TOKEN"]
)Usage
Customers
# List customers (25 per page by default)
customers = client.customers.all
customers.each { |c| puts "#{c.full_name} — #{c.email}" }
# Paginate
page_2 = client.customers.all(first: 25, after: cursor)
# Find a specific customer
customer = client.customers.find("12345")
puts customer.full_name # => "Jane Smith"
puts customer.company # => "Acme Shirts"Bounded Contact Pages
page = client.contacts.page(
first: 50,
after: previous_cursor,
primary_only: true,
sort_on: Printavo::Enums::ContactSortField::CONTACT_NAME
)
page.records.each { |contact| puts contact.full_name }
next_cursor = page.end_cursor if page.has_next_page
# Partial GraphQL responses preserve successful records and sanitized errors.
warn(page.errors.inspect) unless page.success?contacts.page performs exactly one request and caps first at 100. It never
follows the next cursor automatically.
page.response_payload contains the exact frozen binary HTTP response body
captured before JSON parsing. It is intentionally omitted from ordinary
inspection and serialization because provider responses may contain private
customer data. Persist or log it only through an explicit encrypted evidence
boundary.
Captured contact responses can be replayed through the same SDK parser without provider access:
replayed_page = client.contacts.page_from_response_payload(
page.response_payload,
metadata: page.metadata
)GraphQL Response Envelopes
envelope = client.graphql.query_envelope(
"query Contact($id: ID!) { contact(id: $id) { id email } }",
variables: { id: "123" }
)
envelope.data
envelope.errors
envelope.metadata
envelope.partial?query and mutate retain their strict behavior and raise Printavo::ApiError
when GraphQL errors are present. Envelope methods preserve partial data without
retaining credentials, raw HTTP objects, or provider response bodies.
Orders
# List orders
orders = client.orders.all
orders.each { |o| puts "#{o.nickname}: #{o.status}" }
# Find an order
order = client.orders.find("99")
puts order.status # => "In Production"
puts order.status_key # => :in_production
puts order.status?(:in_production) # => true
puts order.total_price # => "1250.00"
puts order.customer.full_name # => "Bob Johnson"Quotes
Use the dedicated quote queries when invoice records should be excluded:
quotes = client.quotes.all
quote = client.quotes.find("99")
puts quote.total
puts quote.updated_atThese methods map directly to Printavo's documented
quotes and
quote queries.
Jobs (Line Items)
# List jobs for an order
jobs = client.jobs.all(order_id: "99")
jobs.each { |j| puts "#{j.name} x#{j.quantity} @ #{j.price}" }
# Find a specific job
job = client.jobs.find("77")
puts job.taxable? # => trueRich Line Items
line_item = client.line_items.find("77")
line_item.markup_percentage
line_item.price_receipt.price
line_item.personalizations.each { |entry| puts entry.personalization }
line_item.product_status
line_item.product.item_number
line_item.po_line_item.purchase_order.visual_po_idThese accessors follow Printavo's documented
LineItem shape. The
legacy name, quantity, and taxable readers remain available for older
captured payloads; current responses use description, items, and taxed.
Direct Pricing and Transaction Details
Price a line-item group with Printavo's documented input shape:
pricing = client.line_item_groups.pricing(
enabled_columns: { markupPercentage: true },
imprints: [],
line_items: [{ description: "Core Cotton Tee", position: 0 }],
position: 0
)
puts pricing.price
puts pricing.signatureRetrieve a transaction's ledger details directly:
details = client.transactions.detail("9")
puts details.amountThese helpers map to Printavo's documented
lineItemGroupPricing
and
transactionDetail
queries. The pricing input accepts enabledColumns, imprints, lineItems,
and position; it does not accept a pricing-matrix ID.
Payment Ledger
client.transactions.all(order_id: "99").each do |transaction|
case transaction
when Printavo::Payment
puts "Payment: #{transaction.amount}"
transaction.disputes.each { |dispute| puts dispute.status }
when Printavo::Refund, Printavo::Return, Printavo::Void
puts "Adjustment: #{transaction.amount}"
end
endTransactions are returned as their documented concrete union type:
Printavo::Payment, Printavo::PaymentDispute, Printavo::Refund,
Printavo::Return, or Printavo::Void.
Pagination
All list resources support each_page and all_pages in addition to all.
# Iterate page by page (cursor-based, memory-efficient)
client.customers.each_page(first: 50) do |records|
records.each { |c| puts c.full_name }
end
# Collect every record across all pages into one array
all_orders = client.orders.all_pages
# Jobs require order_id
client.jobs.each_page(order_id: "99") do |records|
records.each { |j| puts j.name }
end
all_jobs = client.jobs.all_pages(order_id: "99")Mutations
Customers
# Create
customer = client.customers.create(
primary_contact: { firstName: "Jane", lastName: "Smith", email: "jane@example.com" },
company_name: "Acme Shirts"
)
puts customer.full_name # => "Jane Smith"
puts customer.company # => "Acme Shirts"
# Update
customer = client.customers.update("42", company_name: "New Name Inc")Orders
# Create (Printavo creates orders as quotes first)
order = client.orders.create(
contact: { id: "456" },
due_at: "2026-06-01T09:00:00Z",
customer_due_at: "2026-06-01",
nickname: "Summer Rush"
)
# Update
order = client.orders.update("99", nickname: "Rush Job", production_note: "Ships Friday")
# Move to a new status
registry = client.statuses.registry
order = client.orders.update_status("99", status_id: registry[:in_production].id)
puts order.status # => "In Production"Inquiries
# Create
inquiry = client.inquiries.create(
name: "Bob Johnson",
email: "bob@example.com",
request: "100 hoodies, front + back print"
)
# Update
inquiry = client.inquiries.update("55", nickname: "Hoodies Rush")Statuses
# List all statuses
statuses = client.statuses.all
statuses.each { |s| puts "#{s.name} (#{s.color})" }
# Build a registry for O(1) lookup by symbol key
registry = client.statuses.registry
registry[:in_production] # => <Printavo::Status>
registry[:in_production].color # => "#ff6600"
# Pair with an order's status_key
order = client.orders.find("99")
status = registry[order.status_key]
puts "#{order.status} — #{status.color}"Inquiries
# List inquiries (quotes / leads)
inquiries = client.inquiries.all
inquiries.each { |i| puts "#{i.nickname}: #{i.status}" }
# Find a specific inquiry
inquiry = client.inquiries.find("55")
puts inquiry.status?(:new_inquiry) # => true
puts inquiry.customer.full_name # => "Jane Smith"Raw GraphQL
For queries not yet wrapped by a resource, use the raw GraphQL client directly:
result = client.graphql.query(<<~GQL)
{
customers(first: 5) {
nodes {
id
firstName
lastName
}
}
}
GQL
result["customers"]["nodes"].each { |c| puts c["firstName"] }With variables:
result = client.graphql.query(
"query Customer($id: ID!) { customer(id: $id) { id email } }",
variables: { id: "42" }
)Mutations
Use mutate for GraphQL write operations:
result = client.graphql.mutate(
<<~GQL,
mutation UpdateOrder($id: ID!, $nickname: String!) {
updateOrder(id: $id, input: { nickname: $nickname }) {
order { id nickname }
errors
}
}
GQL
variables: { id: "99", nickname: "Rush Job" }
)
result["updateOrder"]["order"]["nickname"] # => "Rush Job"Raw GraphQL Pagination
Paginate any custom query without a resource wrapper using paginate.
The path: option is dot-separated and maps to the connection in the response:
client.graphql.paginate(MY_QUERY, path: "orders", first: 50) do |nodes|
nodes.each { |n| puts n["nickname"] }
end
# Nested connection (e.g. order.lineItems)
client.graphql.paginate(JOBS_QUERY, path: "order.lineItems",
variables: { orderId: "99" }) do |nodes|
nodes.each { |j| puts j["name"] }
endWebhooks
Printavo::Webhooks.verify provides Rack-compatible HMAC-SHA256 signature verification.
No extra dependencies required.
# Pure Ruby / Rack
valid = Printavo::Webhooks.verify(
signature, # X-Printavo-Signature header value
payload, # raw request body string
secret # your webhook secret
)Rails Controller Example
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def printavo
if Printavo::Webhooks.verify(
request.headers["X-Printavo-Signature"],
request.raw_post,
ENV["PRINTAVO_WEBHOOK_SECRET"]
)
event = JSON.parse(request.raw_post)
# process event["type"] ...
head :ok
else
head :unauthorized
end
end
endError Handling
begin
client.orders.find("not_a_real_id")
rescue Printavo::AuthenticationError => e
# Bad email/token
rescue Printavo::RateLimitError => e
# Exceeded 10 req/5 sec — back off and retry
rescue Printavo::NotFoundError => e
# Resource doesn't exist
rescue Printavo::ApiError => e
# GraphQL error — e.message contains details, e.response has raw data
rescue Printavo::Error => e
# Catch-all for any Printavo error
endVersioning Roadmap
- CHANGELOG — what shipped in each release
- RELEASING — guarded packaging and RubyGems publication
- TODO — planned versions, API coverage gaps, and stretch goals
Rules: PATCH = bug fix · MINOR = new backward-compatible feature · MAJOR = breaking change
API Documentation
Development
git clone https://github.com/scarver2/printavo-ruby.git
cd printavo-ruby
mise install
mise exec -- bundle install
# Run specs across the active Ruby matrix
bin/spec
# Lint across the active Ruby matrix
bin/lint
# Guard DX (watches files, re-runs tests + lint on save)
mise exec -- bundle exec guard
# Interactive console
PRINTAVO_EMAIL=you@example.com PRINTAVO_TOKEN=your_token bin/consoleSee CONTRIBUTING.md for full contribution guidelines. Report security issues through the private process in SECURITY.md.
Colophon
©2026 Stan Carver II

