The project is in a healthy, maintained state
Create customers and orders on Lavenda Pay and verify/parse its webhooks, without depending on Rails.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 2.1
>= 7.1
~> 13.0
~> 3.13
~> 3.23
 Project Readme

Lavenda Pay Ruby

Ruby client for the Lavenda Pay payments API. Framework-agnostic (no Rails/ActiveSupport dependency): create customers and orders, and verify/parse webhooks.

Installation

gem "lavenda-pay-ruby"

Configuration

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
end

Every 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_...")

Customers

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)

Orders

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_..."

Payments

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"

Errors

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"]}
end

Webhooks

Lavenda Pay POSTs JSON to your endpoint with the shared secret in the header exposed as LavendaPay::Webhooks::Signature::HEADER.

Rails engine (recommended)

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
end

Responses: 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.

Your own controller

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
end

LavendaPay::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.

Development

bundle install
bundle exec rspec