Skip to main content

Installation

npm install safefetch

Quick setup

The easiest way to get started is with the safeFetch() convenience function. It reads your API key from the SAFEFETCH_API_KEY environment variable automatically.
export SAFEFETCH_API_KEY=sf_live_...
import { safeFetch } from "safefetch";

const action = await safeFetch({
  url: "https://api.example.com/send-email",
  method: "POST",
  body: { to: "user@example.com", subject: "Hello" },
});

console.log(action.id);     // act_...
console.log(action.status); // "pending" | "completed" | ...

safeFetch() — convenience function

safeFetch(opts) sends an action using a shared instance configured from environment variables. It accepts all SendOptions and returns a Promise<Action>. The function also has shorthand methods for the other operations:
MethodDescription
safeFetch(opts)Send a new action
safeFetch.get(id)Fetch an action by ID
safeFetch.list(opts?)List actions
safeFetch.approve(id)Approve an awaiting-approval action
safeFetch.cancel(id)Cancel a pending or awaiting-approval action
safeFetch.retry(id)Retry a failed or cancelled action
// Get a single action
const action = await safeFetch.get("act_abc123");

// List recent actions
const { data, total } = await safeFetch.list({ status: "pending", limit: 10 });

// Approve an action waiting for human review
await safeFetch.approve("act_abc123");

// Cancel an action
await safeFetch.cancel("act_abc123");

// Retry a failed action (creates a new action)
const retried = await safeFetch.retry("act_abc123");

new SafeFetch() — class API

Use the class directly when you need multiple instances, custom base URLs, or explicit key management.
import { SafeFetch } from "safefetch";

const sf = new SafeFetch({
  apiKey: process.env.SAFEFETCH_API_KEY!,
  // baseUrl: "https://api.safefetch.dev", // optional override
});

const action = await sf.send({
  url: "https://api.example.com/send-email",
  method: "POST",
  body: { to: "user@example.com" },
});
The class exposes the same set of methods: send, get, list, approve, cancel, retry.

SendOptions

All options for safeFetch() / sf.send():
FieldTypeDefaultDescription
urlstringrequiredTarget URL to dispatch to
methodGET | POST | PUT | PATCH | DELETE"POST"HTTP method
bodyRecord<string, unknown>Request body, serialised as JSON
headersRecord<string, string>Additional headers forwarded to the target
approvebooleanfalseWhen true, the action pauses for human approval before dispatch
retriesnumber3Max delivery retries on failure
dedupestringDeduplication key — actions with the same key within 24 h are de-duped
callbackstringURL notified when the action finishes
idempotencyKeystringSent as the Idempotency-Key header
syncbooleanfalsePoll until the action completes and return the final action object
syncTimeoutnumber30000Polling timeout in ms when sync is true
sync: true cannot be combined with approve: true — actions waiting for human approval cannot be awaited synchronously.

Sync mode

Pass sync: true to wait for the action to finish before returning:
const action = await safeFetch({
  url: "https://api.example.com/process",
  method: "POST",
  body: { input: "data" },
  sync: true,
  syncTimeout: 60_000, // 60 s
});

// action.status is guaranteed to be "completed" here
console.log(action.response_code); // e.g. 200

Human approval

Pass approve: true to require a human to approve the action before it is dispatched:
const action = await safeFetch({
  url: "https://api.example.com/delete-account",
  method: "DELETE",
  body: { userId: "u_123" },
  approve: true,
});

// action.status === "awaiting_approval"
// Approve later:
await safeFetch.approve(action.id);
See Human Approval for the full workflow.

ListOptions

Options for safeFetch.list() / sf.list():
FieldTypeDefaultDescription
statusActionStatusFilter by status
limitnumber20Maximum results to return
offsetnumber0Pagination offset

The Action object

Every method returns an Action object:
FieldTypeDescription
idstringAction ID (act_...)
statusActionStatusCurrent status
urlstringTarget URL
methodstringHTTP method
bodyobject | nullRequest body
headersobject | nullForwarded headers
approvebooleanWhether approval was required
approved_atstring | nullISO timestamp when approved
attemptsnumberNumber of dispatch attempts made
retriesnumberMax retries configured
retries_remainingnumberRetries left
dedupestring | nullDeduplication key
deduplicatedbooleantrue if this action was a duplicate of an existing one
response_codenumber | nullHTTP status from the target
response_bodyunknownResponse body from the target
duration_msnumber | nullTime taken for the last dispatch attempt
last_errorstring | nullError message from the last failed attempt
created_atstringISO creation timestamp
finished_atstring | nullISO completion timestamp
callbackstring | nullCallback URL
ActionStatus values: pending, active, awaiting_approval, completed, failed, cancelled.

Error handling

The SDK throws SafeFetchError for all failures:
import { safeFetch, SafeFetchError } from "safefetch";

try {
  await safeFetch({ url: "https://api.example.com/action", method: "POST" });
} catch (err) {
  if (err instanceof SafeFetchError) {
    console.error(err.message);
    console.error(err.source);       // "target" | "dispatch"
    console.error(err.statusCode);   // HTTP status from SafeFetch API
    console.error(err.errorCode);    // machine-readable code, e.g. "RATE_LIMITED"

    if (err.source === "target") {
      // The target endpoint returned a non-2xx response
      console.error(err.targetResponse?.code);  // target HTTP status
      console.error(err.targetResponse?.body);  // target response body
    }

    // The action object at the time of failure, if available
    console.error(err.action?.id);
  }
}
SafeFetchError properties:
PropertyTypeDescription
messagestringHuman-readable error description
source"target" | "dispatch""target" = non-2xx from your endpoint; "dispatch" = network/API/timeout error
statusCodenumber | undefinedHTTP status returned by the SafeFetch API
errorCodestring | undefinedMachine-readable code from the SafeFetch API
targetResponse{ code, body } | undefinedPresent when source === "target"
actionAction | undefinedAction object at the time of the error

TypeScript types

All types are exported from the safefetch package:
import type {
  Action,
  ActionStatus,
  SendOptions,
  ListOptions,
  ListResponse,
  SafeFetchOptions,
} from "safefetch";

Environment variables

VariableDescription
SAFEFETCH_API_KEYYour API key (required when using safeFetch())
SAFEFETCH_BASE_URLOverride the API base URL (optional)