Authentication
API keys, capability URLs, rate limits, and the error envelope
The platform uses two credential types, each scoped to exactly what it needs.
API keys
All general endpoints (/api/v1/submissions, /api/v1/quotes, …) authenticate with a per-organization API key:
curl "$WINFACTOR_URL/api/v1/quotes" \
-H "Authorization: Bearer wf_live_..."- Create / revoke in Settings → Integrations → API keys (organization admins only).
- The plaintext is shown once at creation. WinFactor stores only a peppered hash — a lost key cannot be recovered, only revoked and replaced.
- Up to 10 active keys per organization. Give each integration its own key so you can revoke independently.
- Optional read-only keys reject all mutating requests with
403 read_only_key. lastUsedAtin the settings UI shows when each key last authenticated.
Capability URLs (pricing callbacks)
The live pricing write-back doesn't need an API key at all. Each pricing.calculated event carries a data.callback.url whose path contains a single-use capability token:
PUT https://app.winfactor.app/api/v1/configuration-sessions/{token}/adjustments- The token is the credential — no
Authorizationheader required. This makes the callback a single HTTP step in ActivePieces. - It is minted fresh for every event and the previous token dies with the rotation.
- Writes are accepted only within a bounded window after the event fired.
- The token grants access to one configuration session only.
Treat callback URLs like passwords: don't log them and don't forward them outside your flow.
Rate limits
| Scope | Limit |
|---|---|
| Per API key | 120 requests/minute |
| Per source IP (pre-auth) | 60 requests/minute |
| Capability-URL endpoints, per IP | 120 requests/minute |
Exceeding a limit returns 429 with a Retry-After header (seconds). Back off and retry.
Error envelope
Every error is JSON with a stable machine-readable code:
{
"error": {
"code": "stale_config_hash",
"message": "…human-readable…",
"details": { "currentConfigHash": "sha256:…" }
}
}| HTTP | code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing/invalid API key or capability token |
| 403 | forbidden_plan | Plan lacks API access (details.requiredPlan) |
| 403 | read_only_key | Mutating request with a read-only key |
| 403 | window_expired | Adjustment window for the session has closed |
| 404 | not_found | Unknown ID — including IDs belonging to another organization |
| 400 | validation_error | Body/query failed validation (details.issues) |
| 409 | stale_config_hash | Adjustment targeted a superseded configuration (details.currentConfigHash) |
| 409 | quote_not_editable | Quote is accepted/rejected/expired |
| 409 | requires_revise | Quote was sent — pass revise: true to version it |
| 409 | quote_not_sendable | Quote can't be sent in its current status |
| 409 | already_in_flight | Redelivery requested for a pending event |
| 410 | session_expired | Configuration session has expired |
| 422 | validation_error | Line items failed semantic validation |
| 429 | rate_limited | Slow down (Retry-After header) |
| 500 | internal_error | Our fault — safe to retry |
Cross-organization IDs always return 404, never 403 — existence is not disclosed.
Lists & pagination
List endpoints return { "data": [...], "nextCursor": "..." | null }. Pass ?limit= (max 100, default 50) and ?cursor= to page:
curl "$WINFACTOR_URL/api/v1/submissions?limit=100&cursor=$NEXT" \
-H "Authorization: Bearer $WINFACTOR_KEY"All authenticated responses carry Cache-Control: no-store.