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.
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, throughupdated_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.hitmessages 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 message | Typical field in the CRM | Note |
|---|---|---|
data.tender.id | procuris ID | unique, recognizes the same tender again, see Avoid duplicates |
data.tender.lastModified | procuris state | time of the last change in content at procuris, in UTC |
data.tender.title | name of the sales opportunity | "Nordhausen - Sanierung Rolandbrunnen, Erneuerung Brunnentechnik" |
data.tender.url | link | public page of the notice |
data.tender.organizations[] with role equal to contractingParty | account or business partner | id in "procuris organization", plus name, address, email, phone |
data.tender.organizations[].contactName | contact person on the account | can be null or name a unit instead of a person, in the sample "Vergabestelle" |
data.tender.submissionDate | close date | bid deadline in UTC, in the sample after the postponement 14 October 2026, 08:00 UTC |
data.tender.participationDeadline | second date field, for example "participation deadline" | set only in two-stage procedures |
data.tender.contractAmount.value | amount | excluding VAT, in contractAmount.currency. In the sample 351,000 euros. |
data.tender.contractAmount.estimated | note "amount estimated" | if true, because the contracting authority has not published a value |
data.tender.cpvClassifications[0].title | category | The list can be empty, then the category stays empty. |
data.tender.tenderNumber | reference | tender number, in the sample "33/61/2026" |
data.tender.lots[] | line items or sub-opportunities | per lot lotNumber, title, contractAmount |
data.fit (only search.hit) | Fit | Fit from 0 to 100 |
timestamp (only search.hit) | Fit as of | time 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
archiveReason | Status in the CRM |
|---|---|
expired | lost or closed, if no bid was submitted |
cancelled | lost or closed, reason "cancelled" |
awarded | awarded to a third party, if your bid did not win |
merged | If 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.
Related pages
Request a quote
Tell us what you want to use or connect. Your quote is based on that scope.