procuris

CRM and ERP

New matching tenders automatically as sales opportunities in the CRM, deadlines and values in the ERP, with field mapping and protection against duplicates.

On request

You receive this service through an individual quote.

Your CRM gets new matching tenders as sales opportunities. The CRM is the software in which your sales team manages customers and sales opportunities. The opportunity carries deadline, contracting authority and contract value, as far as procuris knows them. If the contracting authority has not published a contract value, an estimated value can arrive, marked as estimated. If the contracting authority postpones the deadline, the CRM follows. Your ERP, the software for orders, planning and accounting, gets the same data for bid planning and reporting.

Your IT or your service provider builds the integration. procuris delivers the data through webhooks and, depending on the variant, also through the API. If you want procuris to build the integration, we include that in your quote.

The recipes only require an interface in your system. Your CRM or ERP has to accept data through an interface of its own. Salesforce, HubSpot, Microsoft Dynamics, Pipedrive and SAP offer such an interface. There is no ready-made connector for a specific product. You decide yourself which field goes where, following the table under Map fields.

For a CRM, we recommend a small service of your own. A workflow builder would work without programming, but it is only enough for simple flows such as a Teams message. The sync with the CRM needs a queue and a comparison of states in both variants. Variant A also needs an initial load through the API.

Recipe 1: New hit becomes a sales opportunity

The recipe has two variants. They differ in which tenders should go into the CRM and which access you need.

  • Variant A brings all tenders matching your filters for industry and region into the CRM (tender.published). It needs the API and webhooks: the API for the initial load and the nightly sync, webhooks for everything in between.
  • Variant B brings only the hits of your saved searches (search.hit). It needs webhooks only. There is no initial load.

Steps 4, 5 and 9 apply to variant A only.

Create fields. In the CRM, on the sales opportunity, create a unique text field "procuris ID" for tender.id, a field "procuris state" for lastModified, a number field "Fit" and a field "Fit as of" for the time of the fit. On the account or business partner, create a unique text field "procuris organization" for organizations[].id. Through "procuris ID", your system recognizes the same tender again. Through "procuris organization", it recognizes the same contracting authority, provided procuris has recognized it as the same body. Two different id values do not rule out that it is the same body. In that case, two accounts appear in the CRM, which your sales team merges by hand. Step 7 needs the fields "procuris state" and "Fit as of".

Build a receiver with a queue. A small service accepts the messages and checks the signature with a ready-made library from Delivery and security. Then it puts the message into a queue and responds with 2xx before it writes into the CRM. This keeps it under the 15 seconds that procuris waits for a response. It writes into the CRM only in step 6.

Create an endpoint and choose events. The person with the role Owner creates the address of the service as an endpoint in procuris under Settings › Organization › Webhooks. The Webhooks section exists only once access has been set up after the contract is signed. Until then, the row of the same name in the On request section only requests a quote. Variant A: the event types tender.published with the filters for your industry and region, plus tender.updated for changed values, lots and deadlines and tender.archived for closing. Variant B: the event types search.hit, tender.updated and tender.archived, plus the saved searches whose hits should go into the CRM. Set on the endpoint that the selection of saved searches also applies to tender.updated and tender.archived. Then changed values, postponed deadlines and archiving arrive for every tender that has once been a hit of one of these saved searches, see Events. From the moment you save, the chosen events flow into the queue. Check with Send test message whether they arrive there.

Initial load (variant A). The initial load brings all tenders that already exist for your filters into the CRM once. Your service remembers the time at which it starts. Then it fetches through the API with the same filters as on the endpoint and creates one sales opportunity per tender, see Filters and timing. For every sales opportunity, it stores lastModified in "procuris state".

Catch-up fetch (variant A). Then it fetches a second time, with updated_since set to the time remembered in step 4. This way it gets what changed during the initial load.

Work through the queue. Only now does the service process the messages from the queue, and after them every new one. In variant B, the first hits arrive on the morning after the endpoint is created. The order of messages is not guaranteed, because retries and resent messages can arrive later. That is why every event except the test message can create a sales opportunity. The service skips messages with data.test set to true.

The procuris ID decides between creating and updating. The service first looks for a sales opportunity with procuris ID equal to data.tender.id. If there is one, step 7 applies. If there is none, it creates it from data.tender, for tender.archived directly with the status from the table under Map status. When creating, it stores data.tender.lastModified in "procuris state". For search.hit, it stores data.fit in "Fit" and the timestamp of the message in "Fit as of". It looks up the contracting authority through "procuris organization" and creates it only if it does not exist in the CRM.

In variant B, an opportunity without a fit can appear. This happens when tender.updated or tender.archived arrives for an earlier hit for which the CRM has no opportunity yet, for example a hit from before the endpoint was created. Only search.hit carries a fit. The field "Fit" then stays empty.

Apply changes. If data.tender.lastModified is newer than "procuris state", the service rewrites all mapped fields from data.tender, not only those in changedFields. Then it stores the new lastModified in "procuris state". If it is older or equal, it takes nothing from data.tender. It takes data.fit only if the timestamp of the message is newer than "Fit as of", and then stores this timestamp there. This way, neither a late nor a resent search.hit overwrites a newer state. Messages about changes that are already in the CRM from the initial load or the catch-up fetch drop out this way. In variant A, there is no gap between the initial load and webhooks, because the queue runs from the moment the endpoint is saved and the initial load starts only after that.

Close. For tender.archived, the service maps data.tender.archiveReason to the status in the CRM, see the table under Map status.

Nightly sync (variant A). The nightly sync updates tenders that dropped out and creates missing opportunities. If a tender no longer matches your filters after a change, for example because the contracting authority corrects the region, no further tender.updated arrives for it, only tender.archived. That is why the service fetches through the API every night without content filters, only with updated_since.

The first nightly run fetches everything from the start of the initial load. For this, the service sets updated_since to the time it remembered in step 4. This way it also covers tenders that have dropped out of the filters since the initial load began. From the second night on, the largest lastModified of the previous run applies.

The procuris ID decides what happens with a record. If it exists in the CRM, the service updates the opportunity by the rules from step 7. If it does not, the service checks itself whether the record matches the filters of the endpoint, for example through cpvClassifications and regionCodes. If it matches, the service creates the opportunity as in step 6, an archived one directly with status. This way, tenders whose message got lost also reach the CRM. It skips all other records.

At the end of the run, it stores the largest lastModified as the state for the next night. If a run returns nothing, the stored state stays unchanged. The fetch without filters gets every changed tender in the stock and therefore needs more requests than a filtered one, see Filters and timing.

Close gaps

How you recover missed messages depends on their age. procuris discards a message when the tenth attempt also fails, a little over three days after the first. procuris reports discarded messages to the owner in a summary email, once a day, see Delivery and security.

  • Messages from the last 7 days are resent by the owner under Webhooks on the endpoint. This applies to both variants.
  • Older gaps in tender.* can be filled only by a system with API access, through updated_since. In variant A, the nightly sync from step 9 does this. In variant B without the API, such a gap stays open.
  • Older search.hit messages cannot be recovered, because the API knows no fit and no saved searches.

Map fields

Events carry the tender with the fields of the API. It is under data.tender. The values in the notes come from the sample record of the data model.

Field in the messageTypical field in the CRMNote
data.tender.idprocuris IDunique, recognizes the same tender again, see Avoid duplicates
data.tender.lastModifiedprocuris statetime of the last change in content at procuris, in UTC
data.tender.titlename of the sales opportunity"Nordhausen - Sanierung Rolandbrunnen, Erneuerung Brunnentechnik"
data.tender.urllinkpublic page of the notice
data.tender.organizations[] with role equal to contractingPartyaccount or business partnerid in "procuris organization", plus name, address, email, phone
data.tender.organizations[].contactNamecontact person on the accountcan be null or name a unit instead of a person, in the sample "Vergabestelle"
data.tender.submissionDateclose datebid deadline in UTC, in the sample after the postponement 14 October 2026, 08:00 UTC
data.tender.participationDeadlinesecond date field, for example "participation deadline"set only in two-stage procedures
data.tender.contractAmount.valueamountexcluding VAT, in contractAmount.currency. In the sample 351,000 euros.
data.tender.contractAmount.estimatednote "amount estimated"if true, because the contracting authority has not published a value
data.tender.cpvClassifications[0].titlecategoryThe list can be empty, then the category stays empty.
data.tender.tenderNumberreferencetender number, in the sample "33/61/2026"
data.tender.lots[]line items or sub-opportunitiesper lot lotNumber, title, contractAmount
data.fit (only search.hit)FitFit from 0 to 100
timestamp (only search.hit)Fit as oftime of the fit, decides about overwriting in step 7

Missing values stay empty or get a substitute. If submissionDate is null and your CRM requires a close date, set publicationDate plus 30 days and mark the opportunity with "deadline open". As soon as the contracting authority names a deadline, tender.updated arrives with submissionDate in changedFields. If contractAmount is null, leave the amount empty.

Map status

archiveReasonStatus in the CRM
expiredlost or closed, if no bid was submitted
cancelledlost or closed, reason "cancelled"
awardedawarded to a third party, if your bid did not win
mergedIf the opportunity for mergedInto exists, merge both. If the remaining record is missing in the CRM: with API access (variant A), the service fetches it with GET /v1/tenders/{mergedInto} and creates the opportunity from it. Without API access (variant B), it closes the old opportunity and notes mergedInto on it. The opportunity for the remaining tender appears only once it is a hit of a chosen saved search and an event arrives for it.

Avoid duplicates

The procuris ID prevents duplicate opportunities for the same id. One record per tender is the goal at procuris, not a guarantee. If a notice appears on several portals, procuris merges it into one record. If procuris detects a duplicate only later, it archives the newer record with archiveReason set to merged and names, in mergedInto, the id of the record that remains. Your service handles this according to the merged row in the table above.

Recipe 2: Nightly sync into the ERP

In the ERP, every matching tender becomes a bid project. Matching here means: it matches your filters. In SAP, for example, this is a project or a sales document of the type quotation. Attached to it are the deadline as a date, the contracting authority as a business partner and the contract value as planned revenue.

A nightly fetch through the API is enough here. Planning and reporting work with the state of the previous day. More frequent fetches are possible. However, procuris delivers at most one new state per hour.

Initial load. A load job fetches GET /v1/tenders with your filters and creates one project per record, with id as the key.

Nightly run. It fetches with updated_since set to the largest lastModified of the previous run. New, changed and archived tenders arrive. It closes archived ones in the ERP or merges them, according to the table under Map status.

Tenders that dropped out keep their old state in the filtered run. If they should stay current in the ERP, the run fetches without content filters, like step 9 in recipe 1. Filters and timing describes the process in detail, and the data model lists all fields.

Request a quote

Tell us what you want to use or connect. Your quote is based on that scope.

Request a quote

On this page