Skip to content

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.

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

Set up an API endpoint that can receive HTTP POST requests from Tendium.

Webhooks can be created in the settings page if you have the necessary access. Only company admins can view and set up new webhooks.

  1. Go to https://app.tendium.com/settings/webhooks
  2. Click Create webhook
  3. Enter your Endpoint URL — this must be a valid, publicly available API that can receive HTTP POST requests
  4. Enter a Secret — any string that can be used to verify that the payload is coming from Tendium and has not been tampered with

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.

  1. Go to https://app.tendium.com/settings/tokens
  2. Click Create an integration token
  3. Store the token in a safe place — you will only be able to see it once

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.

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.

{
"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>"
}
}

You can test your integration without using the Tendium platform by sending an HTTP POST request that mimics the webhook payload.

Terminal window
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>"
}
}'

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.

  1. Take the string value from data.bid (replace <bidId> with your actual bidId).
  2. Send the payload to the URL provided in data.publicUrl (i.e., https://prod.public-gateway.radon.tendium.net/graphql) under a query property.
  3. Provide the integration token in an Authorization header to authenticate the request.

Replace <integrationToken> with your actual integration token, and <bidId> with your actual bidId.

Terminal window
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}}}}}"}'
{
"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" } }
]
}
}
}

If you have access to the files feature, the bid payload will include two additional fields:

FieldDescription
filesPublic procurement files
manualFilesFiles 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.

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.

Typevalue 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)

Section titled “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

  1. Extract the Tendium-Signature header from the webhook request.
  2. Split the header by , to separate key-value pairs, then split each by = to get signature and timestamp.
  3. Concatenate the timestamp and stringified request body: <timestamp>.<stringifiedRequestBody>
  4. Compute the HMAC-SHA512 hex signature using the secret and the value from step 3.
  5. Compare the computed signature with the one extracted from the header in step 2.
createHmac("sha512", "TestSecret")
.update("<timestamp>.<stringifiedRequestBody>")
.digest("hex");

Step 4: CRM or other integrations (optional)

Section titled “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.

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:

Arguments:

ParameterTypeRequiredDescription
signatureStringYesThe value provided in Tendium-Signature header. Example: 'signature=abc,timestamp=123'
relatedEntityIdStringYesThe relational ID for which the webhook is created. For BidCreated event type, this represents data.bidId.
statusSendWebhookStatus!YesThe status of the webhook. Supported values: Unknown, Failed, Success.
messageStringNoA custom message the end user will see when hovering over the status icon.
Terminal window
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":{}}'
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>"
}
}
}

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:

PropertyDescription
errorsArray of all errors returned
errors[n].messageDetails about the error
errors[n].extensionsAdditional information about the error(s)