Building billing software that talks to storage management platforms like SiteLink Web Edition, storEDGE, or Easy Storage Solutions is a significant chunk of my day-to-day work in Fort Lauderdale. These platforms expose REST APIs that let you pull tenant data, push payments, listen for unit status changes, and trigger automated billing runs. But "integrating with an API" is a lot more nuanced than firing off a few cURL requests. Let me walk through the patterns I rely on when connecting billing systems to storage management platforms — from authentication through rate limiting and the abstraction layer that keeps everything sane when you're supporting multiple platforms at once.
OAuth vs API Key Authentication
Most storage management APIs you'll encounter fall into one of two authentication camps: OAuth 2.0 client credentials flow, or a simpler API key in the request header. SiteLink Web Edition, for instance, uses a combination of a corporate code, location code, and API key — all passed as query parameters or headers depending on the endpoint. storEDGE uses Bearer tokens issued after an OAuth flow.
For API key auth, I keep credentials in a per-location configuration object stored in the database, encrypted at rest with sodium_crypto_secretbox(). The key is never in the codebase — it lives in an environment variable loaded at runtime. The configuration looks roughly like this in PHP:
class StorageApiCredentials {
public function __construct(
private string $platform, // 'sitelink' | 'storedge'
private string $locationId,
private string $encryptedKey,
private string $nonce
) {}
public function getDecryptedKey(): string {
$masterKey = sodium_base642bin(getenv('STORAGE_API_MASTER_KEY'), SODIUM_BASE64_VARIANT_ORIGINAL);
return sodium_crypto_secretbox_open(
base64_decode($this->encryptedKey),
base64_decode($this->nonce),
$masterKey
);
}
}
For OAuth, I store the refresh token encrypted the same way and implement token refresh logic that checks expiry before every request. A 401 response triggers one automatic refresh attempt — if that fails, I flag the credential record as needing re-authorization and alert the facility manager. You never want a billing job silently failing because a token expired at 2 AM.
Webhook Handling for Unit Status Changes
Unit status webhooks are where things get interesting. When a tenant moves out, vacates, or gets overlocked, the storage platform fires a webhook to your endpoint. Your billing system needs to react — stopping recurring charges, applying a pro-rated final invoice, or triggering a collection workflow.
The first problem with webhooks is verification. SiteLink and most platforms sign their payloads with an HMAC-SHA256 signature in a header like X-Webhook-Signature. Always verify this before processing anything:
function verifyWebhookSignature(string $payload, string $signature, string $secret): bool {
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $signature);
}
$rawBody = file_get_contents('php://input');
if (!verifyWebhookSignature($rawBody, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '', $webhookSecret)) {
http_response_code(401);
exit;
}
The second problem is processing time. Webhooks expect a fast 200 response — ideally within two seconds. If your billing logic for a move-out takes ten seconds to run, you'll get duplicate webhook deliveries. The pattern I use is immediate acknowledgment followed by queued processing: write the raw payload to a webhook_queue table, return 200, and let a separate cron job or background worker process the queue. This also gives you a built-in audit trail and retry mechanism.
Idempotency in Payment API Calls
Payment calls are the area where a bug can cost real money, and idempotency is your safety net. Most payment gateways (Stripe, Authorize.Net, Braintree) support an idempotency key — a unique string you pass with each request that the gateway uses to deduplicate. If your server crashes after sending the request but before receiving the response, you can retry with the same idempotency key and the gateway will return the original result instead of charging the card twice.
I generate idempotency keys from a deterministic hash of the business operation, not a random UUID. That way a retry from a failed job produces the same key:
function makeIdempotencyKey(int $tenantId, int $invoiceId, string $date): string {
return hash('sha256', "payment:{$tenantId}:{$invoiceId}:{$date}");
}
On the storage platform side, when pushing payment records back via the API after collecting them through a separate gateway, I apply the same principle: check whether a payment with that external reference ID already exists before creating a new one. A GET /payments?external_ref={key} call before a POST /payments prevents duplicate payment records on the facility side.
Error Handling and Retry Logic
REST APIs fail. Networks partition, rate limits get hit, and the storage platform occasionally deploys a bad release that returns 500s for twenty minutes. Billing jobs that stop and send an alert on the first failure cause alert fatigue. Billing jobs that retry indefinitely can amplify a problem. The sweet spot is exponential backoff with a cap and a maximum retry count.
function callApiWithRetry(callable $request, int $maxRetries = 4): mixed {
$attempt = 0;
while (true) {
try {
return $request();
} catch (ApiRateLimitException $e) {
if ($attempt >= $maxRetries) throw $e;
$delay = min(pow(2, $attempt) + random_int(0, 1000) / 1000, 60);
sleep((int) $delay);
$attempt++;
} catch (ApiServerException $e) {
if ($attempt >= $maxRetries) throw $e;
sleep(pow(2, $attempt));
$attempt++;
} catch (ApiClientException $e) {
// 4xx except 429 — don't retry
throw $e;
}
}
}
The jitter in the rate limit path (that random_int addition) is important when you're running multiple concurrent billing jobs. Without it, all jobs back off to the exact same second and hit the rate limit again simultaneously.
Rate Limiting Strategies
Storage platform APIs typically publish rate limits per API key, per endpoint, or both. SiteLink's API has per-location limits. When you're running a nightly billing batch that touches every tenant at a facility, you can exhaust those limits fast.
I handle this at two levels. First, a token bucket implementation in shared memory (using APCu or Redis) that tracks requests per key per minute and sleeps the calling thread when the bucket is empty. Second, job-level throttling that spaces out API calls based on the platform's documented limits:
class RateLimiter {
private const REQUESTS_PER_MINUTE = 120;
private const SLEEP_MS = (int)(60_000 / self::REQUESTS_PER_MINUTE);
public function throttle(string $apiKey): void {
$cacheKey = "rate_limit:{$apiKey}";
$count = apcu_fetch($cacheKey) ?: 0;
if ($count >= self::REQUESTS_PER_MINUTE) {
usleep(self::SLEEP_MS * 1000);
}
apcu_store($cacheKey, $count + 1, 60);
}
}
For large batch jobs I also implement a backpressure mechanism: if the API is responding slower than usual (response time over 2 seconds), I reduce the request rate by 50% automatically and log the degradation so I can spot patterns over time.
Building an Abstraction Layer for Multi-Platform Support
The real complexity hits when a billing platform needs to support SiteLink at one facility, storEDGE at another, and Easy Storage Solutions at a third. Every platform has different endpoint structures, different field names for the same concepts (SiteLink calls it unitID, storEDGE uses unit_id, Easy Storage uses unit_number), and different webhook event schemas.
The pattern I've settled on is a platform adapter interface that all platform-specific implementations must satisfy:
interface StoragePlatformAdapter {
public function getTenants(string $locationId): array;
public function getUnit(string $locationId, string $unitId): StorageUnit;
public function postPayment(string $locationId, PaymentRecord $payment): PaymentResult;
public function getUnitStatusChanges(string $locationId, \DateTime $since): array;
public function normalizeWebhookPayload(array $rawPayload): WebhookEvent;
}
Each adapter returns domain objects — StorageUnit, PaymentResult, WebhookEvent — that the billing core knows how to work with. The adapter layer handles all the API-specific quirks: field mapping, pagination (SiteLink uses cursor-based, storEDGE uses offset), date format differences, and authentication header formats. The billing logic above the adapter layer never touches raw API responses.
A factory resolves the right adapter at runtime based on the facility's platform configuration:
class StoragePlatformFactory {
public static function make(string $platform, StorageApiCredentials $creds): StoragePlatformAdapter {
return match($platform) {
'sitelink' => new SiteLinkAdapter($creds),
'storedge' => new StorEdgeAdapter($creds),
'easystorage' => new EasyStorageAdapter($creds),
default => throw new \InvalidArgumentException("Unknown platform: {$platform}"),
};
}
}
This pattern has saved me enormous amounts of refactoring time. When a platform releases a breaking API change — which happens more often than you'd hope — I update one adapter file, run the integration test suite for that adapter, and everything else keeps working. The billing core doesn't care that SiteLink changed their payment endpoint path in v3.2.
Practical Notes from the Field
A few things I've learned the hard way that don't fit neatly into any of the above categories: Always log the raw API request and response for payment calls, not just the outcome — when a facility manager disputes a charge, you need an immutable record. Use database transactions when you're writing the result of an API call — if the API call succeeds but your database write fails, you need to handle that as a separate reconciliation problem, not silently lose the data. And test your webhook endpoint with replay — most platforms have a "resend last webhook" feature in their developer console, and using it regularly during development catches timing bugs that unit tests can't.
Storage management API integration is genuinely complex work, but when the abstraction layer is clean and the error handling is thorough, the billing system it supports becomes rock-solid. Tenants get accurate invoices, facilities get reliable payment processing, and I get to sleep through the night instead of getting 3 AM alerts about failed billing runs.