Making Requests¶
Using the CLI?
The IPTO CLI handles authentication headers, pagination, and error formatting automatically. Most examples in this guide are for direct HTTP usage. See the CLI Commands for the simpler CLI equivalents.
This page covers the conventions you need to know for every API call: base URL, headers, request and response formats, pagination, error handling, and rate limiting.
Base URL¶
All API requests are made to:
All endpoints are versioned under the /v1 prefix. For example, the datasets endpoint is:
Required Headers¶
| Header | Value | Required | Notes |
|---|---|---|---|
Content-Type | application/json | Yes (for request bodies) | All request bodies must be JSON |
Authorization | Bearer {session_token} | Yes (if using session auth) | See Authentication |
X-API-Key | ipto_{prefix}_{secret} | Yes (if using API key auth) | See Authentication |
Idempotency-Key | Any unique string | Optional | Recommended for all POST requests |
Idempotency
Include an Idempotency-Key header on POST requests to safely retry without creating duplicate resources. The server deduplicates requests with the same key and body. Reusing a key with a different body returns 409 Conflict.
Request Format¶
All request bodies use UTF-8 JSON. Timestamps should be ISO 8601 UTC strings (e.g., 2026-04-05T10:00:00Z).
const res = await fetch("https://api.ipto.ai/v1/datasets", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
name: "Q4 Invoices",
description: "Scanned invoice PDFs from Q4 2025",
source_modality: "document",
}),
});
Response Format¶
Successful Responses¶
Successful responses return a flat JSON object with the resource fields at the top level:
{
"dataset_id": "dset_abc123",
"name": "Q4 Invoices",
"status": "active",
"created_at": "2026-04-05T10:00:00Z"
}
List endpoints return an array of objects along with pagination metadata:
{
"items": [
{ "dataset_id": "dset_abc123", "name": "Q4 Invoices", "status": "active" },
{ "dataset_id": "dset_def456", "name": "HR Policies", "status": "active" }
],
"next_cursor": "eyJpZCI6MTIzfQ==",
"has_more": true
}
Error Responses¶
Errors return a structured envelope with a machine-readable code and a human-readable message:
Some errors include additional context in a details object:
{
"error": {
"code": "invalid_request",
"message": "Validation failed for 2 fields.",
"details": {
"fields": [
{ "field": "name", "reason": "required" },
{ "field": "source_modality", "reason": "must be one of: document, image, audio, video" }
]
}
}
}
HTTP Status Codes¶
| Status | Meaning | When Returned |
|---|---|---|
200 OK | Success | Reads and synchronous updates |
201 Created | Resource created | Successful POST that creates a resource |
400 Bad Request | Malformed input | Missing fields, invalid JSON, bad parameter types |
401 Unauthorized | Invalid or missing credentials | Expired token, wrong API key, missing auth header |
403 Forbidden | Insufficient permissions | Valid credentials but lacking the required scope or tenant access |
404 Not Found | Resource not found | Invalid ID or resource belongs to another tenant |
409 Conflict | State conflict | Idempotency key reused with a different body, or conflicting state transition |
422 Unprocessable Entity | Semantically invalid | Syntactically valid JSON but logically inconsistent (e.g., invalid enum value) |
429 Too Many Requests | Rate limited | Too many requests in the current window |
500 Internal Server Error | Server error | Unexpected failure; safe to retry |
503 Service Unavailable | Temporarily unavailable | The service is under maintenance or overloaded; retry after the indicated delay |
Pagination¶
List endpoints use cursor-based pagination. Pass cursor and limit as query parameters.
| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | (none) | Opaque cursor from a previous response's next_cursor field. Omit for the first page. |
limit | integer | 20 | Number of items per page. Maximum: 100. |
Paginating Through Results¶
import requests
cursor = None
all_datasets = []
while True:
params = {"limit": 10}
if cursor:
params["cursor"] = cursor
resp = requests.get(
"https://api.ipto.ai/v1/datasets",
headers={"Authorization": f"Bearer {token}"},
params=params,
)
page = resp.json()
all_datasets.extend(page["items"])
if not page.get("has_more"):
break
cursor = page["next_cursor"]
let cursor: string | undefined;
const allDatasets: any[] = [];
while (true) {
const params = new URLSearchParams({ limit: "10" });
if (cursor) params.set("cursor", cursor);
const res = await fetch(
`https://api.ipto.ai/v1/datasets?${params}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const page = await res.json();
allDatasets.push(...page.items);
if (!page.has_more) break;
cursor = page.next_cursor;
}
Cursor stability
Cursors are opaque strings and are valid only for a limited time. Do not store cursors for long-term use. If a cursor expires, restart pagination from the first page.
Rate Limiting¶
The API enforces per-tenant rate limits to ensure fair usage. When you exceed the limit, requests return 429 Too Many Requests.
The response includes headers to help you manage your request rate:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | UTC epoch timestamp when the window resets |
Retry-After | Seconds to wait before retrying (included with 429 responses) |
Handling rate limits
Implement exponential backoff when you receive a 429 response. Read the Retry-After header to know the minimum wait time before your next request.
FAQ¶
Do I need to set Content-Type for GET requests?
No. The Content-Type: application/json header is only required for requests that include a JSON body (POST, PUT, PATCH). GET and DELETE requests do not need it.
What format are timestamps in?
All timestamps in requests and responses use ISO 8601 format in UTC, for example: 2026-04-05T10:00:00Z.
How should I handle 500 errors?
500 errors indicate an unexpected server-side failure. It is safe to retry the request. Use exponential backoff (e.g., wait 1s, 2s, 4s between retries) and include an Idempotency-Key on POST requests to avoid creating duplicates.
Is there a maximum request body size?
JSON request bodies are limited to 1 MB. File uploads bypass this limit because they are sent directly to presigned storage URLs, not to the API server.
Can I use query parameters instead of a JSON body for POST requests?
No. All POST, PUT, and PATCH endpoints accept data exclusively through a JSON request body. Query parameters are used only for filtering and pagination on GET endpoints.