# Direct moderation

Asynchronous ingestion is right for scoring users over time. To decide about
a piece of content **before** anyone sees it (a message, a listing, a
review), send it with `"wait": true`: the API records the events, scores the
object inline and returns the verdict.

cURL:

```bash
curl -X POST "$API/v1/objects/user-42/events?api_key=$ANALYZE_API_KEY" \
  -H "Authorization: Bearer $(sign)" -H 'Content-Type: application/json' \
  -d '{"wait":true,"signals":["is_scammer"],
       "events":[{"type":"message","content":"pay me by wire and I double it"}]}'
```

Node.js:

```ts
const verdict = await analyze.events.ingest("user-42", {
  wait: true,
  signals: ["is_scammer"],
  events: [{ type: "message", content: "pay me by wire and I double it" }],
});
if ("answers" in verdict && (verdict.answers[0]?.noul ?? 0) > 0.9) {
  return reject();
}
```

Python:

```python
verdict = analyze.events.ingest(
    "user-42",
    wait=True,
    signals=["is_scammer"],
    events=[{"type": "message", "content": "pay me by wire and I double it"}],
)
if verdict["answers"] and verdict["answers"][0].get("noul", 0) > 0.9:
    reject()
```

Go:

```go
res, err := client.Events.Ingest(ctx, "user-42", &analyze.IngestRequest{
	Wait:    analyze.Bool(true),
	Signals: []string{"is_scammer"},
	Events:  []analyze.EventInput{{Type: analyze.EventTypeMessage, Content: analyze.String("pay me by wire and I double it")}},
})
if v := res.ModerationVerdict; v != nil && len(v.Answers) > 0 && *v.Answers[0].Noul > 0.9 {
	reject()
}
```

PHP:

```php
$verdict = $analyze->events->ingest('user-42', [
    'wait' => true,
    'signals' => ['is_scammer'],
    'events' => [['type' => 'message', 'content' => 'pay me by wire and I double it']],
]);
if (($verdict['answers'][0]['noul'] ?? 0) > 0.9) {
    reject();
}
```

The response is `200 OK`:

```json
{
  "object_id": "user-42",
  "accepted": 1,
  "analyzed": true,
  "history_included": true,
  "model": "model-1",
  "latency_ms": 840,
  "answers": [
    { "signal": "is_scammer", "type": "noul", "noul": 0.97 }
  ]
}
```

## Options

- **`signals`** scores only the listed signals, which is faster and cheaper
  than scoring all of them. Omit it to score every enabled signal.
- **`include_history`** (default `true`) scores over the object's prior events
  too. Set it to `false` to judge this content on its own, for example a
  listing description regardless of who wrote it.

When no configured signal matches, nothing is scored: `analyzed` is `false`
and `reason` says why. The events are still recorded.

## Deciding

A common pattern is three bands on a `noul` signal:

| `noul` | Action |
|--------|--------|
| `>= 0.9` | Block |
| `0.6 – 0.9` | Hold for human review |
| `< 0.6` | Publish |

Tune the thresholds on your own traffic: read a sample of answers from the
console before you enforce them.

## Failure handling

The request blocks on the analyzer, so give it a timeout suited to your
latency budget and decide what happens when it fails:

- `402` means the balance is empty; the events were recorded but not scored.
- `502` means the analyzer failed; the events were recorded.
- A timeout or network error on your side means you don't know. Failing open
  (publish, then review asynchronously) or closed (hold) is a product decision.

Synchronous scoring is billed like any analysis and does not schedule another
asynchronous one.
