Multi-item requesting API

Multi-item requesting API

Context

This document outlines the API changes for interaction between OPAC/Locate and FOLIO for multi-item requesting, including enhancements to existing endpoints as well as the introduction of new ones.

1 Working with allowed-service-points

When requesting a title or item, it is important to determine the service points that are allowed for the requested material and from which the patron selects the appropriate pickup service point. To support this behavior for single requests, the module edge-patron currently provides the following endpoints (as well as their modifications for secure patrons):

  • GET /patron/account/{id}/item/{itemId}/allowed-service-points for item requesting,

  • GET /patron/account/{id}/instance/{instanceId}/allowed-service-points for title requesting.

The response looks similar to this example:

{"allowedServicePoints":[{"id":"be6cdc85-1202-4c48-99fa-56af591f5a3a","name":"Application"},{"id":"650b2d28-dc05-4177-9dc3-c45316fe5aa6","name":"Main circ college"}]}

For multi-item requests, it should also be possible to determine the allowed service points. According to the UX mockups, this should happen after selecting the items and pressing the “Add to list” button. This required outcome can be implemented in one of the following ways. It is proposed to implement this as follows: OPAC/Locate makes a single call, providing the list of selected item IDs; FOLIO replies with a JSON-format response with allowed service points information. Such a single call between OPAC/Locate and FOLIO minimizes the resource consumption.

Note that this can be implemented using either the GET or POST HTTP method. Assuming all item IDs (in the form of a UUID) are passed in the query string of the GET method, this introduces a limit on the number of item IDs in one request. The fact is that the query string size on FOLIO is limited to 8,000 characters (TBC), which for UUIDs allows approximately 50-60 elements. To implement a generic API, we should strive to avoid introducing this kind of technical limitation. So, we proceed with the POST method for this scenario.

Add a new endpoint with an array of all requested items:

  • POST /patron/account/{id}/instance/{instanceId}/allowed-service-points-multi-item

Request body:

{"itemIds":["650b2d28-dc05-4177-9dc3-c45316fe5aa6","9631dd19-63e4-570d-a50c-de0665de3b44"]}

The structure of the Response body is quite similar - we make pairs of “item - list of service points” and collect these pairs into one array. Note that it’s still the responsibility of OPAC/Locate to process this information in any suitable way.

{ "allowedServicePointsPerItem": [ { "itemId": "7e881333-3a3a-501c-a6d4-03b81e3c7e79", "allowedServicePoints": [ { "id": "be6cdc85-1202-4c48-99fa-56af591f5a3a", "name": "One good service point" }, { "id": "650b2d28-dc05-4177-9dc3-c45316fe5aa6", "name": "Main circ college" } ] }, { "itemId": "9631dd19-63e4-570d-a50c-de0665de3b44", "allowedServicePoints": [ { "id": "f297326f-60ea-5d8c-8e8f-9b0179421d10", "name": "Another good service point" }, { "id": "650b2d28-dc05-4177-9dc3-c45316fe5aa6", "name": "Main circ college" } ] } ] }

Other considered options:

  • All efforts on the OPAC side - Minimum effort, but slow and taxing on the system. OPAC/Locate calls the existing edge-patron’s endpoint for each item separately, aggregates all the information, and then displays it on the UI.

  • FOLIO groups a-s-p lists on its side - This appears to be a customization for Locate's current design and UX. Same endpoint as the proposed approach, but here FOLIO tries to figure out for itself how to group service points.

2 Creating a batch multi-item request

Creates a new batch request and returns a batchRequestId value. The batchRequestId value can be provided by the client or generated by the FOLIO.

A new endpoint must be created (in 2 modifications):

  • POST /account/{id}/instance/{id}/batch-request (modification for regular patrons),

  • POST /account/instance/{id}/batch-request (modification for secure patrons)

Request body:

{ "batchRequestId": uuid | null, // optional parameter "requests": [ { "itemId": uuid, // required "pickUpLocationId": uuid // required } ], "patronComments": "Some patron comment" }

200 OK Response body is returned if FOLIO has accepted the request and successfully persisted it for further processing:

{ "batchId": "5203c035-005e-4a70-b555-ddaa3094c51c", "requesterId": "0e46eba6-daa0-4ab8-8d89-968657e13540" "mediatedRequestStatus": "In Progress", "itemRequestsStats": { "total": 100, "pending": 2, "inProgress": 6, "completed": 90, "failed": 2 }, "metadata": { "createdByUserId": "{{user-uuid}} | null", "createdDate": "{{offsetDateTime}}", "updatedByUserId": "{{user-uuid}} | null", "updatedDate": "{{offsetDateTime}}" } }

If the request could not be parsed, saved, etc., the corresponding HTTP code should be returned.

When creating a single request, the response also contains information about the item (instance ID, item ID, title, and author), and also duplicates Patron notes - should this behavior be repeated for a batch request?

{ "requestId": "5203c035-005e-4a70-b555-ddaa3094c51c", "item": { "instanceId": "52623310-7766-57dc-8e3b-65d764ac5e5a", "itemId" : "9631dd19-63e4-570d-a50c-de0665de3b44", "title": "Globalization of water: sharing the planet's freshwater resources / by Arjen Y. Hoekstra and Ashok K. Chapagain.", "author" : "Hoekstra, Arjen Y., 1967-; Chapagain, Ashok K" }, "requestDate": "2025-09-08T12:27:33.822+00:00", "status": "Open - Not yet filled", "pickupLocationId": "650b2d28-dc05-4177-9dc3-c45316fe5aa6", "queuePosition": 1, "patronComments": "Some patron comment" }

3 Retrieving the status and summary of a batch request

Returns the processing status of the batch request:

  • GET /account/{id}/instance/{id}/batch-request/{batchRequestId}/status (modification for regular patrons),

  • GET /account/instance/{id}/batch-request/{batchRequestId}/status (modification for secure patrons)

The response has almost the same structure as when a batch request was successfully created, with the key difference that it also contains itemsFailedDetails and itemsPendingDetails sections. The first of them provides extended information about the items for which the attempt to create an individual circulation request failed (including information about the item, plus the reason/description of the problem that arose when making the request). At the same time, another section contains information about items that are pending. This should allow OPAC/Locate to visualize both failed and pending items in the patron UI form(s).

See the response example below:

{ "batchRequestId": "5203c035-005e-4a70-b555-ddaa3094c51c", "status": "In Progress | Completed", "submittedAt": "2025-09-08T12:27:33.822+00:00", "completedAt": null, "itemsTotal": 100, "itemsRequested": 90, "itemsPending": 7, "itemsPendingDetails": [ { "instanceId": "9d1b77e4-f02e-4b7f-b296-3f2042ddac54", "itemId": "0faa1eec-bdef-4d5e-a458-bceca8e04855", "title": "100 banned books: censorship histories of world literature / Nicholas J. Karolides, Margaret Bald, and Dawn B. Sova; introduction by Ken Wachsberger.", "pickupLocationId": "650b2d28-dc05-4177-9dc3-c45316fe5aa6" }, ... ] }, "itemsFailed": 2, "itemsFailedDetails": [ { "instanceId": "9d1b77e4-f02e-4b7f-b296-3f2042ddac54", "itemId": "0738aade-23ce-4177-8c6e-f5f13572c827", "title": "100 banned books: censorship histories of world literature / Nicholas J. Karolides, Margaret Bald, and Dawn B. Sova; introduction by Ken Wachsberger.", "pickupLocationId": "650b2d28-dc05-4177-9dc3-c45316fe5aa6", "errorCode": "This failed because of some internal issue", "errorDetails": "This failed because of some internal issue" }, { "instanceId": "9d1b77e4-f02e-4b7f-b296-3f2042ddac54", "itemId": "0faa1eec-bdef-4d5e-a458-bceca8e04855", "title": "Something else, pt.2", "pickupLocationId": "650b2d28-dc05-4177-9dc3-c45316fe5aa6", "errorCode": "This failed because of some internal issue", "errorDetails": "Everything failed here, and no one knows why" } ], "itemsRequestedDetails": [ { "instanceId": "9d1b77e4-f02e-4b7f-b296-3f2042ddac54", "itemId": "0faa1eec-bdef-4d5e-a458-bceca8e04855", "title": "100 banned books: censorship histories of world literature / Nicholas J. Karolides, Margaret Bald, and Dawn B. Sova; introduction by Ken Wachsberger.", "requested": "650b2d28-dc05-4177-9dc3-c45316fe5aa6", "pickupLocationId": "650b2d28-dc05-4177-9dc3-c45316fe5aa6" }, ... ] } }

4 Getting all information about a batch and related requests

This part of the API is important so that clients can display information about multi-item requests on their side in an efficient manner. On one hand, we need a generic enough API to be used by different OPAC systems. On the other hand, Locate's UX mockups are the only thing we have right now.

Note: In its current functionality, the module mod-circulation provides a list of circulation requests created by a patron. This list will also, by default, include individual requests created for items from a multi-item batch. However - and this is important - 1) it is impossible to distinguish between single circulation requests and individual circulation requests, and 2) the list does not contain information about failed request creation attempts.

The new functionality solves this problem by enhancing the existing patron account API:

  1. Adding an optional section batchRequestInfo to those individual circulation requests that are created from a batch,

  2. Adding an optional section of batches to the response containing summaries for all batch IDs that appear in the request list.

Thus, the OPAC/Locate client needs to call one of the following Patron Services API endpoints by specifying a includeBatches=true parameter:

  • GET /patron/account/{id} (modification for regular patrons),

  • GET /patron/account (modification for secure patrons)

Below is an example of a response from FOLIO, containing both the familiar charges and loans sections, as well as the familiar holds section, in which some requests have a batchRequestInfo property, along with the new batches section. The batchRequestId makes it easy to match the request to the batch.

{ "isActive": true, "totalCharges": { "amount": 400, "isoCurrencyCode": "USD" }, "totalChargesCount": 1, "totalLoans": 1, "totalHolds": 1, "charges": [ { "item": { "instanceId": "6e024cd5-c19a-4fe0-a2cd-64ce5814c694", "itemId": "7d9dfe70-0158-489d-a7ed-2789eac277b3", "title": "Some Book About Something", "author": "Some Guy; Another Guy" }, "chargeAmount": { "amount": 50.0, "isoCurrencyCode": "USD" }, "accrualDate": "2018-01-31T00:00:01Z", "state": "Paid Partially", "reason": "damage - rebinding", "feeFineId" : "881c628b-e1c4-4711-b9d7-090af40f6a8f" } ], "loans": [ { "id": "9a171a89-baca-4f1a-b2c4-d7253854864e", "item": { "instanceId": "6e024cd5-c19a-4fe0-a2cd-64ce5814c694", "itemId": "7d9dfe70-0158-489d-a7ed-2789eac277b3", "title": "Some Book About Something", "author": "Some Guy; Another Guy" }, "loanDate": "2018-06-01T11:12:00Z", "dueDate": "2525-01-01T11:12:00Z", "overdue": false } ], "holds": [ // single request { "requestId": "8bbac557-d66f-4571-bbbf-47a107cc1589", "item": { "instanceId": "255f82f3-5b1b-4239-93e4-ec6acf03ad9d", "itemId": "26670295-716a-4f84-8f65-2ef31707c017", "title": "I Want to Hold Your Hand", "author": "John Lennon; Paul McCartney" }, "requestDate": "2018-06-02T08:16:30Z", "pickupLocationId": "ebab9ccc-4ece-4f35-bc82-01f3325abed8", "status": "Open - Not yet filled" }, // individual circulation request created from a batch { "requestId": "5678ea81-7388-4f0a-9a90-b98296403c22", "item": { "instanceId": "9d1b77e4-f02e-4b7f-b296-3f2042ddac54", "itemId": "0faa1eec-bdef-4d5e-a458-bceca8e04855", "title": "Domodots. devised by Carol Ann Alspaugh.", "virtual": true, "sourceTypes": [ "Other -- unknown" ] }, "requestDate": "2025-05-21T20:20:33.167+01:00", "status": "Open - Not yet filled", "pickupLocationId": "650b2d28-dc05-4177-9dc3-c45316fe5aa6", "queuePosition": 4 }, "batchRequestInfo": { "batchRequestId": "5521ee94-d4d0-40bc-9e0f-984acb2234b8", -- this ID would be the same for all individual requests of the same batch "batchRequestSubmittedAt": "2025-05-22T12:01:31.535+01:00" -- this would be the same for all individual requests of the same batch } } // new section "batches": [ { "batchRequestId": "5521ee94-d4d0-40bc-9e0f-984acb2234b8", - one batch status info for every unique batch ID from the holds list "status": "In Progress | Completed", "submittedAt": "2025-09-08T12:27:33.822+00:00", "completedAt": null, "itemsTotal": 100, "itemsRequested": 90, "itemsPending": 7, "itemsPendingDetails": [ { "instanceId": "9d1b77e4-f02e-4b7f-b296-3f2042ddac54", "itemId": "0faa1eec-bdef-4d5e-a458-bceca8e04855", "title": "100 banned books: censorship histories of world literature / Nicholas J. Karolides, Margaret Bald, and Dawn B. Sova; introduction by Ken Wachsberger." }, ... ] }, "itemsFailed": 2, "itemsFailedDetails": [ { "instanceId": "9d1b77e4-f02e-4b7f-b296-3f2042ddac54", "itemId": "0738aade-23ce-4177-8c6e-f5f13572c827", "title": "100 banned books: censorship histories of world literature / Nicholas J. Karolides, Margaret Bald, and Dawn B. Sova; introduction by Ken Wachsberger.", "errorCode": "This failed because of some internal issue", "errorDetails": "This failed because of some internal issue" }, { "instanceId": "9d1b77e4-f02e-4b7f-b296-3f2042ddac54", "itemId": "0faa1eec-bdef-4d5e-a458-bceca8e04855", "title": "Something else, pt.2", "errorCode": "This failed because of some internal issue", "errorDetails": "Everything failed here, and no one knows why" } ] } ] }

FOLIO backend hints

Below is a visualization of the FOLIO backend modules that will be used to process allowed service points and request list requests. Requests to create a multi-item request and retrieve its status will ultimately be processed in the mod-requests-mediated module.