Send email with the
WorkOnward Reach SDK
A step-by-step guide to the official JavaScript / TypeScript and Python SDKs — from install to your first send, plus attachments, scheduling, idempotency, contacts, campaigns, domains, and webhook verification. Every snippet is copy-paste ready.
JavaScript / TypeScript
@workonward/reach-js
import { WorkonwardSend } · async · returns { data, error }
Python
workonward-reach
from workonward_reach import WorkonwardSend · sync
Both SDKs talk to https://send.gitdate.ink by default — you only need an API key. Every endpoint lives under https://send.gitdate.ink/api/v1/…, which the SDK appends for you. The examples below are the real, published packages; pick your language once and every snippet on the page switches with it.
Prerequisites
- An API key. Create one under Developer settings → API keys. It starts with
us_and is shown once. - A verified domain. Add and verify one under Domains. You send from an address on it.
- A runtime. Node 18+ (native
fetch) or Python 3.10+.
Install the SDK
Add the package with your preferred package manager:
npm install @workonward/reach-js
# or: pnpm add @workonward/reach-js
# or: yarn add @workonward/reach-js
# or: bun add @workonward/reach-jsInitialize the client
Pass your API key. The client defaults to the WorkOnward Reach instance at https://send.gitdate.ink.
import { WorkonwardSend } from "@workonward/reach-js";
// Defaults to https://send.gitdate.ink
const reach = new WorkonwardSend("us_your_api_key");
// Point at a different instance (optional):
// const reach = new WorkonwardSend("us_your_api_key", "https://your-instance.example.com");
// …or set the WORKONWARDSEND_API_KEY env var and call new WorkonwardSend().Send your first email
The from address must be on a verified domain. Provide html, text, or both.
const { data, error } = await reach.emails.send({
from: "hello@yourdomain.com", // a verified domain
to: "customer@example.com",
subject: "Welcome to WorkOnward Reach 👋",
html: "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
text: "Welcome! Thanks for signing up.",
});
if (error) {
console.error(`${error.code}: ${error.message}`);
} else {
console.log("Sent! emailId =", data.emailId);
}Recipients: to, cc, and bcc each accept a single address or a list.
Responses & error handling
Every method returns { data, error }. On failure error is a flat { code, message } object and data is null — it never throws.
const { data, error } = await reach.emails.send({ /* … */ });
if (error) {
// error.code e.g. "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "RATE_LIMITED"
console.error(error.code, error.message);
return;
}
console.log(data.emailId);Every sending option
Attachments (base64, max 10)
import { readFileSync } from "node:fs";
const content = readFileSync("invoice.pdf").toString("base64");
await reach.emails.send({
from: "billing@yourdomain.com",
to: "customer@example.com",
subject: "Your invoice",
html: "<p>Invoice attached.</p>",
attachments: [{ filename: "invoice.pdf", content }],
});Scheduled send (ISO-8601 with offset), then cancel or reschedule
await reach.emails.send({
from: "news@yourdomain.com",
to: "customer@example.com",
subject: "Scheduled digest",
html: "<p>Sent later.</p>",
scheduledAt: "2026-08-01T09:00:00Z",
});
await reach.emails.update("email_123", { scheduledAt: "2026-08-02T09:00:00Z" });
await reach.emails.cancel("email_123");Templates + variables (subject becomes optional)
await reach.emails.send({
from: "hello@yourdomain.com",
to: "customer@example.com",
templateId: "tmpl_welcome",
variables: { firstName: "Ada", plan: "Pro" },
});Idempotent send — safe to retry
Same key + same body returns the original result; same key + different body returns HTTP 409. Keys expire after 24h.
await reach.emails.send(
{
from: "hello@yourdomain.com",
to: "customer@example.com",
subject: "Confirm your signup",
html: "<p>Click to confirm.</p>",
},
{ idempotencyKey: "signup-user-8931" },
);Batch send — up to 100 emails in one call
const { data } = await reach.emails.batch(
[
{ from: "hello@yourdomain.com", to: "a@example.com", subject: "Hi A", html: "<p>Hello A</p>" },
{ from: "hello@yourdomain.com", to: "b@example.com", subject: "Hi B", html: "<p>Hello B</p>" },
],
{ idempotencyKey: "welcome-batch-1" },
);
// data.data -> [{ emailId }, { emailId }]Read & list emails
// One email, including its delivery events
const { data: email } = await reach.emails.get("email_123");
const latest = email.emailEvents?.at(-1)?.status; // SENT, DELIVERED, BOUNCED, OPENED, CLICKED…
// List, newest first (paginated + filterable)
const { data: page } = await reach.emails.list({
page: 1,
limit: 50,
// startDate: "2026-01-01T00:00:00Z",
// domainId: 1,
});
console.log(page.count, page.data.length);Contacts & contact books
// List contacts in a contact book (paginated, filterable)
const { data: contacts } = await reach.contacts.list("cb_12345", {
page: 1,
limit: 100,
});
// Create a contact
await reach.contacts.create("cb_12345", {
email: "new@example.com",
firstName: "Ada",
properties: { plan: "pro" },
});Campaigns
const campaign = await reach.campaigns.create({
name: "Welcome Series",
from: "hello@yourdomain.com",
subject: "Welcome to WorkOnward Reach!",
contactBookId: "cb_12345",
html: "<h1>Welcome!</h1><p>Thanks for joining.</p>",
sendNow: false,
});
await reach.campaigns.schedule(campaign.data.id, {
scheduledAt: "2026-08-01T09:00:00Z",
batchSize: 1000,
});
await reach.campaigns.pause(campaign.data.id);
await reach.campaigns.resume(campaign.data.id);Domains
const { data: domains } = await reach.domains.list();
const { data: domain } = await reach.domains.create({ name: "yourdomain.com", region: "us-east-1" });
// After adding the DNS records, trigger verification
await reach.domains.verify(domain.id);Verify webhooks
Create a webhook in the dashboard, then verify each incoming request with your signing secret (starts with whsec_). Always pass the raw request body — not parsed JSON.
import { WorkonwardSend } from "@workonward/reach-js";
const reach = new WorkonwardSend("us_your_api_key");
const webhooks = reach.webhooks(process.env.WORKONWARDSEND_WEBHOOK_SECRET!);
// Next.js App Router
export async function POST(request: Request) {
try {
const rawBody = await request.text(); // raw, not await request.json()
const event = webhooks.constructEvent(rawBody, { headers: request.headers });
if (event.type === "email.delivered") {
// event.data is strongly typed
}
return new Response("ok");
} catch (err) {
return new Response((err as Error).message, { status: 400 });
}
}Signatures are accepted within 5 minutes of the timestamp by default. WorkOnward Reach signs the body as v1= + HMAC-SHA256(timestamp.rawBody) and sends it in the X-UseSend-Signature header alongside X-UseSend-Timestamp, X-UseSend-Event, and X-UseSend-Call.
Configuration & environment variables
The client resolves settings in this order: constructor arguments → environment variables → defaults.
WORKONWARDSEND_API_KEY=us_your_api_key
# optional — overrides the default https://send.gitdate.ink
WORKONWARDSEND_BASE_URL=https://send.gitdate.inkWith those set you can construct the client with no arguments: new WorkonwardSend().