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
Section titled “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
Section titled “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
Section titled “Step 1: Configure webhooks in Tendium”Create the webhook
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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)
Section titled “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
Section titled “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
Section titled “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
Section titled “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)
Section titled “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
Section titled “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
Section titled “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)
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
Verification steps
Section titled “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
Section titled “JavaScript example”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.
Step 5: Webhook statuses (optional)
Section titled “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
Section titled “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
Section titled “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
Section titled “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)
Section titled “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) |