API Key Integration Guide
This guide covers everything you need to integrate with the GYBC platform using API keys, including key management, authentication, scopes, rate limiting, and best practices.
Overview
API keys provide programmatic, backend-to-backend access to the GYBC platform. There are two key types:
| Key Type | Prefix | Use Case |
|---|---|---|
| Secret key | sk_* | Server-side integration from your backend |
| Publishable key | pk_* | Client-side apps (iOS, web) — requires a user JWT alongside it |
Secret keys authenticate your backend directly. Publishable keys are safe to embed in client apps but must always be paired with a user JWT (see Multi-Tenancy for client app setup).
The full key value is only returned once at creation time. Store it securely — it cannot be retrieved again.
Using API Keys in Requests
Pass your API key in the X-API-Key header. All endpoints use POST with a JSON body.
curl -X POST https://api.yocaso.dev/api/v1/llm/gateway/list-threads \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
User Impersonation
Secret keys with the users:impersonate scope can act on behalf of a specific user by setting the X-On-Behalf-Of header:
curl -X POST https://api.yocaso.dev/api/v1/llm/gateway/send-message \
-H "X-API-Key: sk_your_key_here" \
-H "X-On-Behalf-Of: user_123" \
-H "Content-Type: application/json" \
-d '{
"conversation_key": "conv_abc",
"user_message": {"role": "user", "content": "Hello"}
}'
Managing API Keys
API key management endpoints require JWT authentication (not API keys). You cannot use an API key to manage other API keys.
Create a Secret Key
POST /api/v1/api-keys/create
Request:
{
"name": "production-backend",
"description": "Production backend server",
"permissions": ["conversations:read", "conversations:write"],
"rate_limit": {
"requests_per_minute": 1000
},
"expires_at": "2026-12-31T23:59:59Z"
}
Response:
{
"result": {
"key_id": "key_abc123",
"key": "sk_full_key_value_here",
"key_prefix": "sk_2hfK"
}
}
| Field | Description |
|---|---|
key_id | Unique identifier — use this for all management operations |
key | The full key value — returned only once, store it securely |
key_prefix | First few characters for display and log identification |
Create a Publishable Key
Publishable keys are designed for client-side apps (iOS, web). They must always be paired with a user JWT, and require OIDC configuration so the platform can validate the JWT.
POST /api/v1/api-keys/create
Request:
{
"name": "socayo-ios",
"description": "Socayo iOS app",
"keyType": "KEY_TYPE_PUBLISHABLE",
"permissions": ["conversations:read", "conversations:write"],
"publishableConfig": {
"oidc": {
"issuer": "https://securetoken.google.com/your-firebase-project",
"jwksUrl": "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com",
"audience": "your-firebase-project"
}
}
}
Response:
{
"result": {
"key_id": "key_xyz789",
"key": "pk_full_key_value_here",
"key_prefix": "pk_9kLm"
}
}
The OIDC config tells the platform how to validate user JWTs presented alongside this key:
| Field | Description |
|---|---|
issuer | Expected iss claim in the JWT |
jwksUrl | URL to fetch the public keys for JWT signature verification |
audience | Expected aud claim in the JWT |
userIdClaim | (Optional) JWT claim to extract the user ID from. Defaults to sub |
requiredClaims | (Optional) Map of claim names to expected values. Supports dot notation for nested claims (e.g., firebase.tenant) |
The platform supports any OIDC-compliant provider. Common configurations:
| Provider | issuer | jwksUrl |
|---|---|---|
| Firebase | https://securetoken.google.com/<project-id> | https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com |
| Auth0 | https://<tenant>.auth0.com/ | https://<tenant>.auth0.com/.well-known/jwks.json |
| Okta | https://<org>.okta.com/oauth2/default | https://<org>.okta.com/oauth2/default/v1/keys |
| AWS Cognito | https://cognito-idp.<region>.amazonaws.com/<user-pool-id> | https://cognito-idp.<region>.amazonaws.com/<user-pool-id>/.well-known/jwks.json |
| Keycloak | https://<host>/realms/<realm> | https://<host>/realms/<realm>/protocol/openid-connect/certs |
See Multi-Tenancy for full client app setup examples.
List Keys
POST /api/v1/api-keys/list
Request:
{
"page": 1,
"per_page": 20
}
Response:
{
"keys": [
{
"key_id": "key_abc123",
"name": "production-backend",
"key_prefix": "sk_2hfK",
"description": "Production backend server",
"permissions": ["conversations:read", "conversations:write"],
"rate_limit": {
"requests_per_minute": 1000,
"requests_per_hour": 0,
"burst_size": 0
},
"created_at": "2025-03-01T10:00:00Z",
"expires_at": "2026-12-31T23:59:59Z"
}
],
"pagination": {
"total": 1,
"page": 1,
"per_page": 20,
"total_pages": 1
}
}
Get Key Details
POST /api/v1/api-keys/get
Request:
{
"key_id": "key_abc123"
}
Update a Key
Uses a field mask to specify which fields to modify. Only fields listed in update_mask are changed.
POST /api/v1/api-keys/update
Request:
{
"key_id": "key_abc123",
"name": "production-backend-v2",
"permissions": ["conversations:read", "conversations:write", "plans:read"],
"update_mask": "name,permissions"
}
Revoke a Key
Permanently revokes an API key, making it immediately unusable. This action cannot be undone.
POST /api/v1/api-keys/revoke
Request:
{
"key_id": "key_abc123"
}
Get Usage Statistics
Returns verification statistics for a specific API key.
POST /api/v1/api-keys/usage
Request:
{
"key_id": "key_abc123"
}
Response:
{
"result": {
"total_verifications": 15230,
"successful_verifications": 15100,
"failed_verifications": 130
}
}
Scopes and Permissions
Scopes control what operations an API key can perform. Assign scopes when creating or updating a key via the permissions field.
Available Scopes
| Scope | Description |
|---|---|
* | Full access (all scopes) |
conversations:read | Read conversations and threads |
conversations:write | Send messages, create threads |
plans:read | Read coaching plans |
plans:write | Create and modify coaching plans |
users:read | Read user profiles |
users:impersonate | Act on behalf of users via X-On-Behalf-Of |
billing:read | Read billing information |
billing:write | Modify billing settings |
invoices:read | Read invoices |
invoices:write | Modify invoices |
Scope Matching Rules
- Exact match:
conversations:readgrants only read access to conversations - Wildcard:
*grants access to all scopes - Prefix wildcard:
users:*grants all user-related scopes (users:read,users:impersonate, etc.)
Publishable Key Restrictions
Publishable keys (pk_*) automatically have the following scopes blocked for security:
api_keys:*— Cannot manage API keyswebhooks:*— Cannot manage webhookstenants:*— Cannot manage tenantsbilling:*— Cannot access billingsystem:*— Cannot access system operationsusers:impersonate— Cannot impersonate users*— Cannot have wildcard access
Rate Limiting
Rate limits can be configured per API key to control request throughput.
Configuration
Set rate limits when creating or updating a key:
{
"rate_limit": {
"requests_per_minute": 1000,
"requests_per_hour": 50000,
"burst_size": 100
}
}
| Field | Description |
|---|---|
requests_per_minute | Maximum requests per minute (0 = unlimited) |
requests_per_hour | Maximum requests per hour (0 = unlimited) |
burst_size | Maximum burst size for token bucket algorithm (0 = use default) |
Rate Limit Headers
Every API response includes rate limit information in HTTP headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds until the next request is allowed (present on 429 responses) |
These headers are included on all responses (200, 401, 429), enabling client-side throttling and retry logic.
Rate Limit Response
When a request is rate-limited, the API returns HTTP 429 (Too Many Requests) with Retry-After and X-RateLimit-* headers. The response body also includes:
{
"rate_limit": {
"limit": 1000,
"remaining": 0,
"reset_at": "2026-03-06T12:05:00Z"
}
}
Key Rotation
There are two approaches to key rotation: the automated RotateApiKey RPC and a manual three-step process.
Automated Rotation (Recommended)
Use the RotateApiKey RPC for zero-downtime rotation with a configurable grace period.
POST /api/v1/api-keys/rotate
Request:
{
"key_id": "key_abc123",
"grace_period_hours": 24
}
Response:
{
"result": {
"key_id": "key_new456",
"key": "sk_new_full_key_value",
"key_prefix": "sk_9xYz"
}
}
| Field | Description |
|---|---|
key_id | The key to rotate |
grace_period_hours | Hours during which both old and new keys are valid (default: 24, max: 720 / 30 days) |
The old key is automatically set to expire after the grace period. Both keys work during the transition window, giving consumers time to switch.
Publishable keys (pk_*) cannot be rotated via this RPC — use the manual procedure below.
Manual Rotation
For cases where you need full control over the transition:
- Create a new key with the same permissions as the existing one:
curl -X POST https://api.yocaso.dev/api/v1/api-keys/create \
-H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "production-backend-rotated",
"description": "Rotated key - March 2026",
"permissions": ["conversations:read", "conversations:write"]
}'
-
Update all consumers to use the new key. Both old and new keys work during this transition window.
-
Revoke the old key once all consumers have been updated:
curl -X POST https://api.yocaso.dev/api/v1/api-keys/revoke \
-H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{"key_id": "old_key_id"}'
Best Practices
- Rotate regularly — establish a rotation schedule (e.g., every 90 days)
- Use key names with dates — e.g.,
production-backend-2026-03for easy tracking - Monitor usage — check usage stats before revoking to confirm the old key is no longer in use
- Set expiration dates — use
expires_atto enforce automatic expiration as a safety net
Code Examples
cURL
# List threads
curl -X POST https://api.yocaso.dev/api/v1/llm/gateway/list-threads \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
# Send a message (with user impersonation)
curl -X POST https://api.yocaso.dev/api/v1/llm/gateway/send-message \
-H "X-API-Key: sk_your_key_here" \
-H "X-On-Behalf-Of: user_123" \
-H "Content-Type: application/json" \
-d '{
"conversation_key": "conv_abc",
"user_message": {"role": "user", "content": "Hello"}
}'
Python
import requests
BASE_URL = "https://api.yocaso.dev"
API_KEY = "sk_your_key_here"
headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
}
# List threads
response = requests.post(
f"{BASE_URL}/api/v1/llm/gateway/list-threads",
headers=headers,
json={},
)
print(response.json())
# Send a message on behalf of a user
response = requests.post(
f"{BASE_URL}/api/v1/llm/gateway/send-message",
headers={**headers, "X-On-Behalf-Of": "user_123"},
json={
"conversation_key": "conv_abc",
"user_message": {"role": "user", "content": "Hello"},
},
)
print(response.json())
Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const (
baseURL = "https://api.yocaso.dev"
apiKey = "sk_your_key_here"
)
func main() {
// List threads
body, _ := json.Marshal(map[string]any{})
req, _ := http.NewRequest("POST",
baseURL+"/api/v1/llm/gateway/list-threads",
bytes.NewReader(body))
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println("Status:", resp.Status)
fmt.Println("Body:", string(respBody))
}
Node.js
const BASE_URL = "https://api.yocaso.dev";
const API_KEY = "sk_your_key_here";
// List threads
const response = await fetch(
`${BASE_URL}/api/v1/llm/gateway/list-threads`,
{
method: "POST",
headers: {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
}
);
const data = await response.json();
console.log(data);
// Send a message on behalf of a user
const msgResponse = await fetch(
`${BASE_URL}/api/v1/llm/gateway/send-message`,
{
method: "POST",
headers: {
"X-API-Key": API_KEY,
"X-On-Behalf-Of": "user_123",
"Content-Type": "application/json",
},
body: JSON.stringify({
conversation_key: "conv_abc",
user_message: { role: "user", content: "Hello" },
}),
}
);
const msgData = await msgResponse.json();
console.log(msgData);
Error Responses and Troubleshooting
When authentication fails, the API returns an error with a denial_reason field indicating the cause.
Authentication Errors
| Error | HTTP Status | Cause | Fix |
|---|---|---|---|
missing_credentials | 401 | No API key or JWT in request | Add X-API-Key header |
api_key_not_found | 401 | Key doesn't exist | Verify the key value is correct |
api_key_expired | 401 | Key has passed its expiration date | Create a new key |
api_key_disabled | 401 | Key is disabled | Contact your admin to re-enable |
api_key_revoked | 401 | Key has been revoked | Create a new key — revocation is permanent |
api_key_invalid | 401 | Generic invalid key | Verify the key format (sk_* or pk_*) |
insufficient_scope | 403 | Key lacks the required scope | Update the key's permissions or create a new key with the needed scope |
rate_limited | 429 | Key hit its rate limit | Wait for the reset window or increase the rate limit |
Publishable Key Errors
| Error | HTTP Status | Cause | Fix |
|---|---|---|---|
publishable_key_requires_jwt | 401 | pk_* key used without a user JWT | Add Authorization: Bearer <jwt> header |
jwt_expired | 401 | User JWT has expired | Refresh the JWT token |
jwt_malformed | 401 | JWT is malformed | Verify the JWT structure |
jwt_invalid_signature | 401 | JWT signature verification failed | Ensure the JWT was issued by the correct provider |
jwt_invalid_issuer | 401 | JWT issuer doesn't match expected value | Check the OIDC issuer config on the publishable key |
jwt_invalid_audience | 401 | JWT audience doesn't match | Check the OIDC audience config on the publishable key |
user_id_claim_not_found | 401 | JWT missing the user ID claim | Ensure your JWT includes the sub claim (or the configured userIdClaim) |
Server Errors
| Error | HTTP Status | Cause | Fix |
|---|---|---|---|
unkey_error | 500 | Internal key verification error | Retry the request; if persistent, contact support |
misconfigured_publishable_key | 500 | Publishable key missing OIDC metadata | Recreate the key with publishableConfig.oidc |
oidc_not_configured | 500 | OIDC validator not initialized | Contact platform support |
Quick Reference
Headers
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Your API key (sk_* or pk_*) |
Content-Type | Yes | Always application/json |
Authorization | For pk_* keys | Bearer <user-jwt> — required with publishable keys |
X-On-Behalf-Of | Optional | User ID to impersonate (requires users:impersonate scope) |
Request Format
All API endpoints are accessed through the KrakenD API gateway:
POST /api/v1/<domain>/<service>/<method>
Content-Type: application/json
Request and response bodies use protojson encoding (JSON representation of Protocol Buffer messages).