Conventions
Response envelopes, cursor pagination, errors, rate limits and versioning.
Success envelopes
A single resource:
{ "data": { "id": "...", "...": "..." }, "message": "Retrieved successfully" }A collection, which is cursor-paginated:
{
"data": [],
"links": { "first": null, "last": null, "prev": null, "next": "https://...?cursor=..." },
"meta": { "path": "https://...", "per_page": 20, "next_cursor": "...", "prev_cursor": null },
"message": "Retrieved successfully"
}message is a human-readable string, not a contract. Do not branch on it.
Pagination
Lists are cursor-based, not page-number based. links.first and links.last are always null and there is no total, so you cannot render "page 3 of 12" from the response. Walk forward by following links.next until it is null.
| Parameter | Default | Notes |
|---|---|---|
limit | 20 | Maximum 100. A larger value is rejected, never clamped |
cursor | none | Opaque value from meta.next_cursor |
sort | desc | asc or desc |
sortBy | id | Use only documented column names |
s or search | none | Free-text search on endpoints that support it |
GET https://api.printcart.com/v1/products?limit=10A limit above 100 returns 400 on almost every list endpoint, with the standard validation body. GET /design-templates and GET /side-templates answer 413 instead. The simplest way to handle both is never to send a limit above 100.
Errors
Branch on the HTTP status, not on the body shape and not on the message. Several body shapes are in use, depending on the layer that rejected the request:
| Shape | Typical source |
|---|---|
{"error":{"message":"..."}} | store-key authentication, domain errors |
{"message":"..."} | public-token and JWT authentication, not found |
{"error":{"<field>":["..."]},"message":"Invalid input"} | request validation |
| Status | Meaning |
|---|---|
| 400 | Validation failed (not 422), a plan limit was reached, or a domain rule such as a duplicate product |
| 401 | Missing or invalid credential |
| 402 | Plan capability not available, media storage cap reached, or an empty token wallet |
| 403 | Authenticated but not permitted |
| 404 | Not found. Another store's resource also answers 404, so existence is never leaked |
| 409 | Conflict, for example a duplicate side-template import |
| 413 | limit above 100 on /design-templates and /side-templates |
| 422 | A few endpoints that validate inline rather than through a request class |
| 429 | Rate limited |
| 5xx | Internal error |
Most errors carry no machine-readable code. storage_quota_exceeded and capability_not_available (both under error.code) and insufficient_tokens (top-level code) are the exceptions. Write clients that tolerate both a string error and an object error.
Rate limits
The general limit is 1000 requests per minute, keyed by the authenticated principal and falling back to the client IP. It is not tiered by plan and it is not per endpoint. X-RateLimit-Limit and X-RateLimit-Remaining are on every response; Retry-After and X-RateLimit-Reset appear on 429 responses.
The AI Services and Studio AI endpoints have their own, lower limit. If your workload is bursty, such as a bulk import or a nightly reconciliation, pace it yourself.
Idempotency, request IDs and versions
Idempotency-Keyis not implemented. Retrying aPOSTcreates a second resource, so make your writes idempotent on your side.PUT /side-templates/{sideTemplate}/productsaccepts anidempotency_keyfield in the JSON body to de-duplicate asset contributions.X-Request-Idis not implemented. To raise a support request, note the time, your storesidand the endpoint.Accept-Versionis not implemented.v1is the only version, and new response fields are additive.