Skip to content
Esc
  • OverviewGuidesWhat exists today, and where to start.
  • QuickstartGuidesKey, domain, first send — in that order.
  • AuthenticationGuidesBearer keys, the mandatory User-Agent, and what each refusal means.
  • ErrorsGuidesThe whole vocabulary, with the status each name carries.
  • IdempotencyGuidesRetry a send without sending it twice.
  • PaginationGuidesCursors are item IDs, not page numbers.
  • Rate limitsGuidesTen a second per team, and the headers that tell you where you are.
  • EventsGuidesEvery event a webhook can carry, with one real payload each.
  • DomainsGuidesThe records, where they go at each registrar, and what the page does while you wait.
  • TrackingGuidesOpens and clicks: one record, two toggles, and what an open really means.
  • ReceivingGuidesInbound mail, and the Inbox: a webhook fires, you read it, you answer it.
  • InboxGuidesChannels, personal mailboxes and seats: who sees what, and where a reply goes.
  • Node SDKGuidesThe rasket package: typed from the API's own document, retries only what is safe.
  • Python SDKGuidesThe rasket package on PyPI: the Node client's methods, in snake_case, over httpx.
  • MCP serverGuidesConnect Claude, ChatGPT or any MCP client: your scopes, no key.
  • AI assistGuidesSubject lines, drafts and diagnosis — in the dashboard and over the API, off until you allow it.
  • AgentsGuidesLet an AI agent set Rasket up: the skill, the rules file, MCP, and the recipe they share.
  • OAuthGuidesLet another app act for a team: register, authorize with PKCE, exchange, refresh.
  • Single sign-onGuidesOIDC login for your team, a domain proved by DNS, enforcement and break-glass.
  • IntegrationsGuidesVercel, Netlify and Cloudflare, plus Zapier and n8n for workflows without code.
  • SMTPGuidesSend from anything that speaks SMTP: settings, setup guides, limits and replies.
  • ZapierGuidesSend email, add contacts and react to email events from a Zap, with no code.
  • n8nGuidesThe Rasket node and trigger for n8n workflows: install, connect, every operation.
  • VercelGuidesAdd Rasket on Vercel: a Sending key in each project as RASKET_API_KEY, no copying.
  • EmailsAPI referenceSend, batch, retrieve, list, reschedule, cancel, attachments.
  • DomainsAPI referenceAdd a domain, publish its records, verify it.
  • API keysAPI referenceCreate, list, rename and revoke credentials.
  • WebhooksAPI referencePayloads, signature verification, retries and replay.
  • SuppressionsAPI referenceAddresses we will not send to, and why.
  • LogsAPI referenceEvery request made with this team's credentials.
  • MetricsAPI referenceDelivery, bounce, complaint and engagement counts.
  • TemplatesAPI referenceVersioned email content with typed variables, addressed by ID or alias.
  • ContactsAPI referenceYour audience: contacts, their typed properties, segments and topic choices.
  • SegmentsAPI referenceAudiences defined by a filter, by hand, or both.
  • TopicsAPI referenceWhat contacts subscribe to, and the preference page's list.
  • CampaignsAPI referenceCampaigns, at /broadcasts: one message to a segment, from draft to results.
  • ImportsAPI referenceCSV uploads: column mapping, conflicts and counts.
  • AutomationsAPI referenceWorkflows that run per contact: the graph, its versions, and every run.
  • Custom eventsAPI referenceThe names your product fires, and what starts a workflow.
  • ReceivingAPI referenceMail sent to you: the message, its attachments, its raw source.
  • OAuthAPI referenceClient registration, the token endpoint, and the grants a team has given.
  • TeamAPI referenceThe team a credential belongs to: its plan, sender identity, AI flag and members.
  • BillingAPI referencePlan, usage, invoices and add-ons, and the hosted pages where a customer pays.
  • AI helpersAPI referenceSubject lines, a first draft, and why an email did what it did.

GuidesNode SDK

Node SDK

The rasket package: a typed client for Node 20+ and TypeScript, with no dependencies. Its request and response types are generated from the same document the API is validated against, so they cannot drift from it.

Coming soon: the rasket package is not published yet. It is not on npm, so there is nothing to install today. Until it is, call the REST API with fetch, as below, or connect an agent over MCP. The rest of this page describes the client as it will ship, so you know what to expect.

Today: the REST API

Every endpoint in the API reference has a JavaScript tab that uses plain fetch, which Node 20 has. This is the first send:

curl -X POST "https://api.rasket.com/emails" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{
  "from": "Acme <orders@send.acme.example>",
  "to": ["ronald.williams@example.com"],
  "subject": "Your order has shipped",
  "html": "<p>Order 1042 left the warehouse this morning.</p>"
}'

The first send

import { Rasket } from "rasket";

const rasket = new Rasket({
  apiKey: process.env.RASKET_API_KEY,
  userAgent: "acme-billing/1.0",
});

const { body: email } = await rasket.emails.send(
  {
    from: "Acme <billing@acme.example>",
    to: ["ronald.williams@example.com"],
    subject: "Your receipt",
    html: "<p>Thanks.</p>",
  },
  { idempotencyKey: "receipt-1042" },
);

console.log(email.id);

The client sends a User-Agent of its own on every request, because a request without one is refused; whatever you pass as userAgent is appended to it, never substituted. Keys start with rk_.

What every call returns

const result = await rasket.emails.get(id);

result.body;               // the parsed body, typed per route
result.rateLimit;          // { limit: 10, remaining, resetSeconds }
result.requestId;          // the x-request-id to quote
result.idempotentReplayed; // true on a replay of an earlier keyed request

One shape for every method. For a list route, body is the { object, has_more, data } envelope described under pagination.

Received mail

const { body: page } = await rasket.emails.receiving.list({ limit: 20 });
const { body: message } = await rasket.emails.receiving.get(page.data[0].id);

const { body: files } = await rasket.emails.receiving.attachments.list(message.id);
for (const file of files.data) {
  // A part we would not store is listed without a link; check before you follow it.
  if (file.download_url === undefined) continue;
  const bytes = await fetch(file.download_url);
}

await rasket.emails.receiving.remove(message.id);

emails.receiving.* is its own resource, not a filter over the mail you sent: a received message is never an emails row, and neither read accepts the other's IDs. The two download routes are deliberately not on this client — they answer bytes rather than JSON, so follow download_url with fetch, as above.

Connected apps

const { body: grants } = await rasket.oauth.grants.list({ limit: 20 });

for (const grant of grants.data) {
  if (grant.revoked_at === null && grant.client.name === "Old CRM sync") {
    await rasket.oauth.grants.revoke(grant.id);
  }
}

oauth.grants lists the apps this team has connected through OAuth — revoked ones included — and revokes one. It needs a full_access key. The flow routes themselves (register, authorize, token, revoke) are not on this client: they are an OAuth library's job, not an API call your server makes.

An OAuth access token works anywhere apiKey does — pass it as apiKey — within the scopes it was granted. No scope reaches oauth.grants, team.members.list or a billing write, so those need a full_access key.

Team, billing and AI

const { body: team } = await rasket.team.get();
await rasket.team.update({ ai_assist_enabled: true });

const { body: checkout } = await rasket.billing.checkout({
  plan_code: "pro",
  return_url: "https://app.example.com/settings/billing",
});
// Send the customer's browser to checkout.url; the card is entered on that hosted page.

const { body: ideas } = await rasket.ai.subjectLines({ subject: "Our November update" });

team is singular: a key belongs to one team, so there is no ID to pass. billing.checkout and billing.portal answer a hosted page's URL, never card data. The six billing writes and team.members.list need a full_access key; see the billing reference for who may call what, and the AI reference for the order an AI call is refused in. emails.diagnose is the third AI helper.

Everything the dashboard does

const { body: records } = await rasket.domains.records(domainId);
const { body: timeline } = await rasket.emails.events.list(emailId);
const { body: preview } = await rasket.templates.preview("welcome", { variables: { name: "Ronald" } });

const { body: upload } = await rasket.contacts.imports.create({
  file: new Blob([await readFile("contacts.csv")]),
  options: { column_map: { email: "Email" }, on_conflict: "upsert" },
});

const { replayed, stopped } = await rasket.webhooks.events.replayMany(webhookId, eventIds);

Every function of the dashboard has a method: domains.records, domains.claims, domains.regenerateDkim and domains.autoconfigure; emails.events.list; templates.preview and segments.preview; broadcasts.checklist; automations.versions.list; and webhooks.parked.list and .deliver. contacts.imports.create is the one multipart call: pass the CSV as a Blob.

webhooks.events.replayMany is not an API call. It replays one event at a time and stops at the first refusal, returning it as stopped instead of throwing, so the events already replayed are not lost.

Idempotency

Pass idempotencyKey to emails.send or batch.send — the two routes that take one. The client sends it verbatim and never generates a key of its own: a key it invented would make a retry look idempotent when you never asked for that.

  • 1–256 characters. A key outside that range is refused before any request is sent, as the same 400 invalid_idempotency_key the API would answer, with error.source === "client".
  • Scoped to your team, remembered for 24 hours.
  • A replay of a finished request returns the original response with idempotentReplayed: true; a different payload under the same key is 409 invalid_idempotent_request.

Errors

import { isRasketApiError, isRasketConnectionError } from "rasket";

try {
  await rasket.emails.send(message);
} catch (error) {
  if (isRasketApiError(error)) {
    switch (error.name) {
      case "validation_error":
        return reject(error.errors); // [{ path, message }]
      case "rate_limit_exceeded":
        return later(error.rateLimit?.retryAfterSeconds);
      default:
        throw error; // error.statusCode, error.message, error.requestId
    }
  }
  if (isRasketConnectionError(error)) {
    // error.reason: "network" | "timeout" -- no answer arrived
  }
}

error.name is the API's stable vocabulary, not a class name — the same names the errors page lists. The one value outside it is unknown_error: the response carried no error body of ours, and statusCode is whatever came back.

Branch on error.kind or the predicates, not instanceof. The package ships both module formats, and a dependency graph that loads both holds two copies of every class.

Retries

What is retried
RequestRetried on a 429 or 5xx?
GETAlways
POST /emails and POST /emails/batch with an Idempotency-KeyYes: the key is what makes a repeat a replay
POST /emails and POST /emails/batch without oneNever: at-most-once matters more than a saved round trip
Every other writeNever

retry-after is honoured when present, and a value above maxDelayMs is not waited for — the error is thrown at once with the header on it. Otherwise the wait is exponential with full jitter. A quota 429 is never retried, and neither is a 501; a 409 concurrent_idempotent_requests on a keyed send is, because it means your earlier attempt is landing.

const rasket = new Rasket({
  apiKey,
  timeoutMs: 30_000,
  retry: { attempts: 3, minDelayMs: 500, maxDelayMs: 8_000 },
});

attempts: 1 turns retrying off. timeoutMs is per attempt, and every method takes { signal } as its last argument to cancel the call.

Verifying a webhook

rasket.webhooks.verify is the published verifier, bundled, so there is exactly one. It returns the parsed event or throws WebhookVerificationError with a reason.

export async function POST(request: Request): Promise<Response> {
  const rawBody = await request.text(); // text(), never json()
  try {
    const event = rasket.webhooks.verify(rawBody, request.headers, process.env.RASKET_WEBHOOK_SECRET);
    await handle(event);
    return new Response(null, { status: 200 });
  } catch {
    return new Response("invalid signature", { status: 400 });
  }
}

The signature covers the raw request body. A framework that parses JSON before your handler has already destroyed what was signed — read the body as text first.

Worth knowing

  • Timestamps are ISO 8601 UTC with milliseconds, everywhere.
  • scheduled_at takes an ISO 8601 instant or a phrase such as "in 2 hours", read as UTC. Anything the grammar does not cover is a 422.
  • webhooks.get returns the signing secret masked. It is in the create and rotateSecret responses and nowhere else.