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::TimeoutErrorinstead of silently eating a fiber. - Keep-alive connection reuse — each
OpenPix::Clientowns one persistentHTTP::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
LibCryptoEVP bindings (the key is parsed once and memoized).
Installation
-
Add the dependency to your
shard.yml:dependencies: open-pix-crystal-sdk: github: MatheusBasso99/openpix-crystal_sdk -
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
- Fork it (https://github.com/MatheusBasso99/openpix-crystal_sdk/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- Matheus Basso - creator and maintainer
openpix-crystal_sdk
- 0
- 0
- 0
- 0
- 2
- about 7 hours ago
- July 12, 2026
MIT License
Sun, 12 Jul 2026 20:11:31 GMT