Skip to content

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:

https://api.ipto.ai

All endpoints are versioned under the /v1 prefix. For example, the datasets endpoint is:

https://api.ipto.ai/v1/datasets

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).

curl -X POST https://api.ipto.ai/v1/datasets \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Q4 Invoices",
    "description": "Scanned invoice PDFs from Q4 2025",
    "source_modality": "document"
  }'
import uuid
import requests

resp = requests.post(
    "https://api.ipto.ai/v1/datasets",
    headers={
        "Authorization": f"Bearer {token}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "name": "Q4 Invoices",
        "description": "Scanned invoice PDFs from Q4 2025",
        "source_modality": "document",
    },
)
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:

{
  "error": {
    "code": "invalid_request",
    "message": "The field 'name' is required."
  }
}

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

# First page
curl "https://api.ipto.ai/v1/datasets?limit=10" \
  -H "Authorization: Bearer $TOKEN"

# Next page (use next_cursor from previous response)
curl "https://api.ipto.ai/v1/datasets?limit=10&cursor=eyJpZCI6MTIzfQ==" \
  -H "Authorization: Bearer $TOKEN"
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.