Skip to main content

Supplier Update Webhook

Use this when an already-onboarded supplier has a change approved, for example an updated bank account or a changed B-BBEE certificate. This is a separate webhook from onboarding. It has its own trigger, its own payload shape, and no callback step.

Breaking change: category keys are moving to PascalCase

The categories keys are changing to PascalCase. For example Bank Details becomes BankDetails, BBBEE becomes Bbbee, and Tax PIN & VAT Details becomes TaxPinAndVatDetails. This is a hard cutover with no dual-emit period: once it ships, a receiver still matching an old key will not error. It iterates the map as this page instructs, finds no key it recognises, and silently skips the update. Update your key matching before the cutover.

Old keyNew key
Bank DetailsBankDetails
Tax ClearanceTaxClearance
Tax PIN & VAT DetailsTaxPinAndVatDetails
COIDACoida
BBBEEBbbee
Company DetailsCompanyDetails
Contact DetailsContactDetails
DocumentsDocuments (unchanged)

Confirm the effective release with your B1Link administrator.

When it fires​

B1Link sends this webhook when an approved change to an already-onboarded supplier crosses its approval threshold.

One category per delivery​

Approval happens per category, and each approval dispatches its own delivery with its own correlationId. So if a supplier changes their bank details and their contact details, and both are approved, you receive two separate POSTs: one carrying BankDetails and one carrying ContactDetails, not a single POST containing both.

Write your receiver to loop over categories anyway. The field is a map, and the delivery contract allows more than one entry even though today exactly one is populated. A receiver that reads "the single key" will break if that ever changes; a receiver that iterates the map handles both cases and costs nothing extra:

for (const [category, snapshot] of Object.entries(payload.categories)) {
// handle the categories you support, log and skip the rest
}

Treat an unrecognised category key as something to log and skip rather than an error, so a category added in future does not break your receiver.

Where it's sent​

It's sent to the same per-client webhook endpoint you already configured for onboarding, with one exception: routes configured to go through a client-specific middleware integration are deliberately skipped for this event. If that applies to your integration, confirm with your B1Link administrator whether you receive this webhook at all.

Payload​

{
"messageType": "SupplierUpdateApproved",
"supplierCompanyId": 456,
"clientCompanyId": 123,
"correlationId": "9c858901-8a57-4791-81fe-4c455b099bc9",
"onboardingRequestErpEventId": "4f6a1e2c-9b3d-4a7e-8c21-5f0b9d2a7e10",
"erpNumber": "VENDOR-00123",
"occurredAt": "2024-01-15T14:32:00Z",
"categories": {
"BankDetails": [
{
"id": 1001,
"bankId": 4,
"textBankName": null,
"accountTypeId": 2,
"textAccountType": null,
"accountName": "Example Supplier (Pty) Ltd",
"accountNumber": "9999999999",
"branchName": "Example Branch",
"branchCode": "000001",
"swiftCode": null,
"countryId": 1,
"isPrimary": true,
"isDeleted": false
}
]
}
}
FieldTypeDescription
messageTypestringAlways SupplierUpdateApproved for this webhook.
supplierCompanyIdintegerThe supplier company whose change was approved. Same id space as the supplierCompanyId you received during onboarding.
clientCompanyIdintegerThe client company the supplier is onboarded to.
correlationIdGUID (string)Unique identifier for this delivery. Identical on every retry of the same delivery. See Retries.
onboardingRequestErpEventIdGUID (string) or nullThe onboarding ERP event this supplier was last processed under, for the same client. Null when the supplier was onboarded manually or never pushed to an ERP. See Correlation.
erpNumberstring or nullThe ERP vendor number from that same onboarding event. Null when there is no such event, or none has been assigned a vendor number yet. See Correlation.
occurredAtstring (ISO-8601, UTC)When the update was approved.
categoriesobjectA map of category key to that category's snapshot. Iterate it rather than assuming a fixed key. In practice it carries one entry; see One category per delivery. Keys are listed under Category keys.

Category keys​

The keys inside categories are PascalCase identifiers (for example BankDetails, TaxPinAndVatDetails), aligned with the onboarding payload's naming convention. Match them exactly, as literal strings. The per-category sections further down are titled by each category's friendly name; the key you actually match in categories is the one in the table below.

Category keyGroup
BankDetailsBanking
TaxClearanceTax
TaxPinAndVatDetailsTax
CoidaCompliance
BbbeeBEE
CompanyDetailsCompany
ContactDetailsContacts
DocumentsDocs

Only the category that was part of the approved change appears; see One category per delivery above.

The difference from onboarding is structural, not only in the names. The onboarding webhook carries a data array of { category, dataJson, success, errorMessage } objects. This webhook carries a categories object keyed directly by category name. Do not reuse onboarding parsing code as-is, because it will not deserialize this payload correctly.

What's inside a category​

The value under the category key is the full current state of that category, not a delta. It is not "the fields that changed"; it is every field of that category as it stands after the approved change. Apply it as a replace-the-record update for that category, and use your own previous copy if you need to know what actually moved.

All snapshots are camelCase. Some carry internal row ids, foreign-key ids and audit columns (id, companyId, createdBy, dateModified, isDeleted, …). Ignore any property you do not recognise rather than rejecting the payload. Treat the list below as the fields you can rely on, not an exhaustive schema.

Three of the shapes are not single objects, which is the thing most likely to break a receiver:

CategoryShape
BankDetailsArray of every account the supplier currently holds
ContactDetailsArray of every contact the supplier currently holds
DocumentsObject keyed by document-type name
CompanyDetails, Coida, TaxClearance, TaxPinAndVatDetails, BbbeeSingle object

Bank Details​

An array of the supplier's accounts. Even when one account changed, every account is sent, so compare against your own copy to find the one that moved.

{
"categories": {
"BankDetails": [
{
"id": 1001,
"bankId": 4,
"textBankName": null,
"accountTypeId": 2,
"textAccountType": null,
"accountName": "Example Supplier (Pty) Ltd",
"accountNumber": "9999999999",
"branchName": "Example Branch",
"branchCode": "000001",
"swiftCode": null,
"countryId": 1,
"isPrimary": true,
"isDeleted": false
}
]
}
}

bankId and textBankName are alternatives, not both: a South African bank is identified by bankId (a lookup id), a foreign bank by free-text textBankName. The same applies to accountTypeId / textAccountType. Resolve the pair, because reading only one leaves the bank blank for most suppliers.

Contact Details​

An array of the supplier's contacts, sent in full on every change.

{
"categories": {
"ContactDetails": [
{
"id": 20,
"name": "Example Contact",
"email": "contact@example.com",
"landLine": "0110000000",
"mobile": "0820000000",
"fax": null,
"website": null,
"idNumber": "0000000000000",
"contactTypeId": 3,
"designation": "Financial Manager",
"companyId": 5000,
"isDeleted": false
}
]
}
}

Company Details​

{
"categories": {
"CompanyDetails": {
"registeredName": "Example Supplier (Pty) Ltd",
"tradingName": "Example Supplier",
"registrationNumber": "0000/000000/07",
"companyTypeId": 1,
"email": "info@example.com",
"tradeDescription": "Manufacturing",
"vatNumber": "0000000000",
"vatRegistrationDate": "2020-01-01T00:00:00+00:00",
"taxClearanceNumber": "TCN-EXAMPLE-001",
"taxClearanceNumberIssueDate": "2025-01-01T00:00:00+00:00",
"taxClearanceNumberExpiryDate": "2026-01-01T00:00:00+00:00",
"incomeTaxNumber": "0000000000",
"countryId": 1,
"websiteUrl": "https://example.com",
"numberOfEmployees": 50,
"financialYearEndMonthId": 2,
"geographicalLocations": "Gauteng",
"holdingCompanyName": null,
"isRoyalBafokeng": false
}
}
}

Note the overlap: the tax fields above (vatNumber, vatRegistrationDate, taxClearanceNumber and its dates) also appear under TaxClearance and TaxPinAndVatDetails. The same edit can therefore reach you under more than one category depending on which screen the supplier used. Make your handling idempotent so applying the same value twice is harmless.

Tax Clearance​

{
"categories": {
"TaxClearance": {
"taxClearanceNumber": "TCN-EXAMPLE-001",
"taxClearanceNumberIssueDate": "2025-01-01T00:00:00+00:00",
"taxClearanceNumberExpiryDate": "2026-01-01T00:00:00+00:00"
}
}
}

Tax PIN & VAT Details​

{
"categories": {
"TaxPinAndVatDetails": {
"vatNumber": "0000000000",
"vatRegistrationDate": "2020-01-01T00:00:00+00:00"
}
}
}

COIDA​

{
"categories": {
"Coida": {
"id": 7,
"cfRegistrationNumber": "CF0000000",
"certificateNumber": "COID-EXAMPLE-001",
"dateOfIssue": "2025-01-01T00:00:00+00:00",
"expiryDate": "2026-01-01T00:00:00+00:00",
"companyId": 5000,
"isDeleted": false
}
}
}

Note cfRegistrationNumber, lowercase c and lowercase f. This is the same spelling used in the onboarding payload.

BBBEE​

The same field set as the onboarding webhook's B-BBEE category. Abbreviated here:

{
"categories": {
"Bbbee": {
"companyId": 5000,
"hasBbbeeCertificate": true,
"isCurrentCertificate": true,
"blackOwnershipPercentage": 51.0,
"blackWomenOwnershipPercentage": 30.0,
"bbbeeCertificateNumber": "BEE-EXAMPLE-0001",
"bbbeeCertificateIssueDate": "2026-01-01T00:00:00+00:00",
"bbbeeCertificateExpiryDate": "2027-01-01T00:00:00+00:00",
"bbbeeCertificateDocumentId": "00000000-0000-4000-8000-000000000001",
"bbbeeLevel": "Level 2",
"bbbeeSector": "Generic",
"annualTurnoverRange": "R10m - R50m",
"isLocalServiceProvider": true,
"isLocalGoodsManufacturer": false,
"isEmpoweringSupplier": true
}
}
}

See the onboarding payload reference for the complete B-BBEE field list; the same fields are used here.

Documents​

An object keyed by document-type name, not an array. Each entry carries the file name and a reference to the stored document.

{
"categories": {
"Documents": {
"Tax Clearance": {
"documentName": "tax-clearance.pdf",
"blobReference": "00000000-0000-4000-8000-000000000002"
},
"BBBEE Certificate": {
"documentName": "bbbee-certificate.pdf",
"blobReference": "00000000-0000-4000-8000-000000000003"
}
}
}
}

Only document types that are required for that supplier and have a file actually uploaded appear. The document-type names are the same ones used elsewhere in B1LINK, so treat them as data rather than a fixed enumeration, and skip a type you do not recognise.

Correlation: read this before you build against it​

Alongside supplierCompanyId, clientCompanyId and correlationId, the payload carries two identifiers that let you map an approved change straight to your vendor record:

  • onboardingRequestErpEventId: the onboarding ERP event this supplier was last processed under for this client.
  • erpNumber: the ERP vendor number from that same onboarding event.

Both can be null. A supplier onboarded manually, or never pushed to an ERP, has no onboarding event, so both are null. A supplier whose onboarding event exists but has not yet been assigned a vendor number has an onboardingRequestErpEventId but a null erpNumber.

So build for both cases:

  • When erpNumber is present, key straight to your vendor record.
  • When it is null, fall back to your own mapping from supplierCompanyId (which you received in the onboarding webhook) to your vendor record.

Retain that supplierCompanyId mapping regardless. It is the one identifier always present, and it is what keeps a null-identifier delivery actionable.

Authentication​

This webhook carries no signature and no API key, so establish that a request came from B1Link at the network layer and validate the payload before you act on it.

  • Accept requests only from B1Link's platform IP addresses. Configure your gateway or firewall to allow those addresses and reject everything else, and ask your B1Link administrator for the current list.
  • Serve the endpoint over HTTPS only.
  • Validate the payload shape before acting on it. Do not assume a well-formed SupplierUpdateApproved body.
  • Corroborate against your own records before applying a change, for example that supplierCompanyId maps to a vendor you actually onboarded.

Response handling​

The response body is ignored entirely. Return whatever you like, or nothing. Only the HTTP status code matters:

  • Any 2xx status is treated as success.
  • Anything else is treated as a failure and may be retried (see below).

The vendor number travels to you, in the erpNumber field above. Nothing is expected back in the response: this webhook has no callback step, so unlike onboarding there is nothing further for your code to send back to B1Link for this event.

Retries​

  • Up to 4 attempts by default (1 initial attempt + 3 retries).
  • Backoff between attempts: 1s, then 2s, then 4s.
  • Per-attempt timeout: 30s by default.
  • The retry count is configurable per client, but the configured value is clamped so that the full attempt sequence, including backoff, completes within a fixed delivery window.
  • A retried delivery is identical to the original: same correlationId, same body.

Because retries resend the same body under the same correlationId, your receiver must be idempotent and should de-duplicate on correlationId. Applying the same approved update twice should be a no-op the second time.

How this differs from onboarding​

Onboarding webhookSupplier-update webhook
TriggerOnboarding reaches the sync pointAn approved change crosses its approval threshold
Payload shapedata array of { category, dataJson, success, errorMessage }categories object keyed by category name
Vendor numberYou return it to B1Link, via callback or inline responseSent to you inbound as erpNumber (nullable); you return nothing
Inline (synchronous) response modeAvailable if enabled; see Sync LifecycleNot available; there is no response payload for this event
Client-specific middleware routingIncludedExcluded for routes configured to use it
Signature on the inbound requestNoneNone
Signature on your replyOnboarding's asynchronous callback is signed (X-B1LINK-Signature)N/A; no callback for this event

Next steps​