---
title: Decide
description: Evaluate incentive/decision rules for a profile and event with POST /api/v1/decide.
---

`POST /api/v1/decide` runs Retidal's incentive decision pipeline for one profile
and one event, and returns which rules matched — synchronously. The rules'
actual effects (like granting a coupon) are queued asynchronously; the response
tells you what matched, not that the effect has already been applied.

Call this when you need an answer in the moment — for example, deciding whether
to show a discount banner when a user abandons checkout.

## Endpoint

```
POST https://api.retidal.com/api/v1/decide
```

**Auth**: `X-API-Key` with scope `decide` or `decision:call`.

**Rate limit**: 30 requests/minute per (project, IP).

## Request body

<ParamField name="projectId" in="body" type="string" required>
  Must match the API key's own project — a mismatch on `POST /api/v1/decide`
  returns `404`, the same shape as "not found," to avoid leaking whether a
  project exists.
</ParamField>
<ParamField name="profileId" in="body" type="string" required>
  The user/profile whose traits are loaded for rule evaluation.
</ParamField>
<ParamField name="event" in="body" type="object" required>
  <Expandable title="event fields">
    <ParamField name="name" in="body" type="string" required>
      The event name to evaluate rules against.
    </ParamField>
    <ParamField name="props" in="body" type="object">
      Optional event-specific properties available to rule conditions.
    </ParamField>
  </Expandable>
</ParamField>
<ParamField name="traits" in="body" type="object">
  Optional traits merged over the profile's stored traits, for this decision
  only — not persisted. Use this to pass in-the-moment context (like a live
  cart value) without writing it to the profile.
</ParamField>
<ParamField name="userFeatures" in="body" type="object">
  Optional feature snapshot used for offer selection: `valueScore` (number or
  null), `payingAtRisk` (boolean), `attributionChannelQuality` (number or
  null), `historicalRedemptionRate` (number or null). Omitted fields default to
  null.
</ParamField>

```json title="Request"
{
  "projectId": "prj_123",
  "profileId": "uid_456",
  "event": {
    "name": "checkout_abandoned",
    "props": { "cartValue": 12000 }
  }
}
```

Profile traits themselves come from the same store `PUT /api/v1/profile` writes
to (`identified_users.traits_masked`); any inline `traits` you send here are
shallow-merged on top for this evaluation only.

## Response

```json title="200"
{
  "decisionId": "dec_abc",
  "matched": [
    {
      "ruleId": "rule_1",
      "effects": [
        { "type": "grant_coupon", "summary": "5% off next order" }
      ]
    }
  ]
}
```

`matched` is an empty array when no rule fires — that's a valid, successful
response, not an error.

## Errors

| Status | Meaning |
| --- | --- |
| `400` | Invalid JSON or a required field is missing (`POST /api/v1/decide`). |
| `401` | Missing or invalid API key (`POST /api/v1/decide`). |
| `403` | The API key lacks the `decide` / `decision:call` scope (`POST /api/v1/decide`). |
| `404` | Project not found, or the key belongs to a different project — `POST /api/v1/decide`. |
| `429` | Rate limit exceeded (30/min per project + IP) — `POST /api/v1/decide`. |
| `503` | The decision cache or a backing service is unavailable (`POST /api/v1/decide`). |

## Next steps

<CardGroup cols={2}>
  <Card title="Profiles" href="/docs/decisioning/profiles">
    Keep the traits decide rules evaluate against up to date.
  </Card>
  <Card title="Coupons" href="/docs/decisioning/coupons">
    Redeem and check the coupons a decision might grant.
  </Card>
</CardGroup>
