Implementation guides
Approvals
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
- You create a policy — a rule that says "actions of this type, matching these conditions, need approval from these people."
- When a matching entity is created (a payout, a transaction, …), Qualy holds it and opens a request in
pending. - Approvers approve or reject the request.
- 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 -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"
}
}'
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);
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. |
approvers | Required unless strategy is auto-approve. User ids who can decide (1–100). |
conditions | Optional. Narrows which entities match — see 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:
# 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." }'
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"
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.
- The entities you can gate: transactions/refunds, payment splits, and payouts.
- Filter policies and requests with Querying data.