Developers · SDK

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+.
1

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-js
2

Initialize 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().
3

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.

4

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);
5

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 }]
6

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);
7

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" },
});
8

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);
9

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);
10

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.ink

With those set you can construct the client with no arguments: new WorkonwardSend().

Next steps