Skip to main content

The problem

An AI agent that calls Stripe directly can issue a refund the moment it decides to — no review, no record, no recourse. One hallucination or misread order ID and the money is gone.
// ❌ Direct Stripe call — executes immediately, no audit trail
await stripe.refunds.create({ charge: "ch_3Qx9R2eZpVMlhbBV0Abc123", amount: 4999 });

The SafeFetch solution

Wrap the Stripe call in safeFetch() with approve: true. The refund is held as awaiting_approval until a human clicks approve. Nothing hits Stripe until that happens.

Full example

import { safeFetch } from "safefetch";

const action = await safeFetch({
  url: "https://api.stripe.com/v1/refunds",
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: {
    charge: "ch_3Qx9R2eZpVMlhbBV0Abc123",
    amount: 4999, // $49.99 in cents
    reason: "requested_by_customer",
  },
  approve: true,
  // Optional: email a reviewer a magic-link approve/reject button
  notify: { email: "billing-team@yourcompany.com" },
  // Optional: notify your backend when the refund completes
  callback: "https://yourapp.com/webhooks/safefetch",
});

console.log(action.id);     // act_...
console.log(action.status); // "awaiting_approval"
The action is created immediately and returns with status: "awaiting_approval". Stripe is not contacted yet.

What happens next

1

Reviewer receives email

If you passed notify.email, SafeFetch sends an email with one-click Approve and Reject buttons. No login required — the links are signed magic links.
2

Reviewer approves

Clicking Approve moves the action to pending and dispatches the request to Stripe. The receipt page shows the full action details, including the Stripe response.
3

Your callback fires

If you passed a callback URL, SafeFetch sends a POST to it when the action reaches a terminal status (completed or failed). Check action.status and action.response_code to confirm the refund went through.

Checking status programmatically

Poll or check status at any point using the action ID:
const action = await safeFetch.get("act_Xk9mP2nQ4rT1vW8sY");

if (action.status === "awaiting_approval") {
  console.log("Still waiting for reviewer sign-off.");
} else if (action.status === "completed") {
  console.log("Refund issued. Stripe responded:", action.response_code);
  // action.response_body contains the full Stripe refund object
} else if (action.status === "failed") {
  console.log("Refund failed:", action.last_error);
}

Approving via API

If you have your own approval UI instead of the magic-link email, approve programmatically:
await safeFetch.approve("act_Xk9mP2nQ4rT1vW8sY");
Or cancel (reject) it:
await safeFetch.cancel("act_Xk9mP2nQ4rT1vW8sY");

Handling the callback

SafeFetch POSTs to your callback URL when the action finishes. Verify the signature and act on the result:
import type { Action } from "safefetch";

// In your webhook handler (e.g., Express, Hono):
app.post("/webhooks/safefetch", async (req, res) => {
  const action: Action = req.body;

  if (action.status === "completed" && action.response_code === 200) {
    // Stripe accepted the refund — update your records
    await db.orders.update({ id: action.body?.charge }, { refunded: true });
  } else if (action.status === "failed") {
    await alertTeam(`Refund failed for ${action.body?.charge}: ${action.last_error}`);
  }

  res.sendStatus(200);
});

Receipt page

Every action has a receipt page in the dashboard showing the full audit trail: who approved it, when, what Stripe returned, and how long delivery took. Share the URL with your finance team as documentation.
https://api.safefetch.dev/dashboard/actions/act_Xk9mP2nQ4rT1vW8sY
approve: true cannot be combined with sync: true. Synchronous mode waits for immediate completion — that’s incompatible with an asynchronous human decision.