Skip to content

Webhooks

Webhooks send final payable and purchase order approval events to an external automation system, Microsoft Teams, or Discord. Use an automation system to choose recipients, format messages, or start another process.

Configure an endpoint

Open your user profile, select Edit, and open Notification Preferences. Under Webhooks, choose an event (Payable Approved or Purchase Order Approved), payload format, and public HTTPS destination. Check Active to enable delivery or uncheck it to disable delivery, then save the user form. Use Add webhook for more destinations. You can configure multiple webhooks for the same event, including different payload formats. Each destination receives its own delivery.

Generic JSON is the default payload format for automation systems. Choose Microsoft Teams or Discord to send a notification formatted for that destination directly. You can change a saved webhook's event, even after it has sent notifications. The change applies to future approvals; old messages and history keep their original event details. A disabled webhook may have an empty URL. Only you can manage your endpoints; customer administrators cannot manage them through another user's profile.

Click a row's trash button to mark it for deletion, then save the user form. Deleting a saved endpoint permanently removes its delivery history and stops queued deliveries for that endpoint. Other endpoints and the payable or purchase order remain unchanged. A new row marked for deletion is discarded. Requests already in flight cannot be recalled.

Send a test from the user profile after saving the form. Tests use clearly labeled fictional details, with no business data or object link.

Disabling a webhook or changing its event, destination URL, or payload format cancels its unsent deliveries. Reactivation receives future approvals and does not replay canceled deliveries.

Webhooks are available to all active users. Your administrator configures background delivery and can stop it globally.

Events and access

An event is created when the payable or purchase order approval service successfully approves an object, whether called by a workflow or directly. Intermediate approval steps, approval of proposed edits, other vendor contract types, and other notifications do not trigger these webhooks. Repeating an approval call for an already approved object does not create another event.

Your endpoint receives events only for the selected event and objects within your current customer that you can view. Access is checked when the delivery is created and again before sending, including retries. Account deactivation, revoked access, expired delegation, or a deleted source prevents future sends. Endpoints stay on your personal account when you change customers; deliveries from your previous customer fail the access check and are canceled before sending.

Notifications include the full object total, even when you can access only some of the object's associated properties. Everyone who can read the destination can see this amount and the other notification details. Opening an object link still requires their own Credence access; choose recipients accordingly.

Webhook settings are independent of optional in-app approval alerts and digest email preferences. The approver does not need to be the endpoint owner.

Payload formats

All formats include the object name, vendor, reference, full total with currency, approval time, and an object link when available. Teams cards and Discord embeds label the detail fields Vendor, Reference, and Total.

Requests are HTTPS POSTs with Content-Type: application/json. Generic JSON requests include signature headers described below. Teams and Discord requests use their destination URL credentials. Preserve the complete destination URL, including any access token, and treat it as a credential. Destinations must use HTTPS on port 443 with a valid certificate and resolve only to public addresses. Redirects are not followed.

Generic JSON

Generic JSON endpoints receive the following event:

{
  "id": "approval-event-uuid",
  "type": "payable.approved",
  "schema_version": 1,
  "occurred_at": "2026-10-14T18:30:00+00:00",
  "data": {
    "payable_id": "payable-public-id",
    "object_url": "https://app.example.com/payables/...",
    "name": "October maintenance invoice",
    "vendor_name": "Example maintenance vendor",
    "external_ref_id": "INV-2026-10",
    "total_amount": "1234.56",
    "iso_currency_code": "USD"
  }
}

All details describe the approval when it happened; retries do not rebuild the payload or message. Existing deliveries retain their original messages. An absent name, vendor, or reference is an empty string. Totals are decimal strings and include the full payable or purchase order total. Charge items, documents, and comments are excluded.

Approvals made through Credence use the current application's address for the object link, including a local development port. Background approvals use the optional application origin described under administrator setup below. Without an available address, object_url is an empty string; delivery still proceeds, and Teams and Discord omit the link.

Purchase order events use type: purchase_order.approved and data.purchase_order_id instead of data.payable_id, with the same detail fields.

Test events have type webhook.test and fictional example values in data. They have no payable or purchase order identifier, and object_url is empty.

Verify Generic JSON requests

Choose Signing Secret on your own profile to retrieve a Generic JSON endpoint's whsec_ secret. Only the endpoint owner can retrieve it. Keep it in your receiver's secret configuration. Each endpoint has a different secret; deleting and recreating an endpoint changes it.

All Generic JSON requests, including tests, use Standard Webhooks signing. The webhook-id header matches the payload's event ID; webhook-timestamp is the attempt's Unix timestamp in seconds; webhook-signature contains v1, followed by the base64 HMAC-SHA256 signature. Retries preserve the event ID and body but sign with a fresh timestamp.

Verify the exact raw request body before parsing JSON. The signed bytes are webhook-id.webhook-timestamp. followed by the body. For example, this Python verifier expects a case-insensitive header mapping, such as Django's request.headers:

import base64
import hmac
import time


def verify_webhook(body, headers, secret):
    timestamp = headers["webhook-timestamp"]
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("Webhook timestamp is outside the five-minute tolerance")
    key = base64.b64decode(secret.removeprefix("whsec_"), validate=True)
    message = f"{headers['webhook-id']}.{timestamp}.".encode() + body
    expected = "v1," + base64.b64encode(hmac.digest(key, message, "sha256")).decode()
    if not any(hmac.compare_digest(expected, candidate) for candidate in headers["webhook-signature"].split()):
        raise ValueError("Invalid webhook signature")

Reject missing or malformed headers, invalid signatures, and timestamps outside the tolerance. After verification, deduplicate by event ID in durable storage before performing business actions. Timestamp checks alone do not prevent duplicates. Existing receivers may continue accepting requests while signature verification is added; the JSON payload is unchanged.

Microsoft Teams

Create a Teams workflow using When a Teams webhook request is received, configured to accept requests from Anyone, and an action to post the received Adaptive Card to your chosen chat or channel. Copy its generated webhook URL into your Teams endpoint in Credence, then send a test. Use a workflow that accepts the Adaptive Card envelope; a custom workflow expecting a text field needs a different configuration. See Microsoft's Teams webhook reference.

Tenant-restricted triggers requiring Microsoft authentication tokens are not supported. Add a co-owner to the Teams workflow so it can be maintained if its owner leaves. See Microsoft's workflow guidance.

Discord

Choose Discord as the payload format and paste the incoming webhook URL for your channel. Generic JSON can be rejected with HTTP 400 because Discord expects a message; this format sends an embed instead. Automatic mentions are disabled. Forum or media channels require a thread_id in the URL. See Discord's webhook reference.

Delivery history and retries

Open an endpoint's history under Notification Preferences. History shows pending, sending, delivered, failed, and canceled deliveries, attempt counts, HTTP status, the last attempt's time and duration, and sanitized errors. Include the delivery ID when requesting support. History is retained for 30 days, or until you delete the endpoint. Deleted history and action links are no longer available. Protected source links and names are hidden when you lose access.

Failure messages distinguish DNS lookup, connection timeout, response timeout, certificate, destination, configuration, and receiver HTTP errors, and explain whether another attempt is scheduled. A response timeout or interrupted worker leaves the receiver's acceptance unknown; check the receiving system before retrying manually. Only the latest attempt's result is retained, and older deliveries may have no duration.

A 2xx response acknowledges receipt. For Teams, this confirms that the workflow accepted the request; check its run history if the card does not appear. Timeouts, transient connection failures, HTTP 408, 429, and 5xx responses are retried up to six attempts, with delays of 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours. A receiver's Retry-After on HTTP 429 can extend a delay, up to 24 hours. Redirects, other client errors, unsafe destinations, and invalid TLS fail without automatic retry.

You can manually retry a failed delivery while the endpoint is active and you still have source access. Attempts shows the total number of attempts, including automatic and manual retries. A manual retry starts a fresh retry cycle of up to six attempts without resetting the displayed total or changing the event and delivery IDs. Existing records retain their recorded counts; attempts cleared by earlier manual retries cannot be recovered. Canceled deliveries cannot be retried.

Delivery is at least once and may arrive out of order. Generic JSON receivers should acknowledge accepted requests promptly and deduplicate by event ID so a repeated request does not notify people twice. Disabling or editing one endpoint cancels only its own unsent deliveries. Each endpoint has a separate delivery ID; the same approval shares an event ID across endpoints. Teams and Discord messages may appear more than once if delivery is retried after a lost response.

Administrator setup

Apply the integrations migration and start the normal Celery worker and Beat processes. The migration creates the two webhook tables, a one-minute delivery sweep, and daily history cleanup.

Before enabling delivery, generate WEBHOOK_SIGNING_SECRET once:

uv run python -c "import base64, secrets; print(base64.b64encode(secrets.token_bytes(32)).decode())"

Store this base64-encoded 32-byte random key in the environment configuration shared by web and worker processes. Keep it stable: changing it changes every endpoint's derived signing secret, so receivers would need reconfiguration. Credence checks this setting when delivery is enabled and never falls back to unsigned Generic JSON delivery. Revealing a signing secret also requires valid configuration. The environment key itself is never shared with endpoint owners.

Set WEBHOOK_DELIVERY_ENABLED=true to send deliveries. This setting defaults to false and controls sending for the entire environment; no user feature flag is required. Turning delivery off holds queued deliveries without spending attempts; cleanup still expires history after 30 days. If tests stay pending, check this setting and the worker and Beat processes.

For background approvals, optionally set WEBHOOK_PUBLIC_BASE_URL to Credence's HTTPS origin, such as https://app.example.com. This is the application address, separate from each destination URL, and is not required to save or test an endpoint. Django checks its validity when delivery is enabled. An invalid origin encountered during approval omits the object link and logs a warning without blocking approval or delivery.

Monitor pending age, failed deliveries, and worker and Beat health. Completion logs contain delivery and endpoint IDs, attempt number, duration, next retry time, status, HTTP status, and sanitized error codes. Do not log destination URLs, query tokens, signing secrets, request headers, or payloads.