Security & Best Practices
Follow these guidelines to build a secure, reliable, and maintainable integration with the iorder POS system.
Always Use HTTPS in Production
The iorder API transmits API keys and order data. In production, always connect over HTTPS. Never fall back to HTTP. If you are testing against a staging environment that does not use HTTPS, ensure it is isolated from production networks.
Verify TLS in your HTTP client:
curl -v -H "api_key: abc123-def456-ghi789" \
"https://{host}/orders/location"Check that the response includes SSL certificate verify ok or equivalent.
Store API Keys Securely
- Never hardcode API keys in source code, configuration files that are checked into version control, or client-side code.
- Use environment variables or a secrets manager:bash
export IORDER_API_KEY="abc123-def456-ghi789" - On Kubernetes, use a Kubernetes Secret.
- On cloud platforms, use the platform's secrets manager (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault).
- In CI/CD pipelines, inject the API key at runtime rather than storing it in pipeline configuration.
Example: Loading API key from environment (Node.js):
const apiKey = process.env.IORDER_API_KEY;
if (!apiKey) {
throw new Error("IORDER_API_KEY environment variable is not set");
}Example: Loading API key from environment (Java):
String apiKey = System.getenv("IORDER_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("IORDER_API_KEY not set");
}Rotate API Keys Periodically
Agree a rotation schedule with the operator and replace a suspected compromised key promptly. Follow the key replacement procedure to create a replacement for the correct location, switch the client, verify processing, stop old connections, and delete the obsolete key. Coordinate the switch to avoid duplicate processing.
Validate All Data
Although the integration uses polling (not webhooks), you should still treat all data from the API as untrusted:
- Validate that the API response is valid JSON.
- Validate that required fields are present and have expected types.
- Do not pass raw API data directly into database queries (use parameterized queries).
- Validate string lengths before storing (e.g., order comments, product names).
- Sanitize data before displaying it in any user-facing UI.
Use Exponential Backoff for Retries
The following bounded retry example is suitable for read requests. Before retrying a status update or command response after a timeout or 5xx response, reconcile whether it already succeeded. Do not use this wrapper to repeat a cash-register action.
async function fetchWithRetry(url, options) {
const maxAttempts = 5;
const initialDelay = 1000;
const maxDelay = 30000;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
let response;
try {
response = await fetch(url, options);
} catch (error) {
if (attempt === maxAttempts - 1) throw error;
}
if (response?.ok) return response;
if (response && response.status < 500) {
throw new Error(`HTTP ${response.status}`);
}
if (attempt === maxAttempts - 1) {
throw new Error(`HTTP ${response.status}`);
}
const delay = Math.min(initialDelay * 2 ** attempt, maxDelay);
await new Promise(resolve => setTimeout(resolve, delay));
}
}- Minimum backoff: 1 second
- Maximum backoff: 30-60 seconds
- Jitter: Add random jitter to avoid thundering herd
- Retry only on: 5xx errors, network errors, timeouts
- Do not retry on: 4xx errors (except 429 if you implement rate limiting)
Implement Idempotency for Status Updates
Persist the processing outcome before acknowledging an order to iorder. Keep order numbers and command IDs in separate namespaces, scoped to the installation/location. Coordinate multiple workers before submitting work to the register.
If an acknowledgement times out, the operation may already have succeeded. Reconcile the result rather than processing the external order again. A status update does not reserve work, and a second update is not guaranteed to be rejected. See duplicate and concurrent updates.
Log All API Interactions
Record enough metadata to investigate a failure: timestamp, request method, endpoint path without query credentials, HTTP status, duration, location, and the relevant order or command identifier. Record SSE connection changes and sanitized error details.
Do not log API keys, raw SSE message payloads, payment details, or full request/response bodies by default. If temporary payload diagnostics are necessary, agree their scope, access, redaction, and retention with the operator first.
Monitor SSE Connection Health
The SSE stream is the heartbeat of your integration. Monitor it closely:
- Track the number of SSE reconnections per hour.
- Alert if reconnections exceed a threshold (e.g., more than 10 per hour).
- Track the time between the last keepalive event and a connection drop.
- Log every SSE connection state change (connected, disconnected, error).
- If running in a containerized environment, expose SSE health as a Prometheus metric or health check endpoint.
Example metric:
# HELP iorder_sse_connection_status Current SSE connection status (1=connected, 0=disconnected)
# TYPE iorder_sse_connection_status gauge
iorder_sse_connection_status{location="Main Floor"} 1
# HELP iorder_sse_reconnects_total Total number of SSE reconnections
# TYPE iorder_sse_reconnects_total counter
iorder_sse_reconnects_total{location="Main Floor"} 3Handle Network Timeouts Appropriately
Set explicit timeouts for all HTTP operations:
| Operation | Recommended Timeout |
|---|---|
| Regular API calls (GET, PUT, POST) | 30 seconds |
| SSE connection | Idle/read threshold, for example 60 seconds without an event; reconnect on closure |
| DNS resolution | 5 seconds |
| Connection establishment | 10 seconds |
Example (Node.js with fetch):
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch(url, {
headers: { api_key: apiKey },
signal: controller.signal,
});
// ...
} finally {
clearTimeout(timeout);
}Rate Limit Your Own Polling
Even though the server does not currently enforce rate limiting, you should self-limit:
- SSE-driven polling: Poll on
messageevents and after connecting or reconnecting. Use bounded fallback polling for recovery. - Fallback interval polling: Poll at intervals no shorter than 5 seconds as a safety net.
- No busy loops: Never poll in a tight loop (e.g., every 100ms). The server is shared by multiple clients.
Prefer SSE-Driven Polling Over Interval-Only Polling
The system is designed for SSE-driven polling. Interval-only polling:
- Increases server load unnecessarily.
- Delays order processing (you might poll just before an order arrives, then wait the full interval).
- Wastes network and compute resources.
Recommended approach:
- Open a persistent SSE connection.
- Poll immediately when a
messageevent arrives. - Poll at a 5-second interval as a fallback (in case SSE messages are missed).
- If SSE reconnects after a drop, resume SSE-driven polling and reduce fallback polling.
Test Error Scenarios
Before deploying to production, test your integration against these scenarios:
- Invalid API key
- Expired API key
- Server restart (SSE reconnection)
- Network disconnection (SSE reconnection with offset)
- Concurrent order processing (two clients polling the same location)
- Invalid state names and repeated acknowledgements
- Large payload (many order rows)
- Slow network (timeout handling)
- Missing/null fields in API responses
A robust integration handles all of these without crashing, leaking resources, or corrupting data.
Next Steps
- Review the Error Handling chapter for detailed error scenarios
- Refer to the Overview for the complete integration flow