Start with a concrete promise
Consider a fictional API for free consultation appointments. An account may have at most one active reservation; an appointment has a limited number of available places. Users may cancel their own reservations. This sounds simple but contains several decisions: which account is this? Which states count as active? When does a place become available again? What happens if two requests arrive simultaneously?
Write these rules with the business owners before programming thresholds or locks. A technical implementation cannot correctly guess an unstated exception. For example, define whether an expired reservation still occupies a place and whether an approved replacement appointment ends the previous reservation.
OWASP explicitly covers abuse of sensitive business flows even where no classic implementation vulnerability is needed. An automatable permitted flow can cause harm through volume or combination alone. Deciding which use remains intended is a business responsibility. OWASP API6:2023.
Check permissions and state at every transition
A flow can be described through a few named states. For reservations, these could be ‘requested’, ‘confirmed’, ‘cancelled’ and ‘expired’. Not every transition is allowed:
On narrow screens, scroll the table sideways.
| Action | Prerequisite | Intended effect |
|---|---|---|
| Confirm reservation | Own request, still valid, capacity available, account rule satisfied | Allocate a place definitively and change state |
| Cancel reservation | Own active reservation, cancellation permitted by business rules | Change state and release the place exactly once |
| Expire request | Deadline passed, not yet confirmed | End the request; do not undo a confirmed reservation |
| Administratively change reservation | Dedicated administrative permission and documented reason | Perform a separately permitted transition traceably |
This table is a synthetic example, not a general appointment rule. Its purpose is to reveal missing decisions.
The server determines current state from a reliable source. Client-supplied fields such as ‘approved’ or ‘remaining capacity’ are not authoritative. A disabled frontend button does not prevent a direct API call either. Field permissions determine which information a client may set at all.
Authorisation and state checking complement each other. General permission to edit a reservation does not permit changing it from any state. Conversely, an allowed state grants no rights over another user's reservations. Object authorisation must apply to the same action.
Concurrent calls must obey the same rule
A common defect consists of two separate steps: first check whether a place remains, then allocate it later. Two requests may both see the same free place. Each step being fast does not eliminate this window.
One option for a single capacity row is a conditional update:
UPDATE demo_slot
SET remaining = remaining - 1
WHERE slot_id = :slot_id
AND remaining > 0;
The application may continue only if exactly one row was affected. Reservation record, account rule and capacity change must join a suitable common transaction or equivalent consistent mechanism. The example alone does not fulfil the complete reservation rule.
In PostgreSQL under Read Committed, after an intervening change a concurrent update re-evaluates the WHERE condition against the updated row. This supports the single-row approach shown. Rules across multiple rows, tables or external systems need separate assessment; an arbitrary SELECT inside a transaction does not automatically exclude every relevant competitor. PostgreSQL 18: Transaction Isolation, section 13.2.1.
Choose locks, uniqueness constraints, conditional state changes or suitable isolation according to the concrete invariant. An invariant is the condition that must hold after every completed operation, such as ‘remaining capacity is never negative’. If a database reports a conflict, the entire business operation needs safe handling. Retrying one partial statement in isolation may produce different results.
For these flows, OWASP recommends explicit state modelling, server-authoritative values and atomic decisions. Implementation still depends on the storage and flow used. OWASP: Business Logic Security.
Idempotency, quantity rules and rate limits solve different tasks
Idempotency helps recognise multiple delivery attempts for the same job. It does not prevent someone creating several new jobs with new identifiers. ‘At most one active reservation per account’ therefore also needs business-level uniqueness or an equivalent enforced rule.
A rate limit restricts frequency or resource load. Ten allowed calls may still block ten places even though business rules intend only one. Conversely, a slow legitimate action must not gain unlimited permission merely because it stays below a technical threshold.
Consider the authoritative identity for quantity rules. A per-key limit may miss the business goal if one organisation has several legitimate keys. ‘Once per person’ also requires an appropriate, proportionate identity decision; an IP address is not automatically a person. This article defines neither an identification mechanism nor a legal basis for it.
Consider the end of an operation as well as its beginning. Two cancellations must not release the same place twice. A delayed expiry event must not undo a reservation confirmed in the meantime. Check current state before transition and store which effect has already been assigned.
Explicitly plan external effects and reversals
A local transaction cannot readily roll back an external message, payment or partner reservation. If such effects are needed, first store a traceable job and define processing, reconciliation and retries. The external API guide covers this boundary.
A business reversal differs from forgetting the first job. Cancelling a reservation may be a separately authorised, logged action; a sent notification cannot be recalled. Identify possible partial outcomes and a state for unresolved cases.
Rolling back a new security rule also needs this assessment. If a new limit blocks too many legitimate operations, first preserve affected job identifiers and the last stable state. Revert the faulty configuration specifically. Previously enforced uniqueness or capacity conditions should not disappear. Automatically executing masses of previously rejected jobs would be a new business operation needing its own conditions.
Verify the flow with your own data
Test the contract in an authorised isolated environment. Start with a normal allowed flow. Then compare the same actions from an incorrect state, with another test account and after expiry. Use your own synthetic objects; other customers' identifiers are unnecessary.
For the appointment example, evidence also includes two controlled simultaneous confirmations for the last place. At most one may succeed; capacity must match actual reservations. Repeat a cancellation and deliver a delayed expiry event. Check durable final state, not just HTTP statuses.
The SQL example illustrates only a conditional single-row change. It has not been tested here under PostgreSQL or with actual concurrent execution. Your concrete transaction, error rollback and any external effect therefore need their own integration tests.
Make rule violations visible
During operation, record rejected state transitions, conflicts and unresolved jobs with a reliable reason and internal correlation. An increase may indicate a client error, changed flow or abuse. It is not alone proof of attack. Logging and monitoring connects these signals with an accountable response.
A clean HTTP header and a good public check grade say nothing about adherence to the appointment rule. The API Security Check does not test these business flows. The appropriate next step is a documented rule contract with development and business owners, plus targeted testing of your own API promises.
Sources and date
Sources checked: 27 September 2026. Links also appear alongside the statements they support.