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

# 2. Your first protected action

> Acquire a verified Permit and execute the action under it. Fail-closed by construction — no manual decision branching.

<Note>
  These examples use `ehr.write`, which a new organization does not have until you provision that action and a policy for it. Without them every call here returns `deny`. On a new account, start with the [Developer / API quickstart](/quickstart/developer-api): it uses the `database.execute_sql` demo action the console seeds for you.
</Note>

The canonical AtlaSent surface is **two forms of a single
execution-boundary contract**. Both mint a Permit, verify it
end-to-end, and fail closed if anything other than `allow` happens.
The Permit is the artifact you act on; the call site picks the form
that fits.

| Form | Use it when |
| - | - |
| **`protect()`** | You want the verified [`Permit`](/concepts/permit) as a value — to pass it across a boundary, persist it alongside your record, or interleave it with non-trivial control flow. |
| **`with_permit()`** / **`withPermit()`** | The action body is a single lexical scope. The call site reads as "execute this body under a permit." |

Both resolve to the same wire contract (`/v1-evaluate` +
`/v1-verify-permit`) and produce the same audit-chain entry.
You can mix them freely in a codebase.

<Warning>
  Do **not** call `/v1-evaluate` directly and branch on `decision ===
      "allow"`. That pattern is bypassable, audit-weak, and forces the
  caller to re-implement verification. Use `protect()` /
  `with_permit()` instead. The raw `evaluate()` and `verify()` SDK
  methods exist only for callers that need to inspect the four-value
  decision (`allow` / `deny` / `hold` / `escalate`) or hold a permit
  token across a boundary.
</Warning>

## `protect()` — acquire a verified Permit

`protect()` is the primitive. It returns a verified
[`Permit`](/concepts/permit) on success and raises on anything else.
There is no `permitted=False` return path. If `protect()` returns,
the action is authorized end-to-end.

<CodeGroup>
  ```python Python theme={null}
  import os
  from atlasent import protect, AtlaSentDeniedError, AtlaSentError

  # Picks up ATLASENT_API_KEY from the environment.
  try:
      permit = protect(
          agent="agent-001",
          action="ehr.write",
          context={"environment": "production", "patient_id": "patient-42"},
      )
  except AtlaSentDeniedError as exc:
      # Policy denied, hold/escalate, or permit failed verification.
      halt(exc.reason)
  except AtlaSentError:
      # Transport, timeout, auth, rate-limit, or server error.
      halt("fail-closed")

  # Authorized end-to-end. Execute the action and record permit.permit_id
  # alongside your own record so the audit chain stays two-way navigable.
  record = db.patients.update("patient-42", payload)
  print(f"executed under permit={permit.permit_id} audit={permit.audit_hash}")
  ```

  ```typescript TypeScript theme={null}
  import atlasent, { AtlaSentDeniedError, AtlaSentError } from "@atlasent/sdk";

  // Picks up ATLASENT_API_KEY from the environment.
  try {
    const permit = await atlasent.protect({
      agent: "agent-001",
      action: "ehr.write",
      context: { environment: "production", patient_id: "patient-42" },
    });

    // Authorized end-to-end. Execute the action and record permit.permitId
    // alongside your own record so the audit chain stays two-way navigable.
    const record = await db.patients.update("patient-42", payload);
    console.log(`executed under permit=${permit.permitId} audit=${permit.auditHash}`);
  } catch (err) {
    if (err instanceof AtlaSentDeniedError) halt(err.reason);  // policy denied / verify-fail
    else if (err instanceof AtlaSentError)  halt("fail-closed"); // transport / auth / 5xx
    else throw err;
  }
  ```

  ```bash curl theme={null}
  # The raw wire form. The SDK above issues both calls for you.
  # Step 1 — evaluate. On allow, the response carries a signed permit_token
  # and the execution_hash_expected value to re-present at verify time.
  RESP=$(curl -sX POST "$ATLASENT_API_URL/v1-evaluate" \
    -H "Authorization: Bearer $ATLASENT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "action_type": "ehr.write",
      "actor_id": "agent-001",
      "context": {
        "environment": "production",
        "target": { "id": "patient-42", "type": "ehr_record" }
      },
      "state_snapshot": { "source": "quickstart", "complete": true }
    }')
  PERMIT_TOKEN=$(echo "$RESP" | jq -r '.permit_token')
  PAYLOAD_HASH=$(echo "$RESP" | jq -r '.execution_hash_expected')

  # Step 2 — verify before executing. Both calls land in the audit chain.
  curl -sX POST "$ATLASENT_API_URL/v1-verify-permit" \
    -H "Authorization: Bearer $ATLASENT_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{
      \"permit_token\": \"$PERMIT_TOKEN\",
      \"action_type\": \"ehr.write\",
      \"actor_id\": \"agent-001\",
      \"payload_hash\": \"$PAYLOAD_HASH\"
    }" | jq .valid
  ```
</CodeGroup>

The returned `Permit` is a first-class artifact:

```
permit_id    the canonical decision/permit identifier (the wire
             permit_token is a signed pt.v4.… token)
permit_hash  Ed25519-signed verification hash
audit_hash   hash-chained audit entry for this decision
reason       operator-readable policy explanation
timestamp    ISO 8601 verification time
```

Persist `permit_id` next to your action record. That is the link that
makes the audit chain two-way navigable and that satisfies the
electronic-signature record requirement under 21 CFR Part 11 §11.50
([details](/concepts/permit)).

## `with_permit()` / `withPermit()` — the lexically-scoped form

When the action body is a single scope, bind it to the permit's
lifetime directly. The body runs **only** on a verified permit; on
deny, hold, escalate, verification failure, or transport error it is
never invoked.

<CodeGroup>
  ```python Python theme={null}
  from atlasent import with_permit

  result = with_permit(
      agent="agent-001",
      action="ehr.write",
      context={"environment": "production", "patient_id": "patient-42"},
      fn=lambda permit: db.patients.update(
          "patient-42", payload, permit_id=permit.permit_id,
      ),
  )
  ```

  ```typescript TypeScript theme={null}
  import atlasent from "@atlasent/sdk";

  const result = await atlasent.withPermit(
    {
      agent: "agent-001",
      action: "ehr.write",
      context: { environment: "production", patient_id: "patient-42" },
    },
    async (permit) => {
      return db.patients.update("patient-42", payload, { permitId: permit.permitId });
    },
  );
  ```
</CodeGroup>

Same fail-closed contract as `protect()`. Same audit-chain entry. The
difference is purely lexical: the call site reads as one statement
that says "execute this body under a permit," and the verified Permit
is visible inside the body if you need it.

## What just happened

In a single call, the SDK:

1. Posted the request to **`/v1-evaluate`**. The policy engine
   returned `allow` and issued a signed Permit token (`pt.v4.…`) with a
   default 1-hour TTL.
2. Posted the permit to **`/v1-verify-permit`**. The signature
   was checked, the TTL was checked, and the verification was bound
   into the audit chain.
3. Returned the verified Permit to you (or, with `with_permit()` /
   `withPermit()`, invoked your body with it).

Both API calls landed in the audit chain. The Permit you hold is
Ed25519-signed, single-use, and now
inspectable via `/v1/permits/{id}` or the [Console](https://console.atlasent.io).

## Other decisions

`protect()` and `with_permit()` raise `AtlaSentDeniedError` on any
non-allow outcome. If you need to inspect the four-value decision
(`allow` / `deny` / `hold` / `escalate`) at the call site — for
example, to route `hold` to an approval queue — drop down to the raw
`evaluate()` primitive:

```python theme={null}
from atlasent import evaluate

result = evaluate(action_type="ehr.write", actor_id="agent-001", context={...})
match result.decision:
    case "allow":    ...  # verify the permit, then execute
    case "deny":     ...  # surface deny_reason
    case "hold":     ...  # wait for the approval webhook
    case "escalate": ...  # routed to a human reviewer
```

`evaluate()` does **not** verify the permit for you. Callers using
the raw primitive must call `verify_permit_by_id()` / `verifyPermitById()`
before executing — see the next page.

<Warning>
  **Fail closed.** If the API returns 5xx or times out, your agent
  must treat it as deny. `protect()` and `with_permit()` already do
  this for you (they raise `AtlaSentError`). If you drop down to raw
  `evaluate()`, never fall back to "allow on error" in production —
  it defeats the entire authorization layer.
</Warning>

## Next

`protect()` already verified the permit for you. The next page covers
the explicit verify call — needed only when you hold a permit token
that crossed a boundary (e.g. between two services) and the receiving
side wants to re-confirm before acting.

[Verify a permit explicitly →](/quickstart/verify-permit)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.