> Call POST /v1/check before a tool that writes. DENY means do not invoke the tool. For clean Markdown of any page, append .md to the page URL. For the complete index, fetch https://docs.withwrit.com/llms.txt.

# Decide a write

POST https://api.withwrit.com/v1/check
Content-Type: application/json

API key required. **This is the core of Writ — call it before the
agent performs any consequential write.** The check matches the
request against the tenant's verb policy and any live grants on five
fields: sponsor, agent, verb, target, purpose. All five must match a
grant for it to apply.

Decision flow, in order:

0. The request names an `environment` outside `dev`, `staging`,
   `prod` → **DENY**, fail closed. The request is never silently
   evaluated against a different scope; the denial is receipted
   with no policy attribution.
1. The principal (sponsorId + agentId) was revoked → **DENY**
   ("principal revoked by sponsor"), even under an allow policy.
2. The verb's effective policy mode is `allow` → **ALLOW**.
3. The verb's effective policy mode is `deny` → **DENY**.
4. A live, unspent grant matches all five fields → **ALLOW** and the
   grant is spent (single-use).
5. The mode is `step_up` with no live grant → **STEP_UP**: a human
   sponsor approves via `POST /v1/grants`, then the agent
   re-checks.
6. Otherwise (`require_grant` with no live grant) → **DENY**.

The effective policy resolves per-verb by precedence: the calling
API key's **per-principal override** first, then the named
environment's **per-environment override** (when `environment` is
given and known), then the **tenant default**. Overrides are
sparse — a verb an override doesn't name inherits downward. Every
receipt records the `policyId` and `policyVersion` of the policy
that supplied the verb's mode.

Custom-verb aliases resolve before policy lookup: if the verb names
a deprecated custom verb (a rename alias) or differs only in case
from the registry, the check runs against the resolved verb and the
receipt carries `resolvedVerb` — the verb the policy was actually
evaluated for. `verb` always echoes exactly what the caller sent.

**Every check writes a receipt** — including DENYs. The refusal
itself is auditable. Decisions are returned as HTTP 200 even when
the decision is DENY or STEP_UP; only auth, quota, and malformed
requests change the status code.

On **ALLOW**, the response is the receipt plus `authToken`: a
90-second HMAC-signed token (`writ_at_...`) bound to this exact
sponsor/agent/verb/target/purpose, and `tokenExpiresIn`. The action
adapter enforces the token at the point of action; verify it against what
the agent actually did with `POST /v1/tokens/verify` to catch
purpose drift. The token is returned to the caller only — it is
never stored in the receipt.

The free tier is 10,000 receipts per tenant per calendar month. Past
that, checks return **402** until the quota resets or a payment
method is added.


Reference: https://docs.withwrit.com/api-reference/checks/decide-a-write

## Authentication

- `Authorization` header (bearer token, required) — Bearer authentication of the form `Bearer <token>`, where token is your auth token.

## Servers

- `https://api.withwrit.com` (Writ gate, default)
- `https://j72ckh66ukck2kcbq3oiwxaalm0olxwb.lambda-url.us-east-1.on.aws` (Direct Lambda Function URL (fallback))

## Request

### Body (application/json)

This endpoint expects an any.

- `any`

## Response

### 200

ALLOW, DENY, or STEP_UP, plus a receipt. On ALLOW the response also carries authToken, a 90-second HMAC-signed token bound to this exact sponsor/agent/verb/target/purpose, and tokenExpiresIn. STEP_UP means the tenant policy requires a human sponsor to approve; the sponsor mints a grant via POST /v1/grants and the agent re-checks.

## Errors

### 400 Bad Request Error

The body is not a JSON object, or required fields are missing. The response lists the missing fields.

### 401 Unauthorized Error

API key required — missing, malformed, or unknown key.

### 402 Payment Required Error

Payment required — no receipt is written. Either the tenant's free tier of 10,000 receipts for the month is exhausted, or billing is past due. Add a payment method to continue over the free allowance. The body carries portal_endpoint (/v1/billing/portal), which accepts the API key or the dashboard sign-in session.

- `any`

## Examples

### allow

**Response**

```json
{
  "agentId": "support-agent-07",
  "authToken": "writ_at_eyJ...",
  "createdAt": 1759000000,
  "decision": "ALLOW",
  "grantId": "grnt_1a2b...",
  "policyId": "pol_ten_3fa8b91c2d44_tenant",
  "policyVersion": 3,
  "purpose": "Refund order #88412, close ticket #2210",
  "reason": "grant matched and spent",
  "receiptId": "rcpt_9d2f...",
  "sponsorId": "s_42",
  "target": "order #88412",
  "tenantId": "ten_3fa8b91c2d44",
  "tokenExpiresIn": 90,
  "verb": "refund"
}
```

**SDK Code**

```python allow
import requests

url = "https://api.withwrit.com/v1/check"

headers = {"Authorization": "Bearer <token>"}

response = requests.post(url, headers=headers)

print(response.json())
```

```javascript allow
const url = 'https://api.withwrit.com/v1/check';
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go allow
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.withwrit.com/v1/check"

	req, _ := http.NewRequest("POST", url, nil)

	req.Header.Add("Authorization", "Bearer <token>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby allow
require 'uri'
require 'net/http'

url = URI("https://api.withwrit.com/v1/check")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'

response = http.request(request)
puts response.read_body
```

```java allow
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.withwrit.com/v1/check")
  .header("Authorization", "Bearer <token>")
  .asString();
```

```php allow
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.withwrit.com/v1/check', [
  'headers' => [
    'Authorization' => 'Bearer <token>',
  ],
]);

echo $response->getBody();
```

```csharp allow
using RestSharp;

var client = new RestClient("https://api.withwrit.com/v1/check");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
IRestResponse response = client.Execute(request);
```

```swift allow
import Foundation

let headers = ["Authorization": "Bearer <token>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.withwrit.com/v1/check")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### deny

**Response**

```json
{
  "agentId": "support-agent-07",
  "createdAt": 1759000000,
  "decision": "DENY",
  "grantId": null,
  "policyId": "pol_ten_3fa8b91c2d44_tenant",
  "policyVersion": 3,
  "purpose": "Refund order #88412, close ticket #2210",
  "reason": "no live grant for this sponsor, agent, verb, target, and purpose",
  "receiptId": "rcpt_8c1e...",
  "sponsorId": "s_42",
  "target": "order #88412",
  "tenantId": "ten_3fa8b91c2d44",
  "verb": "refund"
}
```

**SDK Code**

```python deny
import requests

url = "https://api.withwrit.com/v1/check"

headers = {"Authorization": "Bearer <token>"}

response = requests.post(url, headers=headers)

print(response.json())
```

```javascript deny
const url = 'https://api.withwrit.com/v1/check';
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go deny
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.withwrit.com/v1/check"

	req, _ := http.NewRequest("POST", url, nil)

	req.Header.Add("Authorization", "Bearer <token>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby deny
require 'uri'
require 'net/http'

url = URI("https://api.withwrit.com/v1/check")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'

response = http.request(request)
puts response.read_body
```

```java deny
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.withwrit.com/v1/check")
  .header("Authorization", "Bearer <token>")
  .asString();
```

```php deny
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.withwrit.com/v1/check', [
  'headers' => [
    'Authorization' => 'Bearer <token>',
  ],
]);

echo $response->getBody();
```

```csharp deny
using RestSharp;

var client = new RestClient("https://api.withwrit.com/v1/check");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
IRestResponse response = client.Execute(request);
```

```swift deny
import Foundation

let headers = ["Authorization": "Bearer <token>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.withwrit.com/v1/check")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### stepUp

**Response**

```json
{
  "agentId": "support-agent-07",
  "createdAt": 1759000000,
  "decision": "STEP_UP",
  "grantId": null,
  "policyId": "pol_ten_3fa8b91c2d44_tenant",
  "policyVersion": 3,
  "purpose": "Refund order #88412, close ticket #2210",
  "reason": "policy requires human approval and no live grant exists",
  "receiptId": "rcpt_7b0d...",
  "sponsorId": "s_42",
  "target": "order #88412",
  "tenantId": "ten_3fa8b91c2d44",
  "verb": "refund"
}
```

**SDK Code**

```python stepUp
import requests

url = "https://api.withwrit.com/v1/check"

headers = {"Authorization": "Bearer <token>"}

response = requests.post(url, headers=headers)

print(response.json())
```

```javascript stepUp
const url = 'https://api.withwrit.com/v1/check';
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go stepUp
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.withwrit.com/v1/check"

	req, _ := http.NewRequest("POST", url, nil)

	req.Header.Add("Authorization", "Bearer <token>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby stepUp
require 'uri'
require 'net/http'

url = URI("https://api.withwrit.com/v1/check")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'

response = http.request(request)
puts response.read_body
```

```java stepUp
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.withwrit.com/v1/check")
  .header("Authorization", "Bearer <token>")
  .asString();
```

```php stepUp
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.withwrit.com/v1/check', [
  'headers' => [
    'Authorization' => 'Bearer <token>',
  ],
]);

echo $response->getBody();
```

```csharp stepUp
using RestSharp;

var client = new RestClient("https://api.withwrit.com/v1/check");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
IRestResponse response = client.Execute(request);
```

```swift stepUp
import Foundation

let headers = ["Authorization": "Bearer <token>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.withwrit.com/v1/check")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Infra failure with fail-open semantics (WRIT_CHECK_FAILURE_MODE=failopen, the default)

**Response**

```json
{
  "agentId": "support-agent-07",
  "createdAt": 1759000000,
  "decision": "ALLOW",
  "failMode": "open",
  "grantId": null,
  "infrastructureFailure": true,
  "policyId": null,
  "policyVersion": null,
  "purpose": "Refund order #88412, close ticket #2210",
  "reason": "infrastructure failure — decision unavailable (fail-open; policy was NOT evaluated; verify out of band)",
  "receiptId": "rcpt_4a9f...",
  "sponsorId": "s_42",
  "target": "order #88412",
  "tenantId": "ten_3fa8b91c2d44",
  "verb": "refund"
}
```

**SDK Code**

```python Infra failure with fail-open semantics (WRIT_CHECK_FAILURE_MODE=failopen, the default)
import requests

url = "https://api.withwrit.com/v1/check"

headers = {"Authorization": "Bearer <token>"}

response = requests.post(url, headers=headers)

print(response.json())
```

```javascript Infra failure with fail-open semantics (WRIT_CHECK_FAILURE_MODE=failopen, the default)
const url = 'https://api.withwrit.com/v1/check';
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Infra failure with fail-open semantics (WRIT_CHECK_FAILURE_MODE=failopen, the default)
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.withwrit.com/v1/check"

	req, _ := http.NewRequest("POST", url, nil)

	req.Header.Add("Authorization", "Bearer <token>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Infra failure with fail-open semantics (WRIT_CHECK_FAILURE_MODE=failopen, the default)
require 'uri'
require 'net/http'

url = URI("https://api.withwrit.com/v1/check")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'

response = http.request(request)
puts response.read_body
```

```java Infra failure with fail-open semantics (WRIT_CHECK_FAILURE_MODE=failopen, the default)
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.withwrit.com/v1/check")
  .header("Authorization", "Bearer <token>")
  .asString();
```

```php Infra failure with fail-open semantics (WRIT_CHECK_FAILURE_MODE=failopen, the default)
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.withwrit.com/v1/check', [
  'headers' => [
    'Authorization' => 'Bearer <token>',
  ],
]);

echo $response->getBody();
```

```csharp Infra failure with fail-open semantics (WRIT_CHECK_FAILURE_MODE=failopen, the default)
using RestSharp;

var client = new RestClient("https://api.withwrit.com/v1/check");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
IRestResponse response = client.Execute(request);
```

```swift Infra failure with fail-open semantics (WRIT_CHECK_FAILURE_MODE=failopen, the default)
import Foundation

let headers = ["Authorization": "Bearer <token>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.withwrit.com/v1/check")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Checks_decideAWrite_example

**Response**

```json
{
  "agentId": "support-agent-07",
  "authToken": "writ_at_eyJ...",
  "createdAt": 1759000000,
  "decision": "ALLOW",
  "grantId": "grnt_1a2b...",
  "policyId": "pol_ten_3fa8b91c2d44_tenant",
  "policyVersion": 3,
  "purpose": "Refund order #88412, close ticket #2210",
  "reason": "grant matched and spent",
  "receiptId": "rcpt_9d2f...",
  "sponsorId": "s_42",
  "target": "order #88412",
  "tenantId": "ten_3fa8b91c2d44",
  "tokenExpiresIn": 90,
  "verb": "refund"
}
```

**SDK Code**

```python Checks_decideAWrite_example
import requests

url = "https://api.withwrit.com/v1/check"

headers = {"Authorization": "Bearer <token>"}

response = requests.post(url, headers=headers)

print(response.json())
```

```javascript Checks_decideAWrite_example
const url = 'https://api.withwrit.com/v1/check';
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Checks_decideAWrite_example
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.withwrit.com/v1/check"

	req, _ := http.NewRequest("POST", url, nil)

	req.Header.Add("Authorization", "Bearer <token>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Checks_decideAWrite_example
require 'uri'
require 'net/http'

url = URI("https://api.withwrit.com/v1/check")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'

response = http.request(request)
puts response.read_body
```

```java Checks_decideAWrite_example
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.withwrit.com/v1/check")
  .header("Authorization", "Bearer <token>")
  .asString();
```

```php Checks_decideAWrite_example
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.withwrit.com/v1/check', [
  'headers' => [
    'Authorization' => 'Bearer <token>',
  ],
]);

echo $response->getBody();
```

```csharp Checks_decideAWrite_example
using RestSharp;

var client = new RestClient("https://api.withwrit.com/v1/check");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
IRestResponse response = client.Execute(request);
```

```swift Checks_decideAWrite_example
import Foundation

let headers = ["Authorization": "Bearer <token>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.withwrit.com/v1/check")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```