Best Practices
Guidelines for building reliable integrations with the OrbitKit API.
Follow these practices to build robust, reliable integrations with the OrbitKit API.
Error handling
- Check
error.code, not just the HTTP status — thecodefield provides a machine-readable identifier (e.g.,app-not-found,subscription-required) - Display
error.messageto users — messages are human-readable and provide actionable guidance - Log
X-Request-Id— every response includes this header; include it when contacting support
const res = await fetch(url, options);
if (!res.ok) {
const { error } = await res.json();
console.error(`[${res.headers.get('X-Request-Id')}] ${error.code}: ${error.message}`);
}
Idempotency
- GET, PUT, DELETE are safe to retry — calling them multiple times produces the same result
- POST creates are NOT idempotent —
POST /api/appscreates a new app each time; guard against duplicate calls - POST actions (deploy, subscribe) have built-in guards — deploying twice in a row is safe (the second deploy is a no-op if nothing changed)
Polling patterns
Some operations are asynchronous. Use exponential backoff when polling:
| Operation | Initial interval | Max interval | Timeout |
|---|---|---|---|
| Domain DNS verification | 30 seconds | 5 minutes | 1 hour |
| Post-checkout status | 5 seconds | 30 seconds | 2 minutes |
async function pollUntil(checkFn, { interval = 5000, maxInterval = 30000, timeout = 120000 }) {
const start = Date.now();
let delay = interval;
while (Date.now() - start < timeout) {
const result = await checkFn();
if (result) return result;
await new Promise(r => setTimeout(r, delay));
delay = Math.min(delay * 2, maxInterval);
}
throw new Error('Polling timeout');
}
Deploy after configuration changes
Configuration changes (site settings, privacy policy, AASA, support/deletion pages) are saved to the database but not reflected on the live site until you deploy:
curl -X POST https://api.orbitkit.io/api/apps/$APP_ID/deploy \
-H "Authorization: Bearer $TOKEN"
var request = URLRequest(url: URL(string: "https://api.orbitkit.io/api/apps/\(appId)/deploy")!)
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let (_, response) = try await URLSession.shared.data(for: request)
// 200 OK on success
await fetch(`https://api.orbitkit.io/api/apps/${appId}/deploy`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
Batch your changes, then deploy once to minimize deploy operations.
Data model
Understanding the data hierarchy helps you use the API effectively:
Account (user)
└── App (appId)
├── Site config (slug, domain, banner)
├── Privacy Policy (wizard data, 14 Apple categories)
├── Support Page (markdown content)
├── Deletion Page (markdown content)
├── AASA (Universal Links, App Clips, Passkeys, Handoff)
└── Subscription (plan, status, Stripe link)
- Each user can have multiple apps — each app has its own subscription
- An app’s site has a slug (e.g.,
my-app.orbitkit.io) and an optional custom domain - Pages (policy, support, deletion) are configured independently and included in each deploy
API key security
- Never commit API keys to source control — use environment variables or secret management tools (e.g., GitHub Actions secrets, AWS Secrets Manager)
- Use descriptive names — name keys after their purpose (e.g., “GitHub Actions Deploy”, “Staging CI”) so you know which to revoke
- Rotate keys regularly — create a new key, update your integration, then revoke the old key. See the rotation guide
- Revoke compromised keys immediately — revocation is instant and the key stops working on the next request
- Use one key per integration — if one system is compromised, you only need to revoke and replace that key
- Monitor
lastUsedAt— check the API keys list to identify unused keys that should be revoked
Idempotency
- GET, PUT, DELETE are safe to retry — calling them multiple times produces the same result
- POST creates are NOT idempotent by default —
POST /api/appscreates a new app each time - Use
Idempotency-Keyfor safe retries — pass anIdempotency-Keyheader on POST requests to ensure the operation only happens once, even if you retry due to a network error:
curl -X POST https://api.orbitkit.io/api/apps \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-request-id-123" \
-d '{"appName": "My App"}'
var request = URLRequest(url: URL(string: "https://api.orbitkit.io/api/apps")!)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("unique-request-id-123", forHTTPHeaderField: "Idempotency-Key")
request.httpBody = try JSONEncoder().encode(["appName": "My App"])
let (data, _) = try await URLSession.shared.data(for: request)
let result = try JSONDecoder().decode(CreateAppResponse.self, from: data)
const res = await fetch("https://api.orbitkit.io/api/apps", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": "unique-request-id-123",
},
body: JSON.stringify({ appName: "My App" }),
});
const { appId, appName, slug } = await res.json();
Idempotency keys are retained for 24 hours. Reusing the same key with the same parameters returns the original response. Reusing with different parameters returns a 422 IDEMPOTENCY_KEY_REUSE error.
Security
- Server-to-server calls only — never embed API keys in client-side code shipped to end users
- Rotate credentials if you suspect a compromise — revoke API keys immediately
- Use HTTPS exclusively — the API only accepts HTTPS connections
- Validate responses — check HTTP status codes and
error.codefields before processing response data