nafrucom / email
Official Nafru Email PHP SDK: send transactional and marketing email from PHP with a Resend-compatible email API, plus contacts, broadcasts, templates, custom events, webhook verification and Connect with Nafru (OAuth).
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.5 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Official client libraries for the Nafru Email API. Send transactional and marketing email, manage contacts, domains, templates and broadcasts, and verify webhooks from TypeScript / JavaScript, Python, Go, PHP, Ruby, Java, .NET (C#), Rust and Swift.
The Nafru Email API is Resend-compatible: the same request shapes, field names (snake_case) and error format. If you
already use Resend, you can switch by changing the base URL and API key; these SDKs add Nafru-only features on top
(custom events, brand, template translations, recurring broadcasts, "Connect with Nafru" OAuth).
Contents
- Installation
- Quick start
- Features
- Concepts: authentication, errors, retries & idempotency, pagination, webhooks, Connect with Nafru, local development
- Versioning and deprecation
- FAQ
- Security · Support
Installation
| Language | Package | Install | Docs |
|---|---|---|---|
| TypeScript / JavaScript | @nafrucom/email |
npm install @nafrucom/email |
npm |
| Python ≥ 3.9 | nafru-email |
pip install nafru-email |
python/ |
| Go ≥ 1.21 | github.com/nafrucom/nafru-email-sdks/go |
go get github.com/nafrucom/nafru-email-sdks/go |
go/ |
| PHP ≥ 8.1 | nafrucom/email |
composer require nafrucom/email |
php/ |
| Ruby ≥ 3.1 | nafru-email |
gem install nafru-email |
ruby/ |
| Java ≥ 17 | com.nafru:nafru-email |
implementation("com.nafru:nafru-email:0.1.1") |
java/ |
| .NET 8+ (C#, F#) | Nafru.Email |
dotnet add package Nafru.Email |
dotnet/ |
| Rust | nafru-email |
cargo add nafru-email |
rust/ |
| Swift (iOS 15+, macOS 12+) | NafruEmail |
.package(url: "https://github.com/nafrucom/nafru-email-sdks", from: "0.1.1") |
swift/ |
Every SDK has the same feature set and is released with the same version number on the same day.
Quick start
Create an API key in the Nafru Email dashboard and keep it on your server (never ship it to a browser or mobile app).
Each example sends one email; idempotencyKey makes a retried request return the same email instead of sending twice.
TypeScript / JavaScript (Node ≥ 18, Bun, Deno, Cloudflare Workers)
import { NafruEmail } from '@nafrucom/email' const nafru = new NafruEmail('nafru_123') const { data, error } = await nafru.emails.send( { from: 'Acme <hello@example.com>', to: 'user@example.org', subject: 'Welcome', html: '<p>Hello!</p>' }, { idempotencyKey: 'signup-1042' } ) if (error) console.error(error.statusCode, error.name, error.message) else console.log(data.id)
Python
import os from nafru_email import NafruEmail, NafruError nafru = NafruEmail("nafru_123") # or NafruEmail(), which reads NAFRU_API_KEY try: email = nafru.emails.send( {"from": "Acme <hello@example.com>", "to": "user@example.org", "subject": "Welcome", "html": "<p>Hello!</p>"}, idempotency_key="signup-1042", ) print(email["id"]) except NafruError as error: print(error.status_code, error.name, error.message)
Go
client, err := nafruemail.NewClient("nafru_123") // "" reads NAFRU_API_KEY if err != nil { log.Fatal(err) } sent, err := client.Emails.Send(ctx, &nafruemail.SendEmailInput{ From: "Acme <hello@example.com>", To: []string{"user@example.org"}, Subject: "Welcome", HTML: "<p>Hello!</p>", }, nafruemail.SendEmailOptions{IdempotencyKey: "signup-1042"}) if err != nil { log.Fatal(err) // *nafruemail.Error carries StatusCode, Name, Message } fmt.Println(sent.ID)
PHP
use Nafru\Email\NafruEmail; use Nafru\Email\NafruException; $nafru = new NafruEmail('nafru_123'); // or new NafruEmail(), which reads NAFRU_API_KEY try { $email = $nafru->emails->send( ['from' => 'Acme <hello@example.com>', 'to' => 'user@example.org', 'subject' => 'Welcome', 'html' => '<p>Hello!</p>'], idempotencyKey: 'signup-1042', ); echo $email['id']; } catch (NafruException $e) { error_log("{$e->statusCode} {$e->name} {$e->getMessage()}"); }
Ruby
require "nafru_email" nafru = NafruEmail.new("nafru_123") # or NafruEmail.new, which reads NAFRU_API_KEY email = nafru.emails.send( { from: "Acme <hello@example.com>", to: "user@example.org", subject: "Welcome", html: "<p>Hello!</p>" }, idempotency_key: "signup-1042" ) puts email[:id]
Java
NafruEmail nafru = new NafruEmail("nafru_123"); // or new NafruEmail(), which reads NAFRU_API_KEY var sent = nafru.emails().send( new SendEmailInput() .from("Acme <hello@example.com>") .to("user@example.org") .subject("Welcome") .html("<p>Hello!</p>"), "signup-1042"); // Idempotency-Key System.out.println(sent.id());
C# / .NET
using Nafru.Email; var nafru = new NafruEmail("nafru_123"); // or new NafruEmail(), which reads NAFRU_API_KEY var sent = await nafru.Emails.SendAsync( new SendEmailInput { From = "Acme <hello@example.com>", To = ["user@example.org"], Subject = "Welcome", Html = "<p>Hello!</p>" }, idempotencyKey: "signup-1042"); Console.WriteLine(sent.Id);
Rust
use nafru_email::{NafruEmail, SendEmailInput, SendEmailOptions}; let nafru = NafruEmail::new("nafru_123")?; // "" reads NAFRU_API_KEY let sent = nafru .emails .send( &SendEmailInput { from: Some("Acme <hello@example.com>".into()), to: vec!["user@example.org".into()], subject: Some("Welcome".into()), html: Some("<p>Hello!</p>".into()), ..Default::default() }, SendEmailOptions::idempotency_key("signup-1042"), ) .await?; println!("{}", sent.id);
Swift
import NafruEmail let nafru = try NafruEmail("nafru_123") // or NafruEmail(), which reads NAFRU_API_KEY let sent = try await nafru.emails.send( SendEmailInput(from: "Acme <hello@example.com>", to: ["user@example.org"], subject: "Welcome", html: "<p>Hello!</p>"), idempotencyKey: "signup-1042" ) print(sent.id)
Features
| Area | What you can do |
|---|---|
| Emails | Send single or batch (up to 100) emails, HTML / text / template bodies, attachments, tags, cc / bcc / reply_to, scheduled sending (scheduled_at, ISO 8601 or natural language like in 1 hour), reschedule, cancel, get, list |
| Domains | Add sending domains, read DNS records (SPF, DKIM, DMARC), trigger verification, update tracking settings |
| Contacts | Create, update, delete and list contacts (by id or email address), custom contact properties, segment membership, topic subscriptions (opt-in / opt-out) |
| Segments & topics | Static and dynamic audiences, subscription topics for preference centers |
| Broadcasts | Draft, schedule, send and cancel campaigns; recurring broadcasts (weekly, monthly, every N days) with per-send data from a URL or webhook |
| Templates | Create, update, publish and duplicate templates; per-locale translations; workspace brand (logo, colors, fonts, footer) |
| Events | Send custom events (single or batch) to trigger automations |
| Webhooks | Manage endpoints and verify signatures (Svix / Standard Webhooks compatible) |
| API keys | Create, list and revoke keys with scoped access |
| Connect with Nafru | OAuth 2.0 + PKCE flow so your SaaS can send on behalf of your customers' Nafru accounts, with automatic token refresh |
Concepts
Authentication
API-key clients send Authorization: Bearer <key>. The default endpoint is https://email.nafru.com/api; pass a base URL
only for self-hosted or local development (http://localhost:48787/api).
Errors
Every SDK exposes the same error fields, idiomatically (a { data, error } result in TypeScript; an exception or error
value elsewhere):
| Field | Meaning |
|---|---|
statusCode |
HTTP status; empty for network failures and timeouts |
name |
Machine-readable name: validation_error, not_found, rate_limit_exceeded, unauthorized, conflict, timeout_error, application_error, … |
message |
Human-readable message |
errors |
Per-field validation errors (field, message) |
retryAfter |
Seconds to wait, from the Retry-After header on 429 |
Retries, timeouts and idempotency
- Requests time out after 30 seconds (configurable).
- Up to 2 automatic retries (configurable,
0disables) with exponential backoff capped at 8 seconds;Retry-Afteris honored. 429 Too Many Requestsis always retried.5xxand network errors are retried only for safe requests:GET,DELETE, or any request carrying anIdempotency-Key. APOSTwithout a key is never retried, so an email is never sent twice by the SDK.
Pagination
List endpoints are cursor-based (limit, after, before) and return { object: "list", data, has_more }. Every SDK ships
a paginate helper that walks all pages for you and stops on the first error.
Webhooks
Nafru signs webhook deliveries the same way as Svix / Standard Webhooks (svix-id, svix-timestamp, svix-signature
headers, HMAC-SHA256 with a whsec_… secret, 5-minute tolerance, constant-time comparison). Every SDK has a
verifyWebhook function; always pass the raw request body. All SDKs are tested against the same shared vector,
webhook-test-vector.json.
Connect with Nafru (OAuth)
Building a product that sends email for your own customers? Instead of asking them to paste an API key, use the
"Connect with Nafru" flow: authorizeUrl (PKCE S256 + state) → redirect → exchange the code for a long-lived
connection → call connection(id, { refreshToken, onTokenRefresh }). Access tokens refresh automatically, concurrent
calls share a single refresh, and the rotated refresh token is handed to your onTokenRefresh callback to store.
Local development: no email is sent
When NODE_ENV=development, emails.send and emails.batch do not send. Each email is written to .nafru/emails/ as
.txt (headers + text) and .html, and the file paths are printed. Change the folder with the devOutbox option or
turn it off with devOutbox: false.
Versioning and deprecation
- All SDKs follow Semantic Versioning and share one version number. A release tag
vX.Y.Zpublishes every language at once. - When a version is deprecated it is listed in
deprecations.json. The API then answers that SDK version withDeprecation,SunsetandLinkheaders, and every SDK prints a one-time warning; requests keep working until the sunset date.
FAQ
Is Nafru Email compatible with Resend? Yes. The API mirrors Resend's endpoints and payloads; the official Resend SDKs
work by pointing them at https://email.nafru.com/api. These SDKs add Nafru-specific endpoints and OAuth.
Can I use these SDKs in the browser or a mobile app? Not with an API key; any key can send email. Call the API from your server. Mobile and desktop apps should go through your backend or use Connect with Nafru tokens kept server-side.
Where is the source? This repository is published automatically from the Nafru Email monorepo on every release. Please open issues here.
Security
Report vulnerabilities privately through GitHub or security@nafru.com. See SECURITY.md for our disclosure policy, response times and how the SDKs protect your keys: HTTPS only, redirects refused, certificate verification always on, secrets redacted from logs, 10 MB response cap, constant-time webhook verification.
Support
| You want to… | Go to |
|---|---|
| Ask a question | GitHub Discussions |
| Report a bug or request a feature | GitHub Issues |
| Get help with your account, billing or deliverability | destek@nafru.com |
| Report a vulnerability | SECURITY.md — privately, never in a public issue |
Every SDK README has a Troubleshooting section. See SUPPORT.md and CONTRIBUTING.md.
License
MIT © Nafru