Webhooks Integration Handbook
This handbook walks you through every step of integrating with Tendium using webhooks — from setting up your API endpoint to verifying payload signatures and reporting statuses back to Tendium.
Prerequisites
To set up webhooks in the Tendium platform, you will need:
- An endpoint that can receive HTTP POST requests
- Access to the Tendium platform to configure webhooks and generate tokens
- The ability to send HTTP POST requests to fetch additional information from Tendium
Step 0: Set up your API endpoint
Set up an API endpoint that can receive HTTP POST requests from Tendium.
Step 1: Configure webhooks in Tendium
Create the webhook
Webhooks can be created in the settings page if you have the necessary access. Only company admins can view and set up new webhooks.
- Go to https://app.tendium.com/settings/webhooks
- Click Create webhook
- Enter your Endpoint URL — this must be a valid, publicly available API that can receive HTTP POST requests
- Enter a Secret — any string that can be used to verify that the payload is coming from Tendium and has not been tampered with
Create an integration token
To receive additional information (e.g., bid data) after receiving the initial webhook payload, you will need to make requests to Tendium’s public API using an integration token.
- Go to https://app.tendium.com/settings/tokens
- Click Create an integration token
- Store the token in a safe place — you will only be able to see it once
Triggering the webhook
Currently, Tendium only supports webhooks for creating bids. You can trigger the webhook by:
- Adding a Tender, Call-Off to a bid space (automatic)
- Manually creating the bid
- Pressing the send webhook button on an existing bid
In your bid space, you can configure if you want the webhooks to be sent automatically when you create bids.
Step 1.1: Testing the webhook
Initiate the webhook by creating a bid. If the bid space has automatic webhooks turned on, an HTTP POST request will be sent to the specified URL upon creating the bid.
Alternatively, you can manually trigger the webhook by pressing the send webhook button on the bid.
Example payload
{ "eventType": "BidCreated", "data": { "bidId": "<bidId>", "itemId": "<itemId>", "bid": "query{webhookGetBid(input:\"<bidId>\"){id item{id name description specialData{buyerInformation{org orgId orgName}deadline contractDuration contractValue{amount currency}linkForSubmittingTender}itemType}files manualFiles assignedTo customFields{name type value{...on CustomBidFieldStringValue{string}...on CustomBidFieldArrayValue{unit array}...on CustomBidFieldNumberValue{number unit}...on CustomBidFieldUrlValue{title url}...on CustomBidFieldRangeValue{unit from to}...on CustomBidFieldDateValue{date}...on CustomBidFieldBooleanValue{boolean}...on CustomBidFieldMoneyValue{amount currency}...on CustomBidFieldDateRangeValue{from to}...on CustomBidFieldMoneyRangeValue{from to currency}}}}}", "publicUrl": "https://prod.public-gateway.radon.tendium.net/graphql", "bidDetailsPageUrl": "https://app.tendium.com/tender/<itemId>" }}Manual testing (optional)
You can test your integration without using the Tendium platform by sending an HTTP POST request that mimics the webhook payload.
curl -X POST <your-webhook-endpoint-url> \ -H "Content-Type: application/json" \ -d '{ "eventType": "BidCreated", "data": { "bidId": "<bidId>", "itemId": "<itemId>", "bid": "query{webhookGetBid(input:\"<bidId>\")...}", "publicUrl": "https://prod.public-gateway.radon.tendium.net/graphql", "bidDetailsPageUrl": "https://app.tendium.com/tender/<itemId>" } }'curl -X POST http://webhooks.test/api \ -H "Content-Type: application/json" \ -d '{ "eventType": "BidCreated", "data": { "bidId": "3c5f9732-be86-468d-add9-d4fbc1a9cab8", "itemId": "67d1ad38374f070e35440bde", "bid": "query{webhookGetBid(input:\"3c5f9732-be86-468d-add9-d4fbc1a9cab8\"){id item{id name description specialData{buyerInformation{org orgId orgName}deadline contractDuration contractValue{amount currency}linkForSubmittingTender}itemType}files manualFiles assignedTo customFields{name type value{...on CustomBidFieldStringValue{string}...on CustomBidFieldArrayValue{unit array}...on CustomBidFieldNumberValue{number unit}...on CustomBidFieldUrlValue{title url}...on CustomBidFieldRangeValue{unit from to}...on CustomBidFieldDateValue{date}...on CustomBidFieldBooleanValue{boolean}...on CustomBidFieldMoneyValue{amount currency}...on CustomBidFieldDateRangeValue{from to}...on CustomBidFieldMoneyRangeValue{from to currency}}}}}", "publicUrl": "https://prod.public-gateway.radon.tendium.net/graphql", "bidDetailsPageUrl": "https://app.tendium.com/tender/67d1ad38374f070e35440bde" } }'Step 2: Fetching the bid data
The initial webhook payload only contains metadata about the bid event — not the full bid data. You need one additional step to get the actual bid data.
- Take the string value from
data.bid(replace<bidId>with your actual bidId). - Send the payload to the URL provided in
data.publicUrl(i.e.,https://prod.public-gateway.radon.tendium.net/graphql) under aqueryproperty. - Provide the integration token in an
Authorizationheader to authenticate the request.
Full example using cURL
Replace <integrationToken> with your actual integration token, and <bidId> with your actual bidId.
curl --location 'https://prod.public-gateway.radon.tendium.net/graphql' \ --header 'Authorization: Bearer <integrationToken>' \ --header 'Content-type: application/json' \ --data '{ "query": "query{webhookGetBid(input:\"<bidId>\"){id item{id name description specialData{buyerInformation{orgId orgName}deadline contractDuration contractValue{amount currency}linkForSubmittingTender}itemType}files manualFiles assignedTo customFields{name type value{...on CustomBidFieldStringValue{string}...on CustomBidFieldArrayValue{unit array}...on CustomBidFieldNumberValue{number unit}...on CustomBidFieldUrlValue{title url}...on CustomBidFieldRangeValue{unit from to}...on CustomBidFieldDateValue{date}...on CustomBidFieldBooleanValue{boolean}...on CustomBidFieldMoneyValue{amount currency}...on CustomBidFieldDateRangeValue{from to}...on CustomBidFieldMoneyRangeValue{from to currency}}}}}"}'Example response
{ "data": { "webhookGetBid": { "id": "<bidId>", "item": { "id": "string", "name": "string", "description": "string", "specialData": { "buyerInformation": { "orgId": "string", "orgName": "string" }, "deadline": "<number>", "contractDuration": "string", "contractValue": { "amount": "<number>", "currency": "string" }, "linkForSubmittingTender": "string" }, "itemType": "string" }, "files": [ "https://example-s3.com/signed-url-file1.pdf", "https://example-s3.com/signed-url-file2.pdf" ], "manualFiles": [ "https://example-s3.com/signed-url-manual-file1.pdf" ], "assignedTo": "string", "customFields": [ { "name": "string", "type": "Date", "value": { "date": "<number>" } }, { "name": "string", "type": "Number", "value": { "number": "<number>", "unit": "string" } }, { "name": "string", "type": "String", "value": { "string": "string" } }, { "name": "string", "type": "DateRange", "value": { "from": "string", "to": "string" } }, { "name": "string", "type": "Money", "value": { "amount": "<number>", "currency": "string" } }, { "name": "string", "type": "URL", "value": { "title": "string", "url": "string" } } ] } }}Step 2.1: Files (optional)
If you have access to the files feature, the bid payload will include two additional fields:
| Field | Description |
|---|---|
files | Public procurement files |
manualFiles | Files uploaded to the platform for the bid |
To download the actual files, make an HTTP GET request to the URLs provided. These URLs expire 24 hours after triggering the webhook — make sure to handle them within this timeframe.
Step 2.2: Custom fields
Bids may contain custom fields defined by the customer. These fields are returned as an array under the customFields property in the bid payload.
Each custom field has a name, type, and a value object. The name and type values are set when the customer defines the custom fields in the settings page.
Custom field value schema
| Type | value schema |
|---|---|
| String | { string: string | null } |
| Number | { number: number | null, unit: string | null } |
| Date | { date: number | null } |
| DateRange | { from: string | null, to: string | null } |
| Money | { amount: number | null, currency: string | null } |
| URL | { title: string | null, uri: string | null } |
Step 3: Verifying payload signature (optional)
For additional security, a signature header is included in the initial webhook payload. This signature can be verified to make sure that the payload has not been tampered with.
Algorithm: HMAC-SHA512
Header format:
Tendium-Signature: 'signature=<hmac-sha512>,timestamp=<timestamp>'The signature is computed using the secret value set in the creation of the webhook in the platform, together with the current timestamp and the request body:
hmac-sha512(secret, '<timestamp>.<requestBody>')Format: hex
Verification steps
- Extract the
Tendium-Signatureheader from the webhook request. - Split the header by
,to separate key-value pairs, then split each by=to getsignatureandtimestamp. - Concatenate the timestamp and stringified request body:
<timestamp>.<stringifiedRequestBody> - Compute the HMAC-SHA512 hex signature using the secret and the value from step 3.
- Compare the computed signature with the one extracted from the header in step 2.
JavaScript example
createHmac("sha512", "TestSecret") .update("<timestamp>.<stringifiedRequestBody>") .digest("hex");Step 4: CRM or other integrations (optional)
After receiving the webhook data from Tendium, you can send relevant data points to any CRM you are currently using. Tendium does not currently provide any out-of-the-box integrations, which means you will have to implement this in your solution.
Step 5: Webhook statuses (optional)
You can update the webhook status in the Tendium platform to give users information on whether the webhook payload has been successfully processed outside of Tendium.
The update webhook status request is a GraphQL mutation:
updateWebhookStatus mutation
Arguments:
| Parameter | Type | Required | Description |
|---|---|---|---|
signature | String | Yes | The value provided in Tendium-Signature header. Example: 'signature=abc,timestamp=123' |
relatedEntityId | String | Yes | The relational ID for which the webhook is created. For BidCreated event type, this represents data.bidId. |
status | SendWebhookStatus! | Yes | The status of the webhook. Supported values: Unknown, Failed, Success. |
message | String | No | A custom message the end user will see when hovering over the status icon. |
cURL example
curl --request POST \ --header 'content-type: application/json' \ --header 'Authorization: Bearer <token>' \ --url https://prod.public-gateway.radon.tendium.net/graphql \ --data '{"query":"mutation updateWebhookStatus {\n updateWebhookStatus(input: { status: <status>, message: \"<message>\", relatedEntityId: \"<bidId>\", signature: \"<Tendium-Signature>\" }) {\n changedAt\n status\n }\n}","variables":{}}'GraphQL example
mutation updateWebhookStatus($input: UpdateWebhookStatusInput!) { updateWebhookStatus(input: $input) { changedAt eventType message status }}Variables:
{ "input": { "message": "<message>", "relatedEntityId": "<bidId>", "signature": "<signature>", "status": "<status>" }}Response:
{ "data": { "updateWebhookStatus": { "changedAt": "<DateTimeISO>", "eventType": "<EventType>", "message": "<message>", "status": "<status>" } }}Step 6: Error handling (optional)
GraphQL APIs can return 200 OK response codes for errors, where REST typically produces 4xx and 5xx. These error codes can still occur and are often related to other unexpected issues like network problems.
Check for these properties in the response object for potential errors:
| Property | Description |
|---|---|
errors | Array of all errors returned |
errors[n].message | Details about the error |
errors[n].extensions | Additional information about the error(s) |