# Approvals

> Gate money-moving actions behind maker-checker approval policies with the Qualy API.

Approvals add a maker-checker layer to Qualy. You define **policies** that gate money-moving actions; when an action matches a policy, Qualy holds it and opens an approval **request** that one or more approvers must decide before it proceeds.

---

## How it works

1. You create a **policy** — a rule that says "actions of this type, matching these conditions, need approval from these people."
2. When a matching entity is created (a payout, a transaction, …), Qualy **holds** it and opens a **request** in `pending`.
3. Approvers **approve** or **reject** the request.
4. On approval the held action resumes; on rejection it's stopped.

Policies can gate these entity types: `transaction`, `payout`, `payment-split`, `authorization`, and `refund-intent`.

---

## Create a policy

`POST /v1/approvals/policies/create`. At minimum a policy needs a `name`, an `entityType`, a `strategy`, and (for every strategy except `auto-approve`) a list of `approvers`. `conditions` narrow *which* entities the policy applies to — omit them to match all entities of that type.

**cURL**

```bash
curl -X POST https://api.qualyhq.com/v1/approvals/policies/create \
  -H 'Authorization: ApiKey your-api-key-here' \
  -H 'X-TENANT-ID: your-tenant-id-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Large payouts need sign-off",
    "entityType": "payout",
    "strategy": "any-of",
    "approvers": ["6606ab7bfb9085579f1b5769"],
    "conditions": {
      "amountMin": 500000,
      "currencyMode": "any"
    }
  }'
```

**JavaScript**

```javascript
const res = await fetch('https://api.qualyhq.com/v1/approvals/policies/create', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'ApiKey your-api-key-here',
    'X-TENANT-ID': 'your-tenant-id-here',
  },
  body: JSON.stringify({
    name: 'Large payouts need sign-off',
    entityType: 'payout',
    strategy: 'any-of',
    approvers: ['6606ab7bfb9085579f1b5769'],
    conditions: {
      amountMin: 500000, // cents — only payouts ≥ $5,000.00
      currencyMode: 'any',
    },
  }),
});

const { data: policy } = await res.json();
console.log(policy._id);
```

**PHP (Laravel)**

```php
use Illuminate\Support\Facades\Http;

$policy = Http::withHeaders([
    'Authorization' => 'ApiKey your-api-key-here',
    'X-TENANT-ID' => 'your-tenant-id-here',
])->post('https://api.qualyhq.com/v1/approvals/policies/create', [
    'name' => 'Large payouts need sign-off',
    'entityType' => 'payout',
    'strategy' => 'any-of',
    'approvers' => ['6606ab7bfb9085579f1b5769'],
    'conditions' => [
        'amountMin' => 500000, // cents — only payouts ≥ $5,000.00
        'currencyMode' => 'any',
    ],
])->json('data');

echo $policy['_id'];
```

### Policy fields

| Field | Description |
| --- | --- |
| `name` | Required. Human-readable label (max 200 chars). |
| `entityType` | Required. What to gate: `transaction`, `payout`, `payment-split`, `authorization`, or `refund-intent`. |
| `strategy` | Required. How approval is reached — see [Strategies](#strategies). |
| `approvers` | Required unless `strategy` is `auto-approve`. User ids who can decide (1–100). |
| `conditions` | Optional. Narrows which entities match — see [Conditions](#conditions). Omit to match all entities of the type. |
| `priority` | Optional. When several policies match, higher priority wins. |
| `enabled` | Optional. Set `false` to keep a policy without enforcing it. |
| `partnership` | Optional. Scope the policy to one partnership. |

### Strategies

| Strategy | How it resolves |
| --- | --- |
| `any-of` | Approved as soon as **one** approver approves. |
| `all-of` | Every listed approver must approve. |
| `sequential` | Approvers act in listed order, one after another. |
| `round-robin` | The request is assigned to approvers in rotation. |
| `auto-approve` | Approved automatically — no approvers, no waiting. Use to explicitly *exempt* a matching set from approval. |

### Conditions

All conditions are optional and combine with AND. An empty or missing condition matches everything.

| Condition | Matches on |
| --- | --- |
| `amountMin` / `amountMax` | Amount range, in cents (`amountMax` must be ≥ `amountMin`). |
| `currencyMode` | `same-currency`, `fx`, or `any` — whether the entity involves a currency conversion. |
| `sourceCurrencies` / `targetCurrencies` | Specific currencies (e.g. `["AUD","USD"]`). |
| `methods` | Payment methods to gate (e.g. bank-transfer methods). |
| `actions` | Entity actions to gate. |
| `recipientTypes` | Who's on the receiving end: `supplier`, `partnership`, or `self`. |
| `primaryTeams` | Team ids the entity belongs to. |

### Manage policies

| Action | Endpoint |
| --- | --- |
| List | `GET /v1/approvals/policies` (returned under `data.policies`) |
| Retrieve | `GET /v1/approvals/policies/{policyId}` |
| Update | `POST /v1/approvals/policies/{policyId}/update` |
| Delete | `POST /v1/approvals/policies/{policyId}/delete` |

---

## Approval requests

When a gated entity is created, Qualy opens a request. List them with `GET /v1/approvals/requests` (returned under `data.requests`) or fetch one with `GET /v1/approvals/requests/{requestId}`.

### Request statuses

| Status | Meaning |
| --- | --- |
| `pending` | Awaiting a decision. The underlying action is held. |
| `approved` | Approved — the held action resumes. |
| `rejected` | Rejected — the held action is stopped. |
| `canceled` | The request was withdrawn (e.g. the underlying entity was canceled). |

To check whether a specific entity is waiting on approval, use its summary: `GET /v1/approvals/summary/{entityType}/{entityId}`.

---

## Approve or reject a request

An approver acts on a pending request. Both endpoints accept an optional `comment`:

**cURL**

```bash
# Approve
curl -X POST https://api.qualyhq.com/v1/approvals/requests/{requestId}/approve \
  -H 'Authorization: ApiKey your-api-key-here' \
  -H 'X-TENANT-ID: your-tenant-id-here' \
  -H 'Content-Type: application/json' \
  -d '{ "comment": "Verified against the signed invoice." }'

# Reject
curl -X POST https://api.qualyhq.com/v1/approvals/requests/{requestId}/reject \
  -H 'Authorization: ApiKey your-api-key-here' \
  -H 'X-TENANT-ID: your-tenant-id-here' \
  -H 'Content-Type: application/json' \
  -d '{ "comment": "Amount does not match the order." }'
```

**JavaScript**

```javascript
const decide = (requestId, action, comment) =>
  fetch(`https://api.qualyhq.com/v1/approvals/requests/${requestId}/${action}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'ApiKey your-api-key-here',
      'X-TENANT-ID': 'your-tenant-id-here',
    },
    body: JSON.stringify({ comment }),
  }).then((r) => r.json());

// action is 'approve' or 'reject'
const { data: request } = await decide('REQUEST_ID', 'approve', 'Looks good.');
console.log(request.status); // "approved"
```

**PHP (Laravel)**

```php
use Illuminate\Support\Facades\Http;

function decide(string $requestId, string $action, ?string $comment = null): array
{
    return Http::withHeaders([
        'Authorization' => 'ApiKey your-api-key-here',
        'X-TENANT-ID' => 'your-tenant-id-here',
    ])->post(
        "https://api.qualyhq.com/v1/approvals/requests/{$requestId}/{$action}",
        ['comment' => $comment],
    )->json('data');
}

// $action is 'approve' or 'reject'
$request = decide('REQUEST_ID', 'approve', 'Looks good.');
echo $request['status']; // "approved"
```

With an `all-of` or `sequential` policy the request stays `pending` until the required approvers have all approved; with `any-of` the first approval resolves it. A single rejection rejects the request under every strategy.

---

## Next steps

- Get notified when requests resolve with [webhooks](/docs/webhooks.md).
- The entities you can gate: [transactions/refunds](/docs/guides/issuing-transaction-refunds.md), [payment splits](/docs/guides/creating-payment-splits.md), and payouts.
- Filter policies and requests with [Querying data](/docs/queries.md).
