> ## Documentation Index
> Fetch the complete documentation index at: https://docs.safefetch.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Admin Action

> Gate sensitive admin operations — user deactivation, permission changes, account deletions — behind a human approval step with a full audit trail.

## The problem

An AI agent with admin API access can make destructive, hard-to-reverse changes the moment it decides to. Deactivating the wrong user account, escalating permissions incorrectly, or deleting data silently can cause real harm before anyone notices.

```ts theme={null}
// ❌ Direct admin call — executes immediately, no review, no record
await fetch(`https://api.yourapp.com/admin/users/${userId}/deactivate`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.ADMIN_API_KEY}` },
});
```

There's no approval gate, no notification, and no audit trail showing who authorized the action.

## The SafeFetch solution

Wrap the admin call in `safeFetch()` with `approve: true` and a `callback` URL. The action is held as `awaiting_approval` until a human approves it. Your backend is notified when the action reaches a terminal status.

## Full example

```ts theme={null}
import { safeFetch } from "safefetch";

const action = await safeFetch({
  url: `https://api.yourapp.com/admin/users/${userId}/deactivate`,
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ADMIN_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: {
    reason: "Flagged for policy violation",
    initiated_by: "ai-agent",
  },
  approve: true,
  // Email a signed magic-link approve/reject button to the reviewer
  notify: { email: "security-team@yourcompany.com" },
  // POST to your backend when the action completes or is rejected
  callback: "https://yourapp.com/webhooks/safefetch",
  // Prevent duplicate deactivation requests for the same user
  dedupe: `deactivate-user:${userId}`,
});

console.log(action.id);     // act_...
console.log(action.status); // "awaiting_approval"
```

The action is stored immediately and returns with `status: "awaiting_approval"`. Your admin API is not called until a reviewer approves.

## What happens next

<Steps>
  <Step title="Reviewer receives email">
    If you passed `notify.email`, SafeFetch sends an email with one-click **Approve** and **Reject** buttons. Links are signed — no login required.
  </Step>

  <Step title="Reviewer approves">
    Clicking **Approve** moves the action to `pending` and dispatches the request to your admin API. The receipt page shows the full details: who approved, when, and what your API returned.
  </Step>

  <Step title="Your callback fires">
    SafeFetch `POST`s to your `callback` URL when the action reaches a terminal status (`completed`, `failed`, or `cancelled`). Check `action.status` and `action.response_code` to confirm the outcome and update your records.
  </Step>
</Steps>

## Handling the callback

Verify the result in your webhook handler and take follow-up action:

```ts theme={null}
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) {
    // Admin action succeeded — log it and notify stakeholders
    await db.auditLog.insert({
      action_id: action.id,
      type: "user_deactivated",
      user_id: action.body?.userId,
      completed_at: action.completed_at,
    });
    await notifyStakeholders(`User ${action.body?.userId} has been deactivated.`);
  } else if (action.status === "cancelled") {
    // Reviewer rejected — record the decision
    await db.auditLog.insert({
      action_id: action.id,
      type: "deactivation_rejected",
      user_id: action.body?.userId,
    });
  } else if (action.status === "failed") {
    await alertTeam(`Admin action failed: ${action.last_error}`);
  }

  res.sendStatus(200);
});
```

## Checking status programmatically

Poll or inspect the action at any point using its ID:

```ts theme={null}
const action = await safeFetch.get("act_Xk9mP2nQ4rT1vW8sY");

if (action.status === "awaiting_approval") {
  console.log("Pending reviewer sign-off.");
} else if (action.status === "completed") {
  console.log("Action completed. API responded:", action.response_code);
} else if (action.status === "cancelled") {
  console.log("Action was rejected by reviewer.");
} else if (action.status === "failed") {
  console.log("Action failed:", action.last_error);
}
```

## Approving or rejecting via API

If you have your own internal approval UI, skip the magic-link email and handle approval programmatically:

```ts theme={null}
// Approve
await safeFetch.approve("act_Xk9mP2nQ4rT1vW8sY");

// Reject (cancel)
await safeFetch.cancel("act_Xk9mP2nQ4rT1vW8sY");
```

## Receipt page as audit trail

Every action has a receipt page in the dashboard. It shows the full audit trail: who approved it, when, what your API returned, and how long delivery took. Share the URL with your security or compliance team.

```
https://api.safefetch.dev/dashboard/actions/act_Xk9mP2nQ4rT1vW8sY
```

For sensitive operations, store the action ID in your own database alongside the event. You can always retrieve the full record via `safeFetch.get(action.id)` or the API.

<Note>
  `approve: true` cannot be combined with `sync: true`. Synchronous mode waits for immediate completion — that's incompatible with an asynchronous human decision.
</Note>
