Installation
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:
| Method | Description |
|---|
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():
| Field | Type | Default | Description |
|---|
url | string | required | Target URL to dispatch to |
method | GET | POST | PUT | PATCH | DELETE | "POST" | HTTP method |
body | Record<string, unknown> | — | Request body, serialised as JSON |
headers | Record<string, string> | — | Additional headers forwarded to the target |
approve | boolean | false | When true, the action pauses for human approval before dispatch |
retries | number | 3 | Max delivery retries on failure |
dedupe | string | — | Deduplication key — actions with the same key within 24 h are de-duped |
callback | string | — | URL notified when the action finishes |
idempotencyKey | string | — | Sent as the Idempotency-Key header |
sync | boolean | false | Poll until the action completes and return the final action object |
syncTimeout | number | 30000 | Polling 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():
| Field | Type | Default | Description |
|---|
status | ActionStatus | — | Filter by status |
limit | number | 20 | Maximum results to return |
offset | number | 0 | Pagination offset |
The Action object
Every method returns an Action object:
| Field | Type | Description |
|---|
id | string | Action ID (act_...) |
status | ActionStatus | Current status |
url | string | Target URL |
method | string | HTTP method |
body | object | null | Request body |
headers | object | null | Forwarded headers |
approve | boolean | Whether approval was required |
approved_at | string | null | ISO timestamp when approved |
attempts | number | Number of dispatch attempts made |
retries | number | Max retries configured |
retries_remaining | number | Retries left |
dedupe | string | null | Deduplication key |
deduplicated | boolean | true if this action was a duplicate of an existing one |
response_code | number | null | HTTP status from the target |
response_body | unknown | Response body from the target |
duration_ms | number | null | Time taken for the last dispatch attempt |
last_error | string | null | Error message from the last failed attempt |
created_at | string | ISO creation timestamp |
finished_at | string | null | ISO completion timestamp |
callback | string | null | Callback 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:
| Property | Type | Description |
|---|
message | string | Human-readable error description |
source | "target" | "dispatch" | "target" = non-2xx from your endpoint; "dispatch" = network/API/timeout error |
statusCode | number | undefined | HTTP status returned by the SafeFetch API |
errorCode | string | undefined | Machine-readable code from the SafeFetch API |
targetResponse | { code, body } | undefined | Present when source === "target" |
action | Action | undefined | Action 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
| Variable | Description |
|---|
SAFEFETCH_API_KEY | Your API key (required when using safeFetch()) |
SAFEFETCH_BASE_URL | Override the API base URL (optional) |