Filters and timing
Fetch only relevant and new tenders, page through large result sets and sync at the right interval.
You receive this service through an individual quote.
One run per hour keeps your system at the state of procuris. Filters determine which tenders your system fetches, for example only construction work in Bavaria and Thuringia. With updated_since, a run fetches only what is new or has changed since the previous run.
The data gets at most one new state per hour. More frequent requests pick up a new state earlier, but no additional states. lastModified shows when procuris took over a change, not when the contracting authority published it.
Complete example request
The example request combines five filters. It fetches construction work in Bavaria and Thuringia with a submission deadline after 1 October and a contract value from 100,000 euros, changed since 24 September, 100 per page:
GET /v1/tenders?cpv=45000000&nuts=DE2,DEG&deadline_after=2026-10-01T00:00:00Z&value_min=100000&updated_since=2026-09-24T00:00:00Z&limit=100 HTTP/1.1
Host: api.procuris.eu
Authorization: Bearer <your token>
Accept: application/jsonOnly one record matches here. That is why nextCursor is null:
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 42{
"data": [
{
"id": "e9da30eb-50e6-47f1-887f-434016267b2f",
"status": "open",
"title": "Nordhausen - Sanierung Rolandbrunnen, Erneuerung Brunnentechnik",
"submissionDate": "2026-10-14T08:00:00.000Z",
"contractAmount": {
"value": 351000,
"currency": "EUR",
"estimated": false
},
"lastModified": "2026-09-24T09:10:00.000Z"
}
],
"nextCursor": null
}The example shows only six fields. In the real response, an entry in data is a complete record as in the data model.
Filters
Filters are parameters on GET /v1/tenders. Different filters apply together (and), while several values in one filter apply as alternatives (or). cpv=45000000&nuts=DE2,DEG therefore means: construction work, in Bavaria or Thuringia.
| Parameter | Value | Example | Effect |
|---|---|---|---|
cpv | CPV codes, separated by commas | 45000000,71000000 | Sector. A code with zeros at the end includes all codes below it. 45000000 therefore covers all construction work. The CPV codes of the lots count too. |
nuts | NUTS codes, separated by commas | DE2,DEG | Region. A code includes the areas below it. DE2 stands for Bavaria, DEG for Thuringia. |
published_since | timestamp according to ISO 8601 | 2026-09-01T00:00:00Z | only notices published from this time on |
updated_since | timestamp according to ISO 8601 | 2026-09-24T00:00:00Z | only records whose lastModified is at or after this time |
deadline_after | timestamp according to ISO 8601 | 2026-10-01T00:00:00Z | only tenders whose submission deadline is after this time |
value_min, value_max | amount in euros excluding VAT, whole number | 100000 | contract value from, to. Estimated values count too. Amounts in other currencies drop out. |
procedure | eForms procedure type, separated by commas | open,restricted | only these procedure types |
archived | true or false | true | including archived tenders. Without it: excluding. Together with updated_since, the API skips archived without an error, because the sync then delivers archived records anyway. |
limit | number from 1 to 100 | 100 | records per page, 50 if not set |
Missing values. A filter on a field excludes tenders where that field is missing. With nuts, tenders without a region drop out, with deadline_after those without a submission deadline, with value_min or value_max those without a contract value. If you do not want to miss tenders without a region, fetch without nuts and assign the region in your system, for example using realizedLocation.
Fetching only new and changed records
updated_since fetches only changes since the previous run. Your system sets the parameter to the largest lastModified of the previous run. If a run returns no record, the stored state stays unchanged. Do not use your system's clock in that case, because it can differ from the time at procuris and your system would otherwise skip changes. With the stored state, it gets the changes since then, as far as they match the filters:
- new tenders that match the filters
- changed tenders, for example with a moved deadline or a correction, with the same
id - archived tenders, even without
archived=true, withstatusandarchiveReason
The id tells new records from changed ones. An id your system does not know is a new tender. A known id indicates a change.
The stored state captures every change if your system saves it at the end of the run. lastModified is the time of the last change in content at procuris and only increases. A new checkedAt on its own is not a change in content. The list is sorted by lastModified in ascending order, and by id when the time is the same. updated_since includes the given time. A run starting from the largest lastModified of the previous run therefore captures every change since then. All records with exactly this largest lastModified reach your system a second time this way. Creating or updating by id absorbs that.
Dropping out of filters. A filtered sync loses tenders that drop out of the filters. If the contracting authority corrects the region, for example, the tender no longer matches your content filters and no longer appears in the filtered sync. Your system then keeps the old state. If you need a complete sync, fetch without content filters, only with updated_since, and filter in your system. This costs more requests, because your system then fetches every changed tender in the data.
Pagination
Large result sets come in pages of at most 100 records. For the next page, append cursor=<nextCursor> to the same request with the same filters. If nextCursor is null, the list has ended. A cursor is valid for 24 hours. Your system stores it only for the current run and does not interpret its content, because its structure can change.
An expired cursor leads to the response 400. The response body gives the reason in the format application/problem+json:
{
"type": "https://docs.procuris.eu/docs/api/zugang#fehler",
"title": "Cursor expired",
"status": 400,
"detail": "The cursor is older than 24 hours.",
"instance": "/v1/tenders"
}Your system then restarts the run. It sets updated_since to the stored state. Records it already received in the aborted run come again and are updated by id.
Timing and limits
| Question | Answer |
|---|---|
| How often does the data change? | at most one new state per hour. lastModified shows when procuris took over a change. |
| How often should your system fetch? | once per hour with updated_since. More frequent requests pick up a new state earlier, but no additional states. |
| How often may it fetch? | 60 requests per minute per organization, across both tokens together. Your quote can include higher limits. |
| What happens above that? | response 429 with Retry-After in seconds |
| How long does the initial load take? | At 100 records per page and 60 requests per minute, your system fetches up to 6,000 records per minute. 30,000 records therefore take about 5 minutes. With archived=true, the archive is added, which goes back to January 2022. |
The hourly sync in steps
- First run: without
updated_since, with your filters andlimit=100. Your system pages throughnextCursorto the end and creates each record byid. - Store the state: save the largest
lastModifiedof the run. - Every hour: the same request with
updated_sinceset to the stored state. Create or update each record byid. Close archived records withexpired,cancelledorawardedin your system. Merge archived records withmergedwith the record frommergedInto. Then save the largestlastModifiedof this run as the new state. If no record came, the old state stays, never your own clock. - On 429 or 503: wait as long as
Retry-Aftersays, then repeat the same request. - On 500, 502 or 504: try again after 1, 2, 4, 8 and 16 minutes. If that also fails, the run aborts without changing the state. The next hourly run picks up again from the old state.
- If the previous run is still going: If the previous run has not finished on the hour, for example because it is waiting for retries, your system skips the new run. This way two runs never write at the same time, and the state stays unambiguous.
Related pages
Request a quote
Tell us what you want to use or connect. Your quote is based on that scope.