Ruby client for the Lavenda Pay payments API. Framework-agnostic (no Rails/ActiveSupport dependency): create customers and orders, and verify/parse webhooks.
gem "lavenda-pay-ruby"Settings are read from LAVENDA_PAY_* environment variables and, inside a Rails app, from the lavenda_pay
credentials, so a Rails app needs no initializer:
# bin/rails credentials:edit
lavenda_pay:
base_url: https://pay.example.com
api_token: ...
webhook_secret: ...| Setting | Environment variable | Credential |
|---|---|---|
base_url |
LAVENDA_PAY_BASE_URL |
lavenda_pay.base_url |
api_token |
LAVENDA_PAY_API_TOKEN |
lavenda_pay.api_token |
webhook_secret |
LAVENDA_PAY_WEBHOOK_SECRET |
lavenda_pay.webhook_secret |
base_url is the root of the Lavenda Pay installation, without the /api suffix. Use the company domain: the API
accepts any host, but the public order pages are only served on the company's own domain.
Values set explicitly win over both:
LavendaPay.configure do |config|
config.base_url = "https://pay.example.com"
config.logger = Rails.logger
endEvery operation is a class with .call that uses this global configuration. For several accounts, build a client and
pass it to the operation:
client = LavendaPay::Client.new(base_url: "...", api_token: "...")
LavendaPay::Orders::Find.new(client:).call("order_...")customer = LavendaPay::Customers::Create.call(
full_name: "Pedro Henrique", user_email: "pedro@example.com",
document_type: "cpf", document_number: "71415355436",
dial_code: "55", phone_number: "(49) 98726-1668", eid: "TUTOR-1"
)
customer.pid # => "cus_..."
# Only `user_email` and `document_number` are supported filters (the API ignores
# anything else and would return every customer); returns nil when none match.
LavendaPay::Customers::FindBy.call(document_number: "71415355436") ||
LavendaPay::Customers::FindBy.call(user_email: "pedro@example.com")
LavendaPay::Customers::Find.call("cus_...")
LavendaPay::Customers::List.call(user_email: "pedro@example.com", page: 1)order = LavendaPay::Orders::Create.call(
customer_pid: customer.pid,
payment_method_kinds: ["pix", "credit_card"],
description: "Material didático",
eid: "ORD-0123",
frequency: "once",
currency: "brl",
items_attributes: [{product_title: "Material didático", quantity: 1, unit_price: 450.0}]
)
order.pid # => "order_..."
order.status # => "draft"
LavendaPay::Orders::Find.call(order.pid)
# Public payment page for the order, built from `base_url`.
LavendaPay::Url.order_path(order.pid) # => "https://pay.example.com/orders/order_..."Sandbox only: confirm a pending payment as if the customer had paid it, to test your payment flow
end to end. LavendaPay marks it as paid and, when the gateway supports it, confirms it there too.
The endpoint does not exist in production, where it raises LavendaPay::NotFoundError.
# The payment pid comes from the payment outcome webhooks.
payment = LavendaPay::Payments::Confirm.call("pay_...")
payment.status # => "paid"
payment.gateway_confirmation # => "confirmed", "failed" or "unsupported"Non-2xx responses raise a LavendaPay::ApiError subclass exposing status, body and errors:
BadRequestError (400), AuthenticationError (401/403), NotFoundError (404), UnprocessableError (422),
ServerError (5xx). Network failures raise ConnectionError; missing config raises ConfigurationError.
begin
LavendaPay::Customers::Create.call(params)
rescue LavendaPay::UnprocessableError => error
error.errors # => {"user_email" => ["is invalid"]}
endLavenda Pay POSTs JSON to your endpoint with the shared secret in the header exposed as
LavendaPay::Webhooks::Signature::HEADER.
Mount the engine and give it a handler. It verifies the secret, parses the payload and calls your handler with a
LavendaPay::Webhooks::Event for every supported event.
# config/routes.rb
mount LavendaPay::Engine => "/webhooks/lavenda_pay"
# config/initializers/lavenda_pay.rb
LavendaPay.configure do |config|
# ...base_url, api_token, webhook_secret
config.on_event = ->(event) { MyApp::Webhooks::Process.call(event:) } # any callable
endResponses: 200 for handled and unsupported events, 401 for a wrong or missing secret, 422 for an invalid payload.
Exceptions raised by the handler are not rescued, so the request fails with 500 and the sender retries; make the
handler idempotent (see event.idempotency_key). Without on_event configured the endpoint raises a
ConfigurationError.
class Webhooks::LavendaPayController < ActionController::API
def create
event = LavendaPay::Webhooks::ConstructEvent.call(
request.raw_post, request.headers[LavendaPay::Webhooks::Signature::HEADER]
)
return head :ok unless event.supported?
if event.order?
order = Order.find_by!(gateway_order_pid: event.pid)
order.update!(status: event.data["status"], paid: event.name == "order_paid")
end
head :ok
rescue LavendaPay::InvalidWebhookSignatureError
head :unauthorized
rescue LavendaPay::InvalidWebhookPayloadError
head :unprocessable_entity
end
endLavendaPay::Webhooks::VerifySignature.call(secret) and LavendaPay::Webhooks::ParseEvent.call(raw_body) are
available separately. event.idempotency_key ("order_paid:Order:order_123") is stable across redeliveries; use it to de-duplicate.
Supported events: order_created, order_pending, order_partially_paid, order_paid, order_refund_requested,
order_refunded, order_cancelled, payment_created, payment_processing, payment_pending, payment_paid,
payment_refused, payment_refund_requested, payment_refunded, payment_partially_refunded, payment_canceled.
bundle install
bundle exec rspec