Webhooks Guide
Receive real-time updates from Taxbit as they happen — TIN validation results, tax form status changes, and tax documentation updates — pushed to an endpoint you control.
Overview
When something changes in your Taxbit organization, we send it to you rather than making you poll for it. You give us an HTTPS endpoint and a list of event types; we POST a JSON payload to that endpoint each time a matching event occurs.
Every delivery is signed with a shared secret so you can confirm it came from Taxbit. Failed deliveries are retried, which means the same event can reach you more than once — your handler needs to be idempotent. Both of those are covered in detail below.
If you read one sectionRead Responding to a delivery. The status code your endpoint returns has consequences beyond a single request — one class of response can disable your subscription until Taxbit manually re-enables it.
Getting set up
Webhook subscriptions are configured by Taxbit, not self-serve. To get started, contact your Implementation Manager with the following.
- Your endpoint URL. Must be HTTPS. One subscription delivers all of its event types to a single endpoint; if you want different events routed to different endpoints, ask for separate subscriptions.
- The event types you want. Pick from the event catalog. You can add or remove types later, but it requires a configuration change on our side — it is not instant.
- Whether you allowlist inbound IPs. If your endpoint sits behind an IP allowlist, say so up front. By default our requests originate from cloud addresses that are not stable over time. Subscriptions can be configured to deliver from a fixed egress address instead — ask your Implementation Manager for the details.
- Whether your endpoint requires a credential to accept the request. Every delivery is signed regardless, but if a gateway in front of your endpoint needs HTTP authentication before it will route the request, say so — see Authentication.
- Optional delivery settings. If you have specific requirements, tell us — see the defaults below.
Your Implementation Manager will then confirm the subscription and send you an HMAC secret through a secure channel. Store it as you would any other credential.
Delivery settings
Three settings are configurable per subscription. Unless you ask for something different, your subscription uses the defaults:
| Setting | Default | What it controls |
|---|---|---|
| Rate limit | 300 req/sec | The maximum rate at which we deliver to your endpoint. Lower it if your endpoint can't absorb bursts. |
| Retry attempts | 2 | How many times we re-attempt a failed delivery. |
| Retry window | 1 hour | How long we keep retrying before giving up on an event. |
Confirm your actual valuesMany subscriptions are provisioned with values other than the defaults — commonly a lower rate limit and a much longer retry window. Ask your Implementation Manager to confirm the three numbers configured for your subscription in writing before you go live, and size your endpoint and your deduplication window against those, not against the defaults on this page.
Anatomy of a delivery
A delivery is an HTTPS POST with a JSON body:
POST /your-webhook-endpoint HTTP/1.1
host: your-service.example.com
content-type: application/json
x-taxbit-signature: v1=<base64_hmac_signature>
x-taxbit-source: webhook
x-trace-id: <trace_id>
{
"timestamp": "<timestamp>",
"data": [
{
"event_type": "ACCOUNT_OWNER_TIN_VALIDATION",
"account_owner_id": "<account_owner_id>",
"account_owner_external_id": "<account_owner_external_id>",
"status": "<tin_verification_status>",
"validation_date": "<validation_date>"
}
]
}Headers
| Header | Description |
|---|---|
x-taxbit-signature | The HMAC signature of the payload. This is how you authenticate the request — see Verifying the signature. May contain more than one signature. |
x-trace-id | An opaque identifier for this delivery. Log it. It is the only value that lets Taxbit support correlate your access logs with ours. Treat it as an arbitrary string — the format is not guaranteed and it is not always a UUID. |
x-taxbit-source | Always the literal string webhook. This is a marker, not a credential. It is identical for every Taxbit customer and carries no secret. Do not use it to authenticate requests. Not sent if your subscription uses basic authentication — see Authentication. |
authorization | Only if your subscription is configured for basic authentication. Carries the credentials you supplied at onboarding. See Authentication. |
content-type | application/json. |
The payload envelope
timestamp— an ISO 8601 timestamp for when Taxbit dispatched this delivery, not when the underlying event occurred. Do not use it for business logic or ordering; use the date fields on the event itself, such asvalidation_date.data— an array of event objects. Today this array always contains exactly one event. Iterate over it rather than readingdata[0], so that if we begin batching events in future it is not a breaking change for you.
Each object in data has an event_type field plus fields specific to that type. See the event catalog.
Authentication
There are two independent layers to how a delivery is authenticated, and it is worth being clear about which does what.
1 · The payload signature — always on. Every delivery is signed with a secret shared only between you and Taxbit, and the signature travels in x-taxbit-signature. This is what proves a request genuinely came from Taxbit and was not modified in transit. It cannot be turned off, and it is the layer you should be checking.
2 · The connection credential — optional. A credential Taxbit presents to your endpoint so that infrastructure in front of your application — a gateway, a load balancer, an API manager — can admit the request before it ever reaches your code. This layer is about getting through your front door. It says nothing about whether the payload is authentic.
Layer 2 is configured per subscription. Whatever you choose there, layer 1 is unchanged.
| Connection credential | What we send | When to choose it |
|---|---|---|
| Source marker (default) | x-taxbit-source: webhook | The default. A fixed marker, not a secret. Choose it when your endpoint is reachable directly and relies on the payload signature for authentication. |
| HTTP Basic | Authorization: Basic … | Your endpoint sits behind something that requires HTTP authentication before it will route the request. |
Basic authentication
Available on request. Give your Implementation Manager a username and password through the secure channel they provide — not over email or a support ticket. Once enabled, every delivery to your subscription carries a standard Authorization: Basic base64(username:password) header, and the x-taxbit-source header is no longer sent.
Basic auth adds to the signature — it does not replace itYou still receive
x-taxbit-signature, and you should still verify it on every request. Basic credentials are replayable by anyone who captures a request and are only as strong as the TLS around them. The signature is what proves the payload came from Taxbit and arrived unmodified. Treat basic auth as a way through your gateway, not as your authentication.
Stale basic credentials are the fastest way to lose your subscriptionIf your endpoint rejects our credentials with a
401— because they were rotated on your side, expired, or were revoked — that response can put your subscription into the permanent deauthorized state described in Responding to a delivery. Delivery stops and does not resume when you fix the credential.This has happened to real customers. If you use basic authentication, treat any change to those credentials as a coordinated change with Taxbit, not a routine rotation on your side.
Rotating basic credentials
Unlike the signing secret, a subscription holds one set of basic credentials — there is no overlap period where both old and new are accepted. Rotation is a cutover, so it has to be scheduled:
- Tell your Implementation Manager you need to rotate, and agree a window.
- Configure your endpoint to accept both the old and new credentials.
- Taxbit updates the subscription to the new credentials.
- Confirm deliveries are arriving, then stop accepting the old credentials.
Step 2 is the one people skip. Without it there is a window where we are presenting credentials you no longer accept — and a 401 in that window can disable the subscription outright.
Verifying the signature
Every delivery carries an x-taxbit-signature header. Verify it on every request before you act on the payload — it is the only thing that distinguishes a genuine Taxbit delivery from anyone who has discovered your endpoint URL.
How the signature is built
- The algorithm is HMAC-SHA256, keyed with the secret your Implementation Manager gave you.
- It is computed over the JSON payload — the same object you receive as the request body.
- The digest is base64-encoded — not hex.
- Each signature is prefixed with a version:
v1=. - The header may contain multiple signatures, comma-separated. Accept the request if any one of them matches.
Example
This mirrors Taxbit's own implementation: parse the body, re-serialize it, and hash that.
const crypto = require('crypto')
const express = require('express')
const SECRET = process.env.TAXBIT_WEBHOOK_SECRET
const app = express()
// Returns true if any signature in the header matches, compared in constant time.
function isFromTaxbit (payload, header) {
const expected = 'v1=' + crypto
.createHmac('sha256', SECRET)
.update(JSON.stringify(payload))
.digest('base64')
const expectedBuf = Buffer.from(expected)
return (header || '')
.split(',')
.some((candidate) => {
const buf = Buffer.from(candidate.trim())
// timingSafeEqual throws on a length mismatch, so check length first.
return buf.length === expectedBuf.length &&
crypto.timingSafeEqual(buf, expectedBuf)
})
}
app.post('/your-webhook-endpoint', express.json(), (req, res) => {
if (!isFromTaxbit(req.body, req.get('x-taxbit-signature'))) {
// Return 400, never 401 or 403 — see "Responding to a delivery".
return res.status(400).send('invalid signature')
}
for (const event of req.body.data) {
enqueueForProcessing(event) // do the real work off the request path
}
res.status(200).send('ok')
})
Notes on the example
- Compare signatures in constant time (
crypto.timingSafeEqualabove). A plain===or.includes()leaks information about the expected value.- Acknowledge fast and process asynchronously. Do the work after you have responded, not before.
Rotating your signing secret
To rotate, ask your Implementation Manager to add a new secret. During the rotation your subscription is signed with both the old and the new secret, and x-taxbit-signature will contain two comma-separated signatures. Because you accept the request if any signature matches, you can switch your stored secret over at your own pace with no dropped deliveries. Once you confirm the new secret is in use, we remove the old one.
This only works if your verification loops over every signature in the header. Code that reads only the first signature, or string-matches the entire header value, will break the moment a rotation begins.
Responding to a delivery
Return 2xx once you have accepted the event. Anything else is treated as a failed delivery and will be retried according to your retry settings.
Never return 401, 403, or 407An authentication-class response from your endpoint can put your subscription into a deauthorized state. This is not a temporary condition and it does not clear itself: delivery stops entirely, and it stays stopped even after you fix whatever caused the response. Restoring it requires manual action by Taxbit.
This applies to anything in front of your endpoint too — a WAF, an API gateway, a reverse proxy, or a rate limiter returning
401or403on your behalf will do the same thing.If you need to reject a request — including one whose signature does not verify — return
400and alert your own team. A failed signature check means something is misconfigured, and you want a human looking at it, not your subscription switched off.
What to return, when
| Situation | Return |
|---|---|
| Accepted successfully | 200–299 |
| You are down, deploying, or overloaded | 500–599 |
| You are rate limiting us | 429 — and tell your Implementation Manager, so your subscription's rate limit can be lowered and we throttle at source |
| Signature invalid, or malformed payload | 400 — and alert your team, this is a misconfiguration |
| Anything requiring authentication | Never — may permanently disable your subscription |
Timeouts and retries
Your endpoint is expected to respond promptly, and a timeout counts as a failed delivery. Acknowledge the request as soon as you have durably queued the event, and do your processing afterwards rather than holding the connection open.
Failed deliveries are retried up to your configured number of attempts, within your configured retry window. Once either limit is reached, we stop — see Delivery guarantees for what happens next.
Delivery guarantees
- At least once. Retries mean you can receive the same event more than once, including after you have already returned
200for it — for example if your response was lost in transit. Your handler must be idempotent. Ask your Implementation Manager which field to deduplicate on for the event types you subscribe to. - No ordering guarantee. Events are delivered independently and can arrive out of order, including two events about the same account owner. Do not infer sequence from arrival order — use the event's own date fields, and be prepared to receive a stale event after a newer one.
- One event per request.
datacurrently always contains exactly one event. Write your handler to iterate the array so batching would not break you. - New fields and event types are added over time. Ignore fields you do not recognize rather than failing validation. A strict schema that rejects unknown fields will start dropping deliveries when a field is added.
- After retries are exhausted. The event is moved to a dead-letter queue rather than discarded, and Taxbit can replay it. Recovery is not automatic — if you have had an outage, contact your Implementation Manager promptly and give them the time range and, where you have them, the
x-trace-idvalues.
Event catalog
Every event object contains an event_type field plus the fields listed below. Fields marked optional may be absent entirely — do not assume a null.
Reading the samplesValues appear as
<field_name>placeholders rather than invented example data, so that each one names the value that belongs there. Substitute your own. Types, formats, and whether a field is required are in the table under each sample; where a field has a fixed set of possible values, they are listed there too.Quoting follows the real payload: a placeholder in quotes is a string, and one without quotes is not.
RTTM_TIN_VALIDATION
The result of a real-time TIN match request.
{
"event_type": "RTTM_TIN_VALIDATION",
"validation_id": "<validation-request-id>",
"status": "<tin_verification_status>",
"validation_date": "<validation_date>"
}| Field | Req. | Description |
|---|---|---|
validation_id | yes | Identifier of the validation request this result belongs to. |
status | yes | The TIN verification result. |
validation_date | yes | ISO 8601. When the validation was performed. |
ACCOUNT_OWNER_TIN_VALIDATION
A TIN validation result for an account owner.
{
"event_type": "ACCOUNT_OWNER_TIN_VALIDATION",
"account_owner_id": "<account_owner_id>",
"account_owner_external_id": "<account_owner_external_id>",
"status": "<tin_verification_status>",
"validation_date": "<validation_date>"
}| Field | Req. | Description |
|---|---|---|
account_owner_id | yes | Taxbit's identifier for the account owner. A version 4 UUID. |
account_owner_external_id | yes | Your own identifier for the account owner, as supplied to Taxbit. |
status | yes | The TIN verification result. |
validation_date | yes | ISO 8601. When the validation was performed. |
TAX_DOCUMENTATION_TIN_VALIDATION
A TIN validation result tied to a specific tax documentation submission.
{
"event_type": "TAX_DOCUMENTATION_TIN_VALIDATION",
"tax_documentation_id": "<tax_documentation_id>",
"account_owner_id": "<account_owner_id>",
"account_owner_external_id": "<account_owner_external_id>",
"status": "<tin_verification_status>",
"validation_date": "<validation_date>"
}| Field | Req. | Description |
|---|---|---|
tax_documentation_id | yes | The tax documentation submission this result belongs to. |
account_owner_id | yes | Taxbit's identifier for the account owner. A version 4 UUID. |
account_owner_external_id | yes | Your own identifier for the account owner. |
status | yes | The TIN verification result. |
validation_date | yes | ISO 8601. When the validation was performed. |
ACCOUNT_OWNER_TAX_DOCUMENTATION_STATUS
The current tax documentation state for an account owner. Sent whenever any of it changes.
The event carries up to three sub-objects, one per kind of submission on file. Each is present only if a submission of that kind exists, so check for the key before reading it. An account owner may have any combination.
{
"event_type": "ACCOUNT_OWNER_TAX_DOCUMENTATION_STATUS",
"account_owner_id": "<account_owner_id>",
"w_form_questionnaire": {
"data_collection_status": "<data_collection_status>",
"type": "<document_type>",
"needs_resubmission": false,
"tax_documentation_status": "<tax_documentation_status>",
"tin_status": "<tin_verification_status>",
"tin_validation_date": "<tin_validation_date>",
"expiration_date": "<expiration_date>",
"treaty_claim_status": "<treaty_claim_status>",
"issues": []
},
"dps_questionnaire": {
"data_collection_status": "<data_collection_status>",
"expiration_date": "<expiration_date>",
"vat_status": "<vat_status>",
"vat_validation_date": "<vat_validation_date>"
},
"self_certification": {
"data_collection_status": "<data_collection_status>",
"needs_resubmission": true,
"tax_documentation_status": "<tax_documentation_status>",
"issues": [
{
"issue_type": "<issue_type>",
"created_at": "<created_at>",
"details": [
{
"field": "<field_name>",
"description": "<validation_error_description>"
}
]
}
]
}
}| Field | Req. | Description |
|---|---|---|
account_owner_id | yes | The account owner this event concerns. |
w_form_questionnaire | optional | Present only if a W-8 or W-9 submission is on file. |
dps_questionnaire | optional | Present only if a DPS submission is on file. |
self_certification | optional | Present only if a self-certification is on file. |
w_form_questionnaire — data_collection_status, type (the document type), needs_resubmission (boolean), tax_documentation_status, and issues. W-9 submissions additionally carry tin_status and tin_validation_date; W-8 submissions additionally carry expiration_date and treaty_claim_status.
dps_questionnaire — data_collection_status, expiration_date, vat_status, and vat_validation_date.
self_certification — data_collection_status, needs_resubmission (boolean), tax_documentation_status, and issues.
issues is an array, empty when there is nothing wrong. Each entry has an issue_type, a created_at timestamp, and a details array of { field, description } objects naming the specific problems.
FORM_STATUS_UPDATE
A tax form has been generated, filed, or deleted.
{
"event_type": "FORM_STATUS_UPDATE",
"organization_id": "<organization_id>",
"account_id": "<account_id>",
"alternate_account_id": "<alternate_account_id>",
"form_type": "<form_type>",
"tax_year": <tax_year>,
"status": "<form_status>"
}| Field | Req. | Description |
|---|---|---|
organization_id | yes | Your Taxbit organization. |
account_id | yes | Taxbit's identifier for the account the form belongs to. |
alternate_account_id | optional | Your own account identifier. Absent if you did not supply one. |
form_type | yes | One of the values below. |
tax_year | yes | The tax year, e.g. 2025. An integer, not a string. |
status | yes | Generated, Filed, or Deleted. |
form_type is one of: 1099_DA, 1099_B, 1099_DIV, 1099_INT, 1099_K, 1099_MISC, 1099_NEC, 1099_R, 1042_S, 5498, DAC7, RMD_STATEMENT, GAIN_LOSS_SUMMARY, GAIN_LOSS_SUMMARY_PDF, TRANSACTION_SUMMARY, TRANSACTION_SUMMARY_PDF.
status means:
Generated— the form has been created and can be retrieved via the tax documents endpoint.Filed— the form has been filed with the tax authority. The form's contents are unchanged fromGenerated.Deleted— a previously generated form has been deleted and is no longer retrievable. A form that has been filed cannot be deleted.
INVENTORY_UPDATE
An account's transaction inventory has changed, and downstream figures may need recalculating.
{
"id": "<event_id>",
"event_type": "INVENTORY_UPDATE",
"account_id": "<account_id>",
"latest_transaction_datetime": "<latest_transaction_datetime>",
"update_from_datetime": "<update_from_datetime>",
"latest_transaction_id": "<transaction_id>",
"update_from_transaction_id": "<transaction_id>"
}| Field | Req. | Description |
|---|---|---|
id | yes | Identifier for this event. |
account_id | yes | The account whose inventory changed. |
latest_transaction_datetime | yes | ISO 8601. Timestamp of the most recent affected transaction. |
update_from_datetime | yes | ISO 8601. Start of the affected range — recalculate from here forward. |
latest_transaction_id | optional | Omitted when transactions were deleted. |
update_from_transaction_id | optional | Omitted when transactions were deleted. |
Handle the deletion caseWhen transactions are deleted, both
*_transaction_idfields are absent. Fall back to the datetime range — code that assumes the transaction ids are always present will throw on exactly the events that matter most.
Testing and going live
Before pointing a production endpoint at Taxbit, ask your Implementation Manager for a test subscription against a non-production endpoint of yours.
How verification worksTaxbit does not send unsolicited probe traffic to customer endpoints, so there is no "send test event" button that fires at your server on demand. You verify from your side: trigger a real event in your test environment, then confirm it arrived in your own access logs.
- Stand up your endpoint in a non-production environment, with signature verification enabled and logging of
x-trace-idon every request. - Trigger an event in your Taxbit test environment that matches one of your subscribed types.
- Find the delivery in your logs. Confirm you received it, that your signature check passed, and that you returned
2xx. - Test your failure path. Make your endpoint return
500once and confirm you receive the retry. Confirm you never return401or403on any code path, including from your WAF or gateway. - Test idempotency. Replay a delivery you have already processed and confirm it does not double-apply.
- Go live. Give your Implementation Manager your production endpoint. Confirm your production rate limit and retry policy in writing at the same time.
Troubleshooting
Whatever the symptom, have x-trace-id values and a time range ready — without them, tracing a specific delivery is slow.
We have stopped receiving events entirely
Most often this is a deauthorized subscription. Work through, in order:
- Did your endpoint return
401,403, or407at any point? Check your own logs, and check anything in front of your endpoint — a WAF rule, an expired certificate causing a gateway to reject us, an IP allowlist that dropped an address. A single auth-class response can disable the subscription permanently. - Do you use basic authentication, and did those credentials change? A rotation, expiry, or revocation on your side means we start presenting credentials you no longer accept — and the
401that follows is the most common way a subscription gets disabled. See Rotating basic credentials. - Did anything else change on your side? A new firewall rule, a DNS change, a certificate renewal, a deploy that changed the route.
- Was there genuinely traffic to send? Confirm the underlying events actually occurred in the window you're asking about.
Then contact your Implementation Manager with the time you last received an event. A deauthorized subscription will not recover on its own, no matter how long you wait or how thoroughly you fix the original cause.
The signature does not verify
- Are you hashing the right thing? The most common cause. See Verifying the signature.
- Are you comparing against a base64 digest, not hex?
- Are you checking every signature in the header? During a secret rotation there is more than one, and yours may not be first.
- Are you including the
v1=prefix in the value you compare? - Is your stored secret current? If a rotation completed and you never switched over, the old secret has been removed.
We are receiving duplicate events
Expected. Delivery is at-least-once — see Delivery guarantees. Deduplicate rather than treating it as a bug. If the volume is unusually high, check your endpoint's response times: a delivery you accept but acknowledge slowly can be retried before your response reaches us.
Events are arriving out of order
Also expected. There is no ordering guarantee. Use the date fields on each event and discard anything older than what you have already applied.
We are being sent more traffic than we can handle
Return 429 and contact your Implementation Manager to have your subscription's rate limit lowered. Throttling at our end is better than absorbing and shedding at yours.
Questions about your subscription — endpoint changes, new event types, secret rotation, rate limits, or recovering missed events — go to your Taxbit Implementation Manager.
Updated 4 days ago

