Best Practices

Build reliable, efficient integrations with the Creor API. This guide covers idempotency, error handling, pagination, streaming, and webhook patterns.

Idempotency Keys

Network failures, timeouts, and retries can cause a request to be sent more than once. Idempotency keys let you safely retry requests without duplicating the operation. The server stores the result of the first request and returns it for any subsequent request with the same key.

How to Use Idempotency Keys

Pass a unique string in the Idempotency-Key header. Use a UUID v4 or another value that uniquely identifies the logical operation on the client side.

curl https://api.creor.ai/v1/agents \
  -u YOUR_API_KEY: \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "name": "bugfix-agent",
    "repository": "org/repo",
    "prompt": "Fix the failing CI test in src/auth.ts"
  }'
BehaviorDescription
First requestProcessed normally. The result is cached against the idempotency key for 24 hours.
Duplicate request (same key)Returns the cached result from the first request without re-executing the operation.
Same key, different bodyReturns a 409 Conflict error. A key must always be paired with the same request body.
Key expirationCached results are purged after 24 hours. After that, the key can be reused.

Tip

Generate the idempotency key on the client before the first attempt and reuse it across retries. Do not generate a new key for each retry -- that defeats the purpose.

Which Endpoints Support Idempotency

  • POST /v1/agents -- launch a new agent
  • POST /v1/agents/:id/followup -- add a follow-up message
  • POST /v1/chat/completions -- chat completion (non-streaming only)

GET, PUT, and DELETE requests are naturally idempotent and do not require an idempotency key.

Error Handling

The Creor API uses standard HTTP status codes and returns structured JSON error bodies. Every error response follows the same shape, making it straightforward to handle errors consistently in your client code.

Error Response Format

{
  "error": {
    "type": "invalid_request",
    "message": "The 'model' field is required.",
    "param": "model",
    "code": "missing_field"
  }
}
Status CodeTypeDescription
400invalid_requestThe request body is malformed or missing required fields.
401authentication_errorThe API key is missing, invalid, or expired.
403permission_deniedThe API key does not have permission for this operation.
404not_foundThe requested resource does not exist.
409conflictIdempotency key conflict or resource state conflict.
422unprocessable_entityThe request is well-formed but semantically invalid (e.g., unsupported model).
429rate_limit_exceededYou have exceeded your rate limit. See the Rate Limits page.
500internal_errorAn unexpected server error. Safe to retry with exponential backoff.
503service_unavailableThe service is temporarily unavailable. Retry after the Retry-After header value.

Handling Errors in Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
async function creorRequest(path: string, body: Record<string, unknown>) {
const response = await fetch(`https://api.creor.ai${path}`, {
method: "POST",
headers: {
"Authorization": `Basic ${btoa(API_KEY + ":")}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
 
if (!response.ok) {
const { error } = await response.json();
 
switch (response.status) {
case 401:
throw new Error("Invalid API key. Check your credentials.");
case 429:
const retryAfter = response.headers.get("Retry-After");
throw new Error(`Rate limited. Retry after ${retryAfter}s.`);
case 500:
case 503:
// Safe to retry with backoff
throw new Error(`Server error: ${error.message}`);
default:
throw new Error(`API error [${error.type}]: ${error.message}`);
}
}
 
return response.json();
}

Note

Always check the error.type field rather than parsing the message string. Error messages may change over time, but types are stable.

Pagination

List endpoints return paginated results using cursor-based pagination. This approach is more reliable than offset-based pagination because it handles insertions and deletions between pages correctly.

Request Parameters

ParameterTypeDefaultDescription
limitinteger20Number of items per page. Maximum 100.
cursorstringnullCursor from the previous response to fetch the next page.
orderstringdescSort order: "asc" or "desc" by creation time.

Response Format

{
  "data": [
    { "id": "agent_abc123", "name": "bugfix-agent", "status": "completed" },
    { "id": "agent_def456", "name": "review-agent", "status": "running" }
  ],
  "has_more": true,
  "next_cursor": "eyJpZCI6ImFnZW50X2RlZjQ1NiJ9"
}

Iterating Through All Pages

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function fetchAllAgents(apiKey: string) {
const agents = [];
let cursor: string | undefined;
 
do {
const params = new URLSearchParams({ limit: "50" });
if (cursor) params.set("cursor", cursor);
 
const response = await fetch(
`https://api.creor.ai/v1/agents?${params}`,
{ headers: { Authorization: `Basic ${btoa(apiKey + ":")}` } }
);
 
const page = await response.json();
agents.push(...page.data);
cursor = page.has_more ? page.next_cursor : undefined;
} while (cursor);
 
return agents;
}

Tip

Use the largest reasonable limit value to minimize the number of requests. Fetching 100 items at a time is more efficient than fetching 20 at a time.

Streaming Responses

The /v1/chat/completions endpoint supports Server-Sent Events (SSE) for streaming responses. Streaming lets you display tokens to the user as they are generated instead of waiting for the full response.

Enabling Streaming

Set "stream": true in your request body. The response will be a text/event-stream instead of a JSON object.

curl https://api.creor.ai/v1/chat/completions \
  -u YOUR_API_KEY: \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4-20250514",
    "stream": true,
    "messages": [
      {"role": "user", "content": "Write a haiku about code reviews"}
    ]
  }'

Event Format

Each event is a JSON object prefixed with "data: ". The stream ends with a "data: [DONE]" sentinel. See the Gateway Streaming page for the full event schema and code examples.

Note

Streaming requests count as a single request against your rate limit, regardless of how many events are sent.

Webhook Reliability

Creor sends webhooks for agent lifecycle events (started, completed, failed) and usage alerts. Building a reliable webhook consumer requires handling retries, verifying signatures, and processing events idempotently.

Webhook Delivery

BehaviorDetails
TimeoutYour endpoint must respond with a 2xx status within 10 seconds.
RetriesFailed deliveries are retried up to 5 times with exponential backoff (1m, 5m, 30m, 2h, 12h).
OrderingEvents are delivered in approximate order but not guaranteed. Use the event timestamp to detect out-of-order delivery.
IdempotencyEach event includes a unique event_id. Store processed event IDs to avoid duplicate handling.

Verifying Signatures

Every webhook includes a Creor-Signature header containing an HMAC-SHA256 signature of the request body. Verify this signature to ensure the webhook was sent by Creor and not tampered with in transit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import crypto from "node:crypto";
 
function verifyWebhookSignature(
body: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
 
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
 
// In your webhook handler:
app.post("/webhooks/creor", (req, res) => {
const signature = req.headers["creor-signature"] as string;
const isValid = verifyWebhookSignature(
JSON.stringify(req.body),
signature,
process.env.CREOR_WEBHOOK_SECRET!
);
 
if (!isValid) {
return res.status(401).send("Invalid signature");
}
 
// Process the event
const { event_id, type, data } = req.body;
// ... handle event idempotently using event_id
res.status(200).send("OK");
});

Best Practices for Webhook Consumers

  • Respond with 200 immediately, then process the event asynchronously. This prevents timeouts.
  • Store the event_id and skip duplicates. Creor may deliver the same event more than once.
  • Use the event timestamp (not arrival time) for ordering logic.
  • Log the raw request body before processing for debugging.
  • Set up a dead-letter queue for events that fail processing after all retries.
  • Monitor your webhook endpoint's error rate and latency in your observability stack.

Warning

Never trust webhook data without verifying the signature. An unauthenticated webhook endpoint is a security vulnerability that could allow attackers to trigger actions in your system.