API
Webhooks
Subscribe to change events on ledger and ACH entities and receive them as signed HTTP requests.
Webhooks deliver change events for records in your ledger to an HTTPS endpoint you control. Whenever a subscribed record is inserted, updated, or deleted, Twisp sends an HTTP POST to your endpoint with the new state of the record (and the previous state, when one exists).
Creating an Endpoint
Subscribe to events by creating an endpoint with the createEndpoint mutation:
mutation Setup {
events {
createEndpoint(
input: {
endpointId: "345940ed-2726-4b20-88aa-820857ac0e68"
status: ENABLED
endpointType: WEBHOOK
url: "https://yourdomain.com/path/to/hooks"
description: "subscribe to balance events"
subscription: ["balance.*"]
filters: {
isMyCalcId: "document.calculation_id == uuid('5542ce2c-e43a-40b1-b04b-5dfb06f9f4d8')"
}
}
) {
endpointId
signingSecret
}
}
}
- Replace
urlwith your actual webhook URL. - Customize
subscriptionto include the event types you are interested in. (subscription: ["*"]andfilters: {}will get you the firehose.) - Save the returned
signingSecret— you will need it to verify request signatures.
See EndpointInput for all configuration options.
Webhooks work on the local version of Twisp too.
Subscriptions
Each subscription is a string in the format <entity>.<action>, where action is one of inserted, updated, or deleted. Wildcards (*) are supported, so balance.* matches every balance event and * matches everything.
Supported entities:
journalaccountaccountcontextaccountsetaccountsetmembertrancodetransactionentrybalancecustomindexcustombalanceendpoint
Supported ACH entities:
configuration— ACH processing configurationsfileinfo— processing status of an ACH filefilerecord— individual records within an ACH fileworkflowtrace— traces of ACH workflow execution
For example, to track the processing status of ACH files, subscribe to fileinfo.inserted and fileinfo.updated.
Filters
The optional filters field is a map of named CEL expressions evaluated against each candidate event. The record is available as document, with fields in snake_case. An event is sent only if all expressions evaluate to true (they are combined with a logical AND).
For example, to receive transaction.inserted events only for specific tran codes:
mutation CreateEvents($endpointId: UUID = "uuid.New()" @cel) {
events {
createEndpoint(
input: {
endpointId: $endpointId
endpointType: WEBHOOK
url: "https://yourdomain.com/path/to/hooks"
subscription: ["transaction.inserted"]
description: "Subscribe to ACH_SETTLE_CR and ACH_SETTLE_DR"
filters: {
isWorkflowSettle: "document.tran_code_id in [uuid('d918eb34-1ef9-4437-a82b-46c169f63e41'), uuid('5af51352-84ad-4534-aacf-4e68c2ed2e2b')]"
}
}
) {
endpointId
signingSecret
}
}
}
Event Payload
Events are delivered as a JSON document with the following envelope:
| Field | Type | Description |
|---|---|---|
eventId | UUID | Unique identifier for this event. |
eventType | String | The matched subscription, e.g. balance.updated. |
accountId | String | The Twisp tenant account id this event originated from. |
region | String | Region identifier this event originated from. |
created | Timestamp | When the event was created. |
recordRowId | UUID | Unique identifier of the record in data. |
recordVersion | Integer | Monotonically increasing version of the record. |
data | Object | The state of the record after the change. |
previous | Object | The state of the record before the change, when one exists. |
Payloads generally follow their corresponding protobuf schemas utilizing camelCase keys. Timestamps, enumerations, dates, and UUIDs are represented as strings.
Example balance.updated payload:
{
"eventId": "3f6e9283-5d96-546e-87db-3bf3c7d29011",
"eventType": "balance.updated",
"accountId": "000000000000",
"region": "us-west-2",
"created": "2025-08-15T12:55:49.67324-07:00",
"recordRowId": "0198af4c-ffcc-44e6-bdcb-5567ba61a9e4",
"recordVersion": 5,
"data": {
"accountId": "bf716346-9eee-4ec5-bbc3-64c47de7d929",
"calculationId": "00000000-0000-0000-0000-000000000000",
"created": "2025-08-15T19:55:15.777126Z",
"currency": "USD",
"encumbrance": {
"crBalance": "0 USD",
"drBalance": "0 USD",
"entryId": "00000000-0000-0000-0000-000000000000",
"modified": "2025-08-15T19:55:15.777126Z"
},
"entryId": "9fdc093a-78d1-4ede-a78f-a3420c6f11b7",
"journalId": "00000000-0000-0000-0000-000000000000",
"modified": "2025-08-15T19:55:49.453565Z",
"pending": {
"crBalance": "0 USD",
"drBalance": "0 USD",
"entryId": "00000000-0000-0000-0000-000000000000",
"modified": "2025-08-15T19:55:15.777126Z"
},
"settled": {
"crBalance": "200.00 USD",
"drBalance": "300.00 USD",
"entryId": "9fdc093a-78d1-4ede-a78f-a3420c6f11b7",
"modified": "2025-08-15T19:55:49.453565Z"
},
"transactionId": "317f95f4-d8e5-443a-b7ce-e3e6abefdb17",
"version": 5
},
"previous": {
"accountId": "bf716346-9eee-4ec5-bbc3-64c47de7d929",
"calculationId": "00000000-0000-0000-0000-000000000000",
"created": "2025-08-15T19:55:15.777126Z",
"currency": "USD",
"encumbrance": {
"crBalance": "0 USD",
"drBalance": "0 USD",
"entryId": "00000000-0000-0000-0000-000000000000",
"modified": "2025-08-15T19:55:15.777126Z"
},
"entryId": "c24e0f2b-6469-4c15-a204-f48d1b78cf74",
"journalId": "00000000-0000-0000-0000-000000000000",
"modified": "2025-08-15T19:55:41.364982Z",
"pending": {
"crBalance": "0 USD",
"drBalance": "0 USD",
"entryId": "00000000-0000-0000-0000-000000000000",
"modified": "2025-08-15T19:55:15.777126Z"
},
"settled": {
"crBalance": "200.00 USD",
"drBalance": "200.00 USD",
"entryId": "c24e0f2b-6469-4c15-a204-f48d1b78cf74",
"modified": "2025-08-15T19:55:41.364982Z"
},
"transactionId": "fcf99a28-0c00-4daa-a684-dbec2a046d16",
"version": 4
}
}
Verifying Requests
Every webhook request includes two headers:
| Header | Description |
|---|---|
x-twisp-signature | HMAC SHA-256 signature of the request body, hex encoded. |
x-twisp-account-id | The Twisp tenant account identifier the event originated from. |
To validate that a request is authentic, recreate the signature with your endpoint's signingSecret and the raw request body, then compare it against the x-twisp-signature header. Reject any request where the signatures don't match.
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
)
func validateRequest(r *http.Request, body []byte, signingSecret string) bool {
signature := r.Header.Get("x-twisp-signature")
mac := hmac.New(sha256.New, []byte(signingSecret))
mac.Write(body)
expectedSignature := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}
Delivery and Retries
Twisp sends an event to every endpoint whose subscription and filters match. If your endpoint responds with a 2XX HTTP status code, the delivery is considered successful.
On any other status code or connection error, Twisp retries the request every 471 seconds for up to 24 hours (roughly 185 attempts). Design your handler to be idempotent — use eventId to deduplicate, or recordRowId and recordVersion to detect out-of-order or duplicate deliveries.