Provider Center authenticates every consumer request with an API key issued to an Application. Business endpoints additionally require an X-End-Customer-Id header identifying the end-user tenant on whose behalf the call is made.
This page covers the key format, the required headers, how to obtain and rotate keys, and how end-customer identification ties back into the quota model.
POST /v1/influencers/search HTTP/1.1
Host: api-providers.fluvip.ai
Authorization: Bearer pk_live_7f2a1c93_8Kj4QmR2wXvP9nLs0tAbE1yUc6HoG5Dz
X-End-Customer-Id: 7a3b5c18-2d4e-4f6a-9b8c-1e3d2f4a5b6c
Content-Type: application/json
{ "query": "beauty creators, Colombia, 10k-100k followers" }
Two headers are mandatory on every business call:
| Header | Required | Purpose |
|--------|----------|---------|
| Authorization: Bearer <key> | yes | Identifies Organization + Application |
| X-End-Customer-Id: <uuid> | yes (on /v1/* business endpoints) | Identifies the caller's tenant for quotas and audit |
Plus these optional headers, documented here for completeness:
| Header | Purpose |
|--------|---------|
| X-Idempotency-Key: <uuid> | Deduplicate a job submission for 24h |
| X-Mode: sync \| job | Force async (job) mode |
| X-Prefer-Provider: <slug> | Pin the call to one provider |
| X-No-Failover: true | Disable failover on domain contracts |
| X-Cache-Policy: bypass \| exact | Force cache miss, or opt into exact-match for non-cacheable endpoints |
Keys follow the pattern:
pk_live_<prefix8>_<secret40>
pk_live_ — environment marker. Production keys use pk_live_, staging keys use pk_test_.<prefix8> — 8-char prefix stored in plaintext. This is what the admin panel shows in lists and in logs.<secret40> — 40-char high-entropy secret. Stored as an argon2id hash; the raw value is shown exactly once at creation.Example:
pk_live_7f2a1c93_8Kj4QmR2wXvP9nLs0tAbE1yUc6HoG5Dz
└─ prefix ─┘ └──────── secret ─────────┘
Never log or commit the secret segment. If it leaks, rotate the key (see below).
Keys are created from the admin panel, scoped to an Application:
ADMIN or DEVELOPER role in the Organization.production-server, ci-pipeline)..env vault, etc.). Closing the dialog destroys the plaintext for good.Equivalent GraphQL mutation used by the panel:
mutation RotateKey($keyId: ID!) {
rotateApiKey(apiKeyId: $keyId) {
id
prefix # visible
secret # returned once, never again
expiresAt
}
}
curl -X POST https://api-providers.fluvip.ai/v1/influencers/search \
-H "Authorization: Bearer $PROVIDER_CENTER_KEY" \
-H "X-End-Customer-Id: $TEAM_ID" \
-H "Content-Type: application/json" \
-d '{
"query": "beauty creators, Colombia",
"filters": { "followers": { "min": 10000, "max": 100000 } },
"limit": 25
}'
const res = await fetch("https://api-providers.fluvip.ai/v1/influencers/search", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.PROVIDER_CENTER_KEY}`,
"X-End-Customer-Id": teamId,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, filters, limit: 25 }),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message} (requestId=${error.requestId})`);
}
const { data, meta } = await res.json();
import httpx, os
r = httpx.post(
"https://api-providers.fluvip.ai/v1/influencers/search",
headers={
"Authorization": f"Bearer {os.environ['PROVIDER_CENTER_KEY']}",
"X-End-Customer-Id": team_id,
},
json={"query": query, "filters": filters, "limit": 25},
timeout=30,
)
r.raise_for_status()
payload = r.json()
X-End-Customer-Id headerProvider Center enforces a three-level quota model: Organization → Application → EndCustomer. The first two are resolved from the API key. The third — EndCustomer — must be supplied by the caller on every business request.
An EndCustomer represents a tenant inside the consuming application. For fluvip-api that's a Fluvip team. For lucia-ai that's a workspace. The value you send is typically your own tenant's ID:
X-End-Customer-Id: 7a3b5c18-2d4e-4f6a-9b8c-1e3d2f4a5b6c
You do not need to pre-register EndCustomers. The first request carrying an unknown (applicationId, X-End-Customer-Id) pair auto-creates the record, seeded from the Application's endCustomerQuotaTemplate. From that moment the admin panel shows the EndCustomer and its quota can be tuned independently.
This removes the need for a provisioning step on every new customer your app onboards — sending a consistent, stable ID per tenant is enough.
Only metadata endpoints are exempt: GET /v1/providers, GET /v1/usage/me, GET /v1/jobs/:id. Every business POST requires it. A request missing X-End-Customer-Id gets:
{
"error": {
"code": "INVALID_PARAMS",
"message": "Missing required header: X-End-Customer-Id",
"retryable": false,
"requestId": "req_01HXA..."
}
}
You should rotate keys:
Two keys can coexist on the same Application, which is the supported rotation mechanism:
401 UNAUTHORIZED immediately.mutation Rotate($id: ID!) {
rotateApiKey(apiKeyId: $id) {
id
prefix
secret # the only time you will see it
}
}
mutation Revoke($id: ID!) {
revokeApiKey(apiKeyId: $id) { id revokedAt }
}
argon2id(secret) with a per-key salt. A database breach does not expose usable credentials.ApiKey.lastUsedAt. Unused keys surface as candidates for revocation.expiresAt. After that timestamp the gateway returns 401.Every request runs through auth first, then quota enforcement. Resolution is:
Authorization: Bearer ... → ApiKey → Application → Organization.X-End-Customer-Id → EndCustomer (created on first sight).orgId, applicationId, endCustomerId) feed the atomic Lua quota script described in Concepts → Quotas. All three must pass or the request is rejected with QUOTA_EXCEEDED.In other words: even a valid API key can fail with 429 QUOTA_EXCEEDED if the EndCustomer — or the parent Application or Organization — has already consumed its window.
| Code | HTTP | Cause |
|------|------|-------|
| UNAUTHORIZED | 401 | Missing, malformed, revoked, or expired API key |
| INVALID_PARAMS | 400 | Missing X-End-Customer-Id or malformed header |
| FORBIDDEN | 403 | Valid key but Application disabled or Organization suspended |
| RATE_LIMITED | 429 | Per-key rate limit exceeded (default 100 req/s) |
| QUOTA_EXCEEDED | 429 | Credit quota exhausted at the scope identified by error.scope |
See Concepts → Errors for the full envelope and retry guidance.