brevo_api 1.0.0
brevo_api: ^1.0.0 copied to clipboard
Pure Dart client for the Brevo API: transactional email and SMS, contacts, campaigns, CRM and every other v3 resource, with typed models and retries.
brevo_api #
A pure Dart client for the Brevo API (formerly Sendinblue). Runs on Dart servers, CLIs and Flutter apps — there is no Flutter dependency, and no code generation step for you.
Used in production at Fullphysio, where it runs inside our Dart Cloud Functions. Bug reports are welcome — see CONTRIBUTING.md.
Warning
The API key grants full account access. Keep it on a server, a Cloud Function or a CLI — never ship it inside a Flutter app.
final brevo = BrevoClient(apiKey: apiKey);
final sent = await brevo.transactionalEmails.sendTransacEmail(
SendTransacEmailRequest(
templateId: 42,
to: [SendTransacEmailRequestToItem(email: 'ada@example.com')],
params: {'FIRSTNAME': 'Ada'},
),
);
print(sent.messageId);
final contact = await brevo.contacts.getContactInfo(identifier: 'ada@example.com');
print(contact.listIds);
Why this exists #
No Dart package covers the Brevo REST API. This package is a port of the
official Node.js client, @getbrevo/brevo: the runtime — transport, retries,
error mapping, query and path encoding, body serialisation — is a hand port
verified against fixtures captured from the real client, and every model and
resource method is generated from Brevo's own OpenAPI specification by a
generator that lives in this repository. Following a Brevo release is a
command, not a rewrite.
Usage #
Every one of the 34 Brevo resources hangs off the client under the name the
Node.js client uses, so Brevo's own documentation applies:
brevo.contacts, brevo.transactionalEmails, brevo.transactionalSms,
brevo.emailCampaigns, brevo.senders, brevo.webhooks, brevo.companies,
brevo.deals, brevo.ecommerce, brevo.event, brevo.masterAccount, …
Path and query parameters are named arguments; a request body is a typed
positional object. Every method takes an optional options argument for a
per-call timeout, retry budget or extra headers.
Resources hang off the client through an extension, so they are reachable
wherever package:brevo_api/brevo_api.dart is imported. Re-exporting
BrevoClient from your own façade means re-exporting this library with it,
or client.contacts will not resolve at the call site.
await brevo.contacts.updateContact(
UpdateContactRequest(attributes: {'FIRSTNAME': 'Ada', 'PLAN': 'pro'}),
identifier: 'ada@example.com',
);
final campaigns = await brevo.emailCampaigns.getEmailCampaigns(
status: GetEmailCampaignsRequestStatus.sent,
limit: 20,
options: const BrevoRequestOptions(timeout: Duration(seconds: 10)),
);
Pagination #
Brevo paginates with limit / offset and reports a count. Every list
that does has a …Paged twin returning a BrevoOffsetPage:
final page = await brevo.contacts.getContactsPaged(limit: 500, listIds: [7]);
await for (final contact in page.autoPaging()) {
print(contact.email);
}
Files #
The three upload endpoints take a BrevoFile:
await brevo.companies.importCompaniesCreationAndUpdation(
PostCompaniesImportRequest(
file: BrevoFile(bytes: csvBytes, filename: 'companies.csv', contentType: 'text/csv'),
mapping: {'name': 'company_name'},
),
);
Escape hatch #
brevo.requestJson, requestVoid, requestBytes and requestMultipart
reach any endpoint this package does not model yet, with the same
authentication, retries and error mapping.
Models #
Response models decode tolerantly: a field Brevo omits, renames or retypes
reads as null (or an empty list), never throws, and the whole payload stays
available as .raw. Timestamps are kept as the strings Brevo sends, with a
…Date getter that parses them. Enums are open: CampaignStatus.fromWire('new')
returns a value with isKnown == false instead of failing, so a Brevo release
never breaks decoding. Request bodies honour the specification's required
fields at the constructor.
Errors #
Every failure is a BrevoException, a sealed hierarchy:
| Exception | When |
|---|---|
BrevoBadRequestException … BrevoTooManyRequestsException, BrevoInternalServerException |
a 400, 401, 402, 403, 404, 405, 409, 412, 415, 417, 422, 424, 425, 429 or 5xx response, with Brevo's code and errorMessage |
BrevoUnexpectedStatusException |
any other non-2xx status |
BrevoConnectionException, BrevoTimeoutException |
the request never produced a response |
BrevoDecodeException (outside the hierarchy) means Brevo answered
successfully but the payload was not JSON, or not the object or list the
operation returns.
Fidelity to @getbrevo/brevo #
This client reproduces @getbrevo/brevo 6.0.3's observable behaviour,
including details that are easy to get subtly wrong:
- Retries on 408, 429 and 5xx — twice by default — honouring
Retry-After(seconds or a date), thenX-RateLimit-Reset, then a 1 s-doubling backoff capped at 60 s with ±10 % jitter. A connection failure or timeout is not retried, as upstream. Brevo defines no idempotency keys, so a retriedPOSTis exactly as eager as upstream. - Query arrays repeat the key,
ids=1&ids=2; keys and values are encoded likeencodeURIComponent. Anullfield of a request body is omitted, exactly asJSON.stringifydropsundefined. - Error messages follow the
Status code: 400/Body: {…}layout upstream prints, with the body decoded as JSON only under a JSON content type.
Conformance is enforced by golden tests whose fixtures — request URLs and
bodies, error messages, and which statuses are retried and how often — were
captured from the real @getbrevo/brevo rather than written by hand. The
length of each backoff is the exception: the captures pin the retry
decisions, while the 60 s cap and the jitter are covered by unit tests
written against upstream's implementation. A mock-server tier then calls all
291 operations against WireMock loaded with Brevo's own mappings, checking
each one's request against the server's journal.
Scope #
Covered. Every operation of the Brevo v3 specification — 291 operations across 34 resources, with typed parameters, request bodies, response models and open enums generated from it — plus offset pagination helpers and multipart uploads.
Not covered. Inbound webhook payload models (Brevo does not sign
webhooks; parse deliveries with jsonDecode) and the OAuth / custom
authentication hooks of the Node.js client.
Deliberate divergences from @getbrevo/brevo #
- Nothing is read from environment variables; the API key is always explicit.
Futures instead ofHttpResponsePromise; the raw JSON is onerequestJsoncall away.- The exception subtype is chosen by status code for every operation, where upstream types only the statuses the specification lists per operation.
partnerKeyis a constructor option sent as thepartner-keyheader, which the specification defines and the Node.js client lacks.- Undiscriminated
oneOfrequest bodies (adding contacts to a list byemails,idsorextIds, say) are one class with every alternative's fields optional, rather than a union type. - No
X-Fern-*headers; theUser-Agentisbrevo_api/<version> (Dart).