garu 0.7.0
garu: ^0.7.0 copied to clipboard
Dart/Flutter SDK for the Garu payment gateway — PIX, credit card, boleto, recurring charges, webhooks.
Garu — Dart / Flutter SDK #
Brazilian payment gateway. Charges (PIX / boleto / credit card / Pix Automático), customers, products + portal customization, scheduled charges (one-time and recurring), webhook signature verification.
Status:
0.5.0. Tracks the Garu v0.14.0 backend surface, including Pix Automático (BACEN auto-debit recurring Pix). Public API still not frozen until v1.0.0 — minor breakages possible. Validated withdart analyzeand 49 passing unit tests.
Install #
# pubspec.yaml
dependencies:
garu: 0.5.0
Quickstart #
import 'package:garu/garu.dart';
final garu = Garu(apiKey: 'sk_live_...');
final charge = await garu.charges.create(
productId: 'b3f2c1e8-6e4a-4b9f-9d1c-2a1f6c3d4e5f',
paymentMethod: 'pix',
customer: const CustomerInput(
name: 'Maria Silva',
email: 'maria@exemplo.com.br',
document: '12345678909',
phone: '11987654321',
),
);
print('Charge id: ${charge['id']}');
Configuration #
final garu = Garu(
apiKey: 'sk_live_...',
baseUrl: 'https://garu.com.br', // default
maxRetries: 2, // default
timeout: const Duration(seconds: 30) // default
);
Charges #
| Method | Description |
|---|---|
charges.create({...}) |
Create a PIX, boleto, or credit-card charge. |
charges.list({...}) |
List charges with pagination + filters. |
charges.get(id) |
Fetch a single charge by id. |
charges.refund(id, [params]) |
Full or partial refund. |
create and refund automatically attach X-Idempotency-Key (UUIDv4) so retries on transient network failures don't double-process. Pass idempotencyKey to override.
Webhooks #
import 'dart:io';
import 'package:garu/garu.dart';
Future<void> handleWebhook(HttpRequest request) async {
final body = await _readBody(request);
try {
final verified = Garu.webhooks.verify(VerifyWebhookParams(
payload: body, // raw bytes — DO NOT parse-and-reserialize
signature: request.headers.value('x-garu-signature') ?? '',
secret: Platform.environment['GARU_WEBHOOK_SECRET']!,
));
print('Received ${verified.event['event']}');
request.response.statusCode = 200;
} on GaruSignatureVerificationError catch (e) {
request.response.statusCode = 400;
request.response.write(e.message);
}
await request.response.close();
}
Important: always pass the raw request body bytes to
verify. Parsing and re-serializing JSON will break the signature check.
Errors #
Every error extends GaruError. Switch on the typed subclasses for handling:
try {
await garu.charges.refund(4472, const RefundParams(amount: 1000));
} on GaruNotFoundError {
// 404 — charge missing
} on GaruValidationError catch (e) {
// 400 / 422 — body or schema invalid
print(e.body);
} on GaruRateLimitError catch (e) {
// 429 — honor e.retryAfterSec
} on GaruApiError catch (e) {
// anything else with a structured response
print('${e.status} ${e.requestId}: ${e.message}');
} on GaruConnectionError catch (e) {
// DNS / socket / timeout
}
| Error class | HTTP / scenario |
|---|---|
GaruAuthenticationError |
401 |
GaruPermissionError |
403 |
GaruNotFoundError |
404 |
GaruValidationError |
400 / 422 |
GaruRateLimitError |
429 |
GaruServerError |
5xx |
GaruConnectionError |
Network failure |
GaruSignatureVerificationError |
Webhook mismatch |
Retries #
The SDK retries automatically on GaruConnectionError, 408, 429, and 5xx responses with exponential backoff + full jitter (max ~8s). Honors Retry-After. Never retries 4xx validation errors.
Customers #
final customer = await garu.customers.create(const CustomerParams(
name: 'Maria Silva',
email: 'maria@exemplo.com.br',
document: '12345678909',
phone: '11987654321',
personType: 'fisica',
));
await garu.customers.setBillingEmailOverride(customer.id, 'cobranca@exemplo.com.br');
Products + portal customization (B2B2C) #
final products = await garu.products.list(search: 'curso', limit: 10);
// Per-coach branding under one Seller account (Atletia-style B2B2C)
await garu.products.portalConfig.set(57, const SetProductPortalConfigParams(
businessName: 'Coach Maria — Corrida & Trilha',
primaryColor: '#257264',
logoUrl: 'https://cdn.exemplo.com/coaches/maria.png',
));
// Read or fall through to seller-level config
final cfg = await garu.products.portalConfig.get(57);
Scheduled charges #
// Recurring with 7-day trial
final series = await garu.scheduledCharges.create(const CreateScheduledChargeParams(
customerId: 42,
productId: 17,
amount: 49.9,
type: 'recurring',
dueDate: '2026-06-01',
methods: ['card', 'pix'],
recurrence: {'interval': 'monthly'},
trialDays: 7,
));
// Per-attempt billing audit (SPEC §4.2). Each attempt carries the canonical
// failureCode for declines.
final attempts = await garu.scheduledCharges.listAttempts(series.id, cycleNumber: 3);
final declines = attempts.data
.where((a) => a.status == ScheduledChargeAttemptStatus.declined)
.toList();
// GaruFailureCode helpers route permanent vs transient failures
final permanentFailures = declines.where((a) => a.failureCode?.isPermanent == true);
Pix Automático (recurring auto-debit Pix) #
Pix Automático is BACEN's auto-debit recurring Pix: the customer authorizes once (a consent link / QR in their bank app, under "Pix Automático" / "Recorrência Pix"), and subsequent cycles debit silently — no card on file.
Enable it on the product, then add 'pix_automatic' to a recurring scheduled charge that carries a productId:
// The product must have pixAutomatic enabled (Product.pixAutomatic == true).
final series = await garu.scheduledCharges.create(const CreateScheduledChargeParams(
customerId: 123,
productId: 456, // required for pix_automatic
amount: 297.5,
type: 'recurring', // required for pix_automatic
dueDate: '2026-06-15',
methods: [PaymentMethod.pixAutomatic.wireValue], // 'pix_automatic'
recurrence: {'interval': 'monthly'},
maxRecoveryDays: 14,
));
methods containing 'pix_automatic' without type: 'recurring' + a productId trips a debug-mode assertion locally and is rejected by the gateway (400 / 404 / 409).
Webhooks — no new event names #
Pix Automático fires the same events as card-backed subscriptions (subscription.*, transaction.payment.*). Branch on the payment method to tell them apart:
final data = verified.event['data'];
if (data is Map<String, dynamic>) {
final charge = Charge.fromJson(data);
switch (charge.method) {
case PaymentMethod.pixAutomatic:
// auto-debit Pix cycle
break;
case PaymentMethod.card:
// card cycle
break;
default:
break;
}
}
Pix Automático does not retry a refused debit at the network level — Garu fires
subscription.payment_failed, flips the subscription topast_due, and the existing dunning state machine takes over. Cancel via the same routes as any other subscription (scheduledCharges.cancelRecurrence/cancelAtPeriodEnd); the customer can also revoke authorization in their bank app, which Garu surfaces assubscription.cancelled.
Failure codes #
import 'package:garu/garu.dart';
void handleCycleFailed(GaruFailureCode? code) {
if (code?.isPermanent ?? false) {
// ask the customer for a new card
} else {
// Garu's retry cron will keep trying — relax
}
}
Every transaction.payment.failed, scheduled_charge.cycle_failed, and listAttempts row carries failureCode (canonical enum, gateway-independent), failureReason (PT-BR human-readable), and gatewayFailureCode (raw ABECS for forensics). Full table at docs.garu.com.br/api-reference/webhooks/codigos-de-falha.
What's NOT in v0.5.0 #
These remain TODO before v1.0.0:
- Strongly-typed event-timeline models for
scheduledCharges.getdetail bundle (currently returns rawMap<String, dynamic>) - Multi-value status filter on
scheduledCharges.list(currently passes first value only) - Card tokenization helpers (today: pass raw card to
charges.create; the backend tokenizes via Celcoin) - A Flutter example app
Contributing #
This is the early-alpha scaffold. PRs welcome at https://github.com/Garu-Pagamentos/garu-flutter.
License #
MIT — see LICENSE.