Send email from your own domain, with one key.
A REST API and an SMTP endpoint over your own DKIM-signed sending domains. No third-party ESP in the middle — you are the provider.
Quickstart
Install the SDK, drop in a key, send. Every request is a POST with a JSON body and a Bearer key — nothing else.
Node SDK
npm i @gofarther/mailimport { Gofarther } from '@gofarther/mail';
const gf = new Gofarther(process.env.GOFARTHER_API_KEY!);
const { id } = await gf.emails.send({
from: 'you@yourdomain.com', // a verified domain
to: 'someone@example.com',
subject: 'Hello',
html: '<p>Hi there 👋</p>',
});curl
curl https://lkpfeqrelvziltfwpuxi.supabase.co/functions/v1/mailer \
-H "Authorization: Bearer gf_live_..." \
-H "Content-Type: application/json" \
-d '{"action":"send","from":"you@yourdomain.com","to":"someone@example.com","subject":"Hello","html":"<p>Hi there</p>"}'
# → { "id": "<...>" }API keys
Create keys in the app under Settings → API keys. The full key is shown once at creation — only a hash is stored, so it can't be shown again. Keys carry two properties:
| Property | Values | What it means |
|---|---|---|
scope | send · full | send sends email and reads back your own sending activity. full does all that plus manages domains via the API. Both see only your own account's data. |
mode | live · test | gf_live_… really sends. gf_test_… validates the request and returns a test_… id without sending — wire up your integration before you've even verified a domain. |
Send-only keys can never mint or list keys — key management is dashboard-only. Up to 25 active keys.
Sending
action: "send". Send from a domain you've verified; the mail server DKIM-signs it so SPF, DKIM and DMARC all align.
| Field | Type | Notes |
|---|---|---|
from | string | required. "Name <you@domain>" or "you@domain". Domain must be verified. |
to | string | required. Recipient. |
subject | string | required. |
html | string | HTML body — provide html and/or text. |
text | string | Plain-text body. |
cc, bcc | string · string[] | Comma-string or array of addresses. |
reply_to | string | Reply-To address. |
attachments | object[] | { filename, content (base64), content_type? }. 10 MB total, 20 max. |
idempotency_key | string | Retries with the same key return the original result. |
With an attachment
await gf.emails.send({
from: 'billing@yourdomain.com',
to: 'customer@example.com',
subject: 'Your invoice',
html: '<p>Attached.</p>',
attachments: [{ filename: 'invoice.pdf', content: base64Pdf, content_type: 'application/pdf' }],
});Success → { "id": "<message-id>" }. Test keys → { "id": "test_…", "test": true }.
Activity any key
See what you've sent and how it landed. Any key — send or full — can read back the emails it sent on your account: recipient, subject, and each one's delivery / bounce status. You only ever see your own data.
const emails = await gf.emails.list(); // last 100, newest first
const one = await gf.emails.get(emails[0].id); // full detail + body
console.log(one.status); // sent | delivered | bounced | complained | failedaction: "messages" → { "messages": [...] }. Each row:
| Field | Notes |
|---|---|
id | The email's id — pass to message / gf.emails.get. |
to_email, from_email | Recipient / sender. |
subject | |
status | sent, delivered, bounced, complained, or failed. |
error | Failure reason when failed / bounced. |
provider_msg_id | Mail-server message id — maps to webhook / bounce events. |
created_at, sent_at, delivered_at | Timestamps (nullable until they happen). |
action: "message" (body { "id": "<uuid>" }) → { "message": {...} } — the same fields plus body_html and body_text. 404 if the id isn't one of your emails.
curl https://lkpfeqrelvziltfwpuxi.supabase.co/functions/v1/mailer \
-H "Authorization: Bearer gf_live_..." \
-H "Content-Type: application/json" \
-d '{"action":"messages"}'Domains full scope
Add and verify sending domains programmatically with a full-scope key. Send-only keys get a 403 here.
const gf = new Gofarther('gf_live_FULL_SCOPE_KEY');
const setup = await gf.domains.add('acme.com'); // → DNS records to publish
// publish setup.records at your DNS host, then:
await gf.domains.verify('acme.com');
await gf.domains.list();
await gf.domains.remove('acme.com');Actions: domain_add, domain_records, domain_verify, domain_list, domain_remove.
SMTP
No SDK, or a system that only speaks SMTP (like Supabase Auth's custom SMTP)? Point any client at the endpoint — the password is your API key.
Host smtp.gofarther.dev
Port 465 # implicit TLS
Username gofarther
Password gf_live_...
From you@yourverifieddomain.comThe SMTP endpoint carries subject / from / to / html / text — ideal for sign-in codes and simple transactional mail. Attachments and cc/bcc go over the HTTP API.
Webhooks
Register endpoints in the app under Webhooks. Each event is signed so you can verify it came from us: header sendra-signature: v1=<hmac>, an HMAC-SHA256 of <timestamp>.<body> using your endpoint secret.
| Event | Fires when |
|---|---|
sent | the message was accepted for delivery |
delivered | the receiving server accepted it |
bounced | delivery failed permanently |
complained | the recipient marked it as spam |
domain.created / updated / deleted | a sending domain changed state |
Errors
Failures return { "error": "…" } with an HTTP status. The SDK throws a GofartherError carrying .status and .message.
| Status | Meaning |
|---|---|
| 400 | bad input — missing field, or the From domain isn't verified |
| 401 | bad or revoked key |
| 403 | the key's scope doesn't allow that action |
| 404 | that From domain isn't on your account |
| 409 | the recipient is suppressed (unsubscribed / bounced / complained) |
| 413 | attachments exceed the size limit |
| 429 | daily sending limit reached |