openpix-crystal_sdk

open-pix-crystal-sdk

Crystal SDK for the OpenPix/Woovi REST API — charges, customers, payments, refunds, subscriptions, webhooks, plus webhook-signature verification.

Feature parity with the Ruby gem openpix-ruby_sdk v2.0.0, ported to idiomatic, compile-time-wired Crystal:

  • Zero runtime dependencies — stdlib only (HTTP::Client, JSON, OpenSSL/LibCrypto, Base64, URI, Log).
  • Every HTTP request is bounded by connect/read/write timeouts (defaults 10s/30s/30s, configurable). A stuck API or half-open socket raises OpenPix::TimeoutError instead of silently eating a fiber.
  • Keep-alive connection reuse — each OpenPix::Client owns one persistent HTTP::Client, so batch/paginated flows don't pay TCP+TLS setup per call. Response compression is enabled.
  • No process-wide singleton (unlike the Ruby gem) — multiple clients with different tokens coexist safely in one process.
  • RSA-SHA256 webhook signature verification against Woovi's embedded public key, via raw LibCrypto EVP bindings (the key is parsed once and memoized).

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      open-pix-crystal-sdk:
        github: MatheusBasso99/openpix-crystal_sdk
    
  2. Run shards install

Usage

require "open-pix-crystal-sdk"

# Get your AppID at https://app.openpix.com/ (API/Plugins section).
client = OpenPix::Client.new(ENV["OPENPIX_APP_ID"])

Note on attribute names: body keys are the camelCase API field names (correlationID, taxID, expiresIn, …), exactly as the Woovi REST API documents them. The typed accessors on each resource use Crystal snake_case (charge.correlation_id).

Charges

charge = client.charges
charge.init_body(JSON.parse(%({
  "correlationID": "my-correlation-id",
  "value": 500,
  "comment": "my-new-charge",
  "customer": {
    "name": "My Name",
    "taxID": "44406223412"
  }
})).as_h)

# Non-bang: always returns an OpenPix::ApiResponse
response = charge.save
if response.success?
  response.resource_response.try(&.["brCode"]?) # raw API object as JSON::Any
else
  response.error_response # error message from the API
end

# Bang: raises OpenPix::Resources::RequestError on non-200
response = charge.save!

# Charge helpers
charge.add_additional_info("order", "shiba blocks toy")
charge.set_interests(2)  # basis points per day after due date
charge.set_fines(200)    # basis points when overdue

# Listing with pagination (defaults: skip 0, limit 100)
response = charge.fetch(limit: 20)
charge.fetch_next_page!     # raises NotFetchedError / PageNotDefinedError
charge.fetch_previous_page! # when there is no fetch/page to move to

# Single resource / deletion
charge.find(id: "my-correlation-id")
charge.destroy!(id: "my-correlation-id")

Customers, payments, refunds, subscriptions, webhooks

All resources share the same interface (init_body, save/save!, fetch/fetch!, find/find!, destroy/destroy! and the pagination bangs). Actions the API does not support raise OpenPix::Resources::ActionNotImplementedError:

Resource save fetch find destroy
client.charges
client.customers
client.payments
client.refunds
client.subscriptions
client.webhooks
customer = client.customers
customer.init_body(JSON.parse(%({
  "name": "My Name",
  "taxID": "31324227036",
  "address": {"country": "Brasil", "zipcode": "02145123"}
})).as_h)
customer.save!

webhook = client.webhooks
webhook.init_body(JSON.parse(%({
  "name": "my webhook",
  "event": "OPENPIX:CHARGE_CREATED",
  "url": "https://mycompany.com.br/webhook",
  "authorization": "my-auth-check",
  "isActive": true
})).as_h)
webhook.save!

Unsupported/new API fields can be passed through verbatim with rest:

charge.init_body(params, rest: JSON.parse(%({"someNewField": true})).as_h)

Webhook signature verification

Prove a webhook actually came from Woovi. Verify the raw request body (byte-exact — do not re-encode or pretty-print it) against the x-webhook-signature header:

raw_body = context.request.body.try(&.gets_to_end) || ""
signature = context.request.headers["x-webhook-signature"]

OpenPix::Utils.verify_signature(signature, raw_body) # => true / false

Timeouts and custom endpoints

client = OpenPix::Client.new(
  ENV["OPENPIX_APP_ID"],
  base_url: "https://api.woovi.com/api", # default
  api_version: "/v1",                    # default
  connect_timeout: 5.seconds,
  read_timeout: 10.seconds,
  write_timeout: 10.seconds,             # pass nil to disable one
)

A request that exceeds a timeout raises OpenPix::TimeoutError (subclass of OpenPix::Error), and the connection is reset so the next request starts clean.

Fiber safety

A single OpenPix::Client may be shared across fibers: its transport serializes requests with a mutex, so concurrent calls are safe but not parallel. For parallel requests, create one client per fiber — there is no shared global state.

Errors

Everything raised by the SDK subclasses OpenPix::Error: OpenPix::TimeoutError, OpenPix::SignatureError, and under OpenPix::Resources: RequestError, NotFetchedError, PageNotDefinedError, ActionNotImplementedError, NotImplementedError.

Development

shards install                # deps (ameba + webmock, dev-only)
crystal spec                  # test suite
crystal tool format --check   # formatting gate
bin/ameba                     # lint gate (zero issues)
crystal build --no-codegen src/open-pix-crystal-sdk.cr  # fast type-check

Coverage (100% of src/ is the gate), measured with kcov:

crystal build --debug spec/spec_helper.cr -o bin/spec_runner
kcov --clean --include-path="$(pwd)/src" coverage ./bin/spec_runner
# open coverage/index.html

On macOS the spec binary may need the get-task-allow entitlement so kcov can attach:

codesign -s - -f --entitlements .kcov-entitlements.plist bin/spec_runner

Contributing

  1. Fork it (https://github.com/MatheusBasso99/openpix-crystal_sdk/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

Repository

openpix-crystal_sdk

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 2
  • about 7 hours ago
  • July 12, 2026
License

MIT License

Links
Synced at

Sun, 12 Jul 2026 20:11:31 GMT

Languages