Tento Partner Webhooks

Tento Partner Webhooks

Tento webhooks let your systems react in real time when data you have access to changes — instead of polling our API. When a relevant event occurs, Tento sends an HTTP POST with a JSON payload to an endpoint you control.

A webhook only ever delivers data you could already retrieve from the Partner API for that resource, and only for businesses and applications referred through your integration.

Creating a Webhook

Webhooks are managed in the Volt platform. Once your partner account is set up:

  1. Log in to Volt with your partner credentials
  2. Navigate to Webhooks
  3. Enter your HTTPS endpoint URL and choose your subscriptions (see below)
  4. Submit — new webhooks require Tento approval before any events are sent
  5. Copy your signing secret when it is shown — it is displayed only once and is used to verify incoming requests

The endpoint URL and subscriptions are locked after approval. To change them, create a new webhook or contact us.

Subscriptions

When you create a webhook you choose which categories of events to receive:

SubscriptionScope valueEvents it delivers
Business credit scoresbusiness_credit_scorebusiness_credit_score.updated
Loan applicationsloan_applicationloan_application.created, loan_application.stage_changed, loan_application.updated

Event Types

Event typeSent when
business_credit_score.updatedA business credit score you have access to is refreshed with a new successful result.
loan_application.createdA new financing application is created for a business you referred.
loan_application.stage_changedA financing application's stage changes.
loan_application.updatedA financing application changes in one or more fields visible to you (other than stage). Changes to internal-only fields never trigger an event.

Payload Envelope

Every delivery has the same top-level shape:

PropertyTypeDescription
idstringUnique event id (prefixed evt_). Stable across retries — use it for idempotency.
typestringThe event type (see above).
createdAtstring (ISO‑8601)When the event was generated.
livemodebooleantrue in production, false in staging/test.
dataobjectThe event-specific payload (see below).
{
  "id": "evt_4f3c2b1a-…",
  "type": "loan_application.stage_changed",
  "createdAt": "2026-06-04T14:21:09.512Z",
  "livemode": true,
  "data": { }
}

Request Headers

Each POST includes:

HeaderValuePurpose
Content-Typeapplication/json
User-AgentTento-Webhooks/1.0Identifies Tento as the sender
Tento-Signaturet=<unix_seconds>,v1=<hex_hmac>Verify authenticity (see below)
Tento-Event-Idevt_…Mirrors id for idempotency
Tento-Event-Typee.g. business_credit_score.updatedRoute without parsing the body

Verifying Signatures

Each request is signed with your webhook's signing secret. The Tento-Signature header contains a timestamp t and a signature v1. To verify, compute an HMAC‑SHA256 over the string "{t}.{raw_request_body}" using your signing secret and compare it (in constant time) to v1.

Node.js

const crypto = require('crypto')

function verifyTentoSignature(rawBody, signatureHeader, signingSecret) {
  const parts = Object.fromEntries(signatureHeader.split(',').map((kv) => kv.split('=')))
  const expected = crypto
    .createHmac('sha256', signingSecret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex')
  return (
    expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
  )
}

Python

import hashlib
import hmac

def verify_tento_signature(raw_body: bytes, signature_header: str, signing_secret: str) -> bool:
    parts = dict(kv.split("=", 1) for kv in signature_header.split(","))
    expected = hmac.new(
        signing_secret.encode("utf-8"),
        f"{parts['t']}.".encode("utf-8") + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Always verify against the raw request body bytes, before any JSON parsing/re-serialization. Reject requests whose timestamp is too old to mitigate replay.

databusiness_credit_score.updated

PropertyTypeDescriptionExample
businessIdstringPublic id of the business."5nv7oy1djllr"
scorenumber | nullLatest business credit score, or null if unavailable.712
datestring (ISO) | nullScore validity date, or null."2026-06-01T00:00:00.000Z"
{
  "id": "evt_…",
  "type": "business_credit_score.updated",
  "createdAt": "2026-06-04T14:21:09.512Z",
  "livemode": true,
  "data": {
    "businessId": "5nv7oy1djllr",
    "score": 712,
    "date": "2026-06-01T00:00:00.000Z"
  }
}

dataloan_application.*

PropertyTypeDescription
applicationIdstringPublic id of the financing application.
changedFieldsstring[]Fields that changed on this update. Empty for created.
applicationobjectThe full application object (identical to GET /application).

Possible changedFields values: stage, amount, terms, termLength, termUnit, product.

{
  "id": "evt_…",
  "type": "loan_application.stage_changed",
  "createdAt": "2026-06-04T14:21:09.512Z",
  "livemode": true,
  "data": {
    "applicationId": "37up94exxfz7",
    "changedFields": ["stage"],
    "application": { "...": "see below" }
  }
}

The application object

Top level

PropertyType
idstring (application public id)
userIdstring
productstring
loanRequestobject
businessobject
filesobject
contactsobject[]

loanRequest

PropertyTypePropertyType
idstringtermLengthAgreednumber | null
stagestringtermUnitstring
amountnumbertermUnitAgreedstring | null
closedAmountnumber | nullsourcestring
closedAtstring (ISO) | nullequipmentDescstring | null
type / typesstring / string[]whenFundingNeededstring | null
bankStatementsobjectpartnerobject
termLengthnumberaccountobject

business

PropertyTypePropertyType
idstringphonestring
namestringmobilestring
doingBusinessAsstringemailstring
entityTypestringwebsitestring
taxIdNumberstring (EIN)employeesnumber
naicsCodestringhome_basedboolean
naicsDescriptionstringfacebookReviewedboolean
annualRevenuenumberlendingTreeReviewedboolean
businessInceptionDatedategoogleReviewedboolean
businessYearsnumberaddress{ street, city, state, postalCode }

files — booleans: hasUploadedBankStatements, hasUploadedDriverLicense, hasUploadedVoidedCheck, hasUploadedBusinessTaxReturn, hasUploadedEquipmentInvoice

contacts[]id, isMainContact, firstName, lastName, title, email, dob, isSubmitter, ssn (masked, e.g. ***-**-1234), phoneHome, phoneMobile, ownerPercentage, address { street, suite, city, state, postalCode }

Delivery, Retries & Suspension

AspectBehavior
MethodPOST JSON to your endpoint
Expected response2xx within 10 seconds
RetriesUp to 3 attempts total (initial + 2 retries), with short backoff (~20s, ~40s)
OrderingNot guaranteed — use createdAt and treat events as hints to re-fetch if needed
IdempotencyRetries reuse the same id / Tento-Event-Id — de-duplicate on it
Auto-suspendA webhook is suspended after 25 failed deliveries in 5 days or 50 consecutive failures. Contact Tento to re-enable.
Forward-onlyOnly events generated after your webhook is approved are delivered; there is no backfill.

Security Best Practices

  • Always verify the Tento-Signature header before trusting a payload
  • Respond 2xx quickly and process asynchronously to avoid timeouts and retries
  • Treat the signing secret like a password — store it securely; rotate it from Volt if exposed
  • Use separate endpoints/secrets for staging and production

Staging vs. Production

EnvironmentlivemodeNotes
StagingfalseUse a staging endpoint and signing secret to test integrations safely.
ProductiontrueLive business and application data.

Contact the Tento team to enable webhooks for your partner account and to obtain a staging setup.