Standard CRUD works well for one resource at a time. But there are three tasks that need a separate approach: create a hundred records at once, wait for a long-running background job, and show an error in the user's language. Let's cover each.
First, all three cases on one diagram: what goes out in the request, what comes back in the response, and what the client does with it.
The HTTP status covers the request, not the fate of each item: 200 OK with one ERROR inside is a normal response. Long-running work moves behind 202 and status polling, and translation touches only detail — code stays machine-readable.
Batch operations
Imagine a client wants to create 50 orders in a single request. You could send 50 separate POSTs — but that's 50 HTTP requests, 50 rounds of overhead, and the client has to wait for each one. A batch operation lets you pass all items in one request.
What the request looks like
A batch endpoint follows the pattern POST /resources/batch or POST /resources/batch/<action>:
live example
POST /api/v1/orders/batch
Content-Type: application/json
{
"items": [
{ "productId": "aaa", "quantity": 2 },
{ "productId": "bbb", "quantity": 1 },
{ "productId": "ccc", "quantity": 5 }
]
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Partial success is not a failure of the whole operation
The key idea: if one of the items fails, the rest are processed as usual. This is called partial success.
The server returns 200 OK with a per-item result:
{
"results": [
{ "index": 0, "status": "SUCCESS", "orderId": "..." },
{ "index": 1, "status": "ERROR", "error": { "code": "INSUFFICIENT_STOCK", "detail": "Product bbb is out of stock" } },
{ "index": 2, "status": "SUCCESS", "orderId": "..." }
],
"summary": {
"total": 3,
"succeeded": 2,
"failed": 1
}
}
What matters here:
200 OKeven on a partial failure — this is not a request failure, but a normal response with results.indexshows the item's position in the original array (zero-based).status—SUCCESSorERRORfor each item.- On error — an
errorobject with a code and a detail. This is not a full ProblemDetails, because the error concerns a specific item rather than the whole request. summary— the final counters: total, succeeded, failed.
When you need atomicity
Partial success is the default behavior. Sometimes you need the opposite: all or nothing. This is called atomicity (all-or-nothing). If a service supports this mode, it's stated explicitly in the documentation:
"All items are processed in a single transaction. A failure of any item rolls back all of them. On a partial failure, a 400 is returned with the indexes of the failed items."
Without such a note, the client should expect partial success.
Size limit
Accepting an unlimited number of items is dangerous — it's a load on the server and a long response time. That's why the maximum batch size is stated in the documentation (for example, no more than 100 items).
If the client exceeds the limit, the server returns:
HTTP/1.1 400 Bad Request
{
"type": "urn:problem:order-service:batch-size-exceeded",
"status": 400,
"title": "Bad Request",
"detail": "Request size exceeds the maximum (100 items)",
"code": "BATCH_SIZE_EXCEEDED"
}
Async operations
Some operations can't finish within the time of an HTTP request. Generating a yearly report may take 30 seconds, a bulk mailing — several minutes. Holding the connection open that long is a bad idea: the network may drop, and the client's timeout will expire.
The solution: the server accepts the task right away, returns a response, and does the processing in the background. The client checks the status periodically — this is called polling.
Step 1: submit the task
POST /api/v1/reports/generate
Content-Type: application/json
{ "dateFrom": "2026-01-01", "dateTo": "2026-12-31" }
The server replies 202 Accepted — the request has been accepted but not yet completed:
HTTP/1.1 202 Accepted
Location: /api/v1/tasks/550e8400-...
{
"taskId": "550e8400-...",
"status": "PENDING",
"createdAt": "2026-05-26T10:30:00Z",
"statusUrl": "/api/v1/tasks/550e8400-..."
}
Locationin the header — the address where the status can be checked.statusUrlin the body — the same thing, for clients that don't read response headers.taskId— the task identifier.
Step 2: poll the status
The client periodically does GET /api/v1/tasks/{id}. While the task is running:
{
"taskId": "550e8400-...",
"status": "PROCESSING",
"progress": 45,
"createdAt": "2026-05-26T10:30:00Z"
}
When the task has completed successfully, a link to the result appears:
{
"taskId": "550e8400-...",
"status": "COMPLETED",
"progress": 100,
"createdAt": "2026-05-26T10:30:00Z",
"completedAt": "2026-05-26T10:35:00Z",
"resultUrl": "/api/v1/reports/550e8400-..."
}
If the task failed, a description of the problem arrives:
{
"taskId": "550e8400-...",
"status": "FAILED",
"createdAt": "2026-05-26T10:30:00Z",
"completedAt": "2026-05-26T10:32:00Z",
"error": {
"code": "REPORT_GENERATION_FAILED",
"detail": "Failed to build the report: no data for the period"
}
}
Task statuses
A task goes through four states:
| Status | What it means |
|---|---|
PENDING | created, waiting in the queue |
PROCESSING | running right now |
COMPLETED | finished; resultUrl is required |
FAILED | finished with an error; error is required |
How often to poll is up to the client. Usually once every 1-5 seconds for short tasks, once every 30-60 seconds for long ones. The server can suggest an interval via the Retry-After header.
Localizing error messages
Users see error messages — and they want to see them in their own language. The client indicates its preferred language via the Accept-Language header:
live example
GET /api/v1/orders/123
Accept-Language: ru
GET /api/v1/orders/123
Accept-Language: en
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
If the header is not provided, the server uses the default language (typically Russian).
What exactly gets localized
Two fields in an error response get localized:
detailin ProblemDetails — the human-readable description of the error.messageinviolations— the description next to a specific form field.
// Accept-Language: ru
{
"code": "ORDER_NOT_FOUND",
"detail": "Заказ не найден"
}
// Accept-Language: en
{
"code": "ORDER_NOT_FOUND",
"detail": "Order not found"
}
What must not be localized
Some parts of the response deliberately stay in English:
code— the machine error code. Client code doesswitch (error.code)and must not depend on the user's language. Correct:ORDER_EMPTY; wrong:ЗАКАЗ_ПУСТОЙ.title— the standard HTTP status name:Bad Request,Not Found. Always in English.type— a URI or URN, a technical identifier. Always in English.- JSON field names —
orderId, notидЗаказа. The JSON structure is the same for all languages.
The reason is simple: these fields are used by program code, not by people. Localizing them means breaking the clients that rely on them.
In short
- A batch operation accepts a list of
itemsin one request and returns200 OKwith a per-item result. - The default is partial success: one item's failure doesn't cancel the rest.
- Atomicity (all or nothing) requires an explicit note in the documentation.
- Exceeding the size limit —
400 BATCH_SIZE_EXCEEDED. - A long-running operation returns
202 AcceptedwithLocationandtaskId; the client polls the status via GET. - Task statuses:
PENDING→PROCESSING→COMPLETED(withresultUrl) orFAILED(witherror). - Only
detailandviolations.messageare localized — via theAccept-Languageheader. - Error codes, HTTP headers, and JSON field names stay in English.
What to read next
- Errors in REST API: ProblemDetails and codes — how
code,detail, andtypeare structured. - Request and response headers —
Idempotency-Keyfor batch operations,Locationfor async ones. - Limits, files, and versioning — adjacent topics.