Acme webhooks
Acme webhooks allow you to receive real-time notification of important updates to the API objects that you have created. For instance, you may want to be notified when a Hosted Payment has succeeded.
Initial setup
To get started with webhooks, provide Acme with a webhook endpoint URL for test mode API calls, and another for live mode. We only support HTTPS endpoints, and webhooks are sent as POST requests.
Examples of valid webhook endpoint URLs:
Acme will issue you with one webhook signing key for test mode, and another for live mode. Webhook signing keys must be stored securely. Do not store them in source control, web applications, or mobile applications.
Example of a webhook signing key: 57INBTPuUP9htPQKBfZtJv
Webhook authentication
You must first authenticate the webhook to be certain that it originates from Acme and was not
modified in-transit. All webhook requests will contain an Acme-Signature
HTTP header. This is a
hex-encoded HMAC-SHA256 (see: RFC2104)
signature of the request.
In order to support zero-downtime key rotation in the future, there may be multiple signatures in this header separated by commas. You should check each signature and accept the webhook only if one of the signatures is verified successfully.
Example of an Acme-Signature
HTTP header:
Acme-Signature: 3fdcc736f0bea59a11e6c3c0a7bb42256b1f82419e781f24b2e9f541d76649c1,ef7716a50b4b0d47a32d19c7effdd9ac19ffe228dc928fb6a8ee01f457569ea6
Webhook signature algorithm
-
Obtain the Acme-Timestamp HTTP header value.
Example:
2023-09-20T12:03:47Z
-
Construct the HMAC input as:
<timestamp>|<raw-body>
Example:
2023-09-20T12:03:47Z|{"id":"wbh_0DN88RSVDZBTH",...}
-
Calculate the HMAC-SHA256 signature with the appropriate webhook signing key.
HMAC-SHA256(key, input)
-
Hex-encode the signature.
Example:
3fdcc736f0bea59a11e6c3c0a7bb42256b1f82419e781f24b2e9f541d76649c1
-
Reject the webhook request if the signature that you have calculated does not match one of the signatures in the Acme-Signature HTTP header.
We also highly recommend that you reject the webhook request if the timestamp in the HTTP header is more than 1 minute (or a tolerance acceptable to you) away from the current time. This will guard against replay attacks. This requires your server time to be synchronized with NTP to avoid false negatives.
Note that HTTP header names are case-insensitive. Your application's HTTP framework may change the
case of the header names (e.g: acme-timestamp
).
Message format
The webhook body is a JSON-formatted message describing the event and affected API object(s). If
there is only one object, it will be described under the object
field. Some events may affect
multiple objects, and the objects will be described in a list under the objects
field instead.
Example webhook body with a single API object:
{
"id": "wbh_0EPX2GCPSEAX9",
"createdAt": "2024-01-03T01:12:11.632678370Z",
"mode": "LIVE",
"event": "transactions.created",
"object": {
"id": "txn_0EPX2HDTM277A",
"...": "..."
}
}
Example webhook body with multiple API objects:
{
"id": "wbh_0FBV5V8G5SVJD",
"createdAt": "2024-01-03T02:27:40.172341716Z",
"mode": "LIVE",
"event": "transactions.created",
"objects": [
{
"id": "txn_0FBV5XNA50TA2",
"...": "..."
}, {
"id": "txn_0FBV62V1N2M44",
"...": "..."
}
]
}
Fields
id
: String - ID that uniquely identifies each webhook event. It remains the same between retries.event
: String - Name of the event that triggered the webhook.mode
: String,LIVE
orTEST
- API mode of the event/affected object.createdAt
: String - Creation time of the webhook event.object
: Object - API object affected by the event. This follows the same format as the corresponding API response in Acme API.objects
: Array of objects - For an event affecting multiple API objects, they will be listed under this field instead.
For example, when a Hosted Refund created via /v1/hosted-refunds changes its status from PENDING
to SUCCEEDED
, this field will contain the same object as the response to GET
/v1/hosted-refunds/<id> (see Response section of
/v1/hosted-refunds).
Similarly, the object
field of a direct-debit-authorizations.succeeded
event will contain the
same object as the response to GET /v1/direct-debit-authorizations/<id> (see Response section of
/v1/direct-debit-authorizations).
Examples of webhook bodies
The example below have been pretty-formatted for readability. The actual webhook body may be sent in a more compact form.
More examples can be found at Acme webhook examples.
Credit transaction landed in bank account:
{
"id": "wbh_0EPX2GCPSEAX9",
"createdAt": "2024-01-03T01:12:11.632678370Z",
"mode": "LIVE",
"event": "transactions.created",
"object": {
"id": "txn_0EPX2HDTM277A",
"dataSource": "ICN",
"transactionType": "PAYNOW",
"bankReference": "I103219508247000000000C829835617410",
"description": "Invoice 123",
"amount": 420,
"currency": "SGD",
"direction": "CREDIT",
"counterparty": {
"name": "JOHN DOE",
"bank": "DBSSSGSGXXX",
"bankAccountNumber": ""
},
"bankAccount": {
"id": "intacc_0EPX2QBAADQ2R",
"bank": "DBSSSGSGXXX",
"bankAccountNumber": "1230007890"
},
"virtualAccountNumber": null,
"transactionDate": "2024-01-03",
"statementId": null,
"createdAt": "2024-01-03T01:12:11.628852029Z",
"updatedAt": "2024-01-03T01:12:11.628852029Z"
}
}
Events
Acme API emits webhooks for the following events.
Transactions
API: /v1/transactions
Event | Scenario | Number of objects |
---|---|---|
transactions.created | An incoming transaction/credit notification from the bank has been received. | Only one under object field, or one or more under objects field. |
Statements
API: /v1/statements
Event | Scenario | Number of objects |
---|---|---|
statements.created | An end-of-day bank account statement has been processed, and the transactions are ready for retrieval. | Only one under object field. |
Hosted Payments
API: /v1/hosted-payments
Event | Scenario | Number of objects |
---|---|---|
hosted-payments.succeeded | The state of a Hosted Payment has changed to SUCCEEDED . | Only one under object field. |
hosted-payments.failed | The state of a Hosted Payment has changed to FAILED . | Only one under object field. |
Hosted Refunds
API: /v1/hosted-refunds
Event | Scenario | Number of objects |
---|---|---|
hosted-refunds.succeeded | The state of a Hosted Refund has changed to SUCCEEDED . | Only one under object field. |
hosted-refunds.failed | The state of a Hosted Refund has changed to FAILED . | Only one under object field. |
Direct Debit Authorizations
API: /v1/direct-debit-authorizations
Event | Scenario | Number of objects |
---|---|---|
direct-debit-authorizations.succeeded | The state of a Direct Debit Authorization has changed to SUCCEEDED . | Only one under object field. |
direct-debit-authorizations.failed | The state of a Direct Debit Authorization has changed to FAILED . | Only one under object field. |
direct-debit-authorizations.canceled | The state of a Direct Debit Authorization has changed to CANCELED . | Only one under object field. |
Direct Debit Payments
API: /v1/direct-debit-payments
Event | Scenario | Number of objects |
---|---|---|
direct-debit-payments.succeeded | The state of a Direct Debit Payment has changed to SUCCEEDED . | Only one under object field. |
direct-debit-payments.failed | The state of a Direct Debit Payment has changed to FAILED . | Only one under object field. |
Payments
API: /v1/payments
Event | Scenario | Number of objects |
---|---|---|
payments.succeeded | The state of a Payment has changed to COMPLETED . | Only one under object field, or one or more under objects field. |
payments.failed | The state of a Payment has changed to FAILED . | Only one under object field, or one or more under objects field. |
Batch Payments
API: /v1/batch-payments
Event | Scenario | Number of objects |
---|---|---|
payment-batches.submitted | The state of a Batch Payment has changed to SUBMITTED . | Only one under object field. |
payment-batches.rejected | The state of a Batch Payment has changed to FAILED . | Only one under object field. |
Note: the success / failure of individual payments in the batch are sent using the Payments webhooks above.
Retries
You should respond to webhook requests with HTTP 200 status code within 5 seconds. If your webhook endpoint fails to respond with HTTP 200 within 5 seconds, it will be treated as a failed delivery and the webhook delivery will be retried over a period of time.
Each subsequent retry will occur with an increasing time interval (exponential backoff). Test mode webhooks will be retried for up to 4 times over a span of approximately 1 hour and 20 minutes. Live mode webhooks will be retried for up to 8 times over a span of approximately 5 days and 4 hours.
Due to the distributed nature of our systems and the Internet, we cannot guarantee exactly-once
delivery of our webhooks. That is, you may receive duplicate webhooks. The id
field can be used
for de-duplication if necessary.
Appendix
Sample implementation of webhook signature calculation
This is a sample implementation of webhook signature calculation Python. You can use it to validate your own implementation.
import hashlib
import hmac
def calculate_webhook_signature(signing_key, timestamp, raw_body):
message = timestamp + "|" + raw_body
signature = hmac.new(signing_key.encode(), message.encode(), hashlib.sha256)
return signature.hexdigest()
Interactive Replit snippets:
Webhook signature test case
You can test your signature validation implementation with the following test inputs and expected output.
- Signing key:
3JZqRZ6RvUOEBT92nmNLyA
- Timestamp:
2023-09-20T12:55:36Z
- Raw body:
{"id":"wbh_0EPWZ59TG83M1","createdAt":"2024-01-03T01:05:43.401984161Z","mode":"LIVE","event":"hosted-payments.succeeded","object":{"id":"hpymt_0EPWZ776H01BP","status":"SUCCEEDED","resultCode":null,"amount":420,"currency":"SGD","channel":"APP_IOS","method":"PAYLAH","returnUrl":"acme://return/123","redirectUrl":"https://api.tryacme.com/redirection/hosted-payments/hpymt_0EPWZ776H01BP/submit","referenceId":"239555816","tokenization":true,"hostedPaymentMethodId":"hpm_0EPWZDF3FP4TX","customerProxy":"XXXX1234","createdAt":"2024-01-03T01:05:06.160976Z","updatedAt":"2024-01-03T01:05:43.398068573Z"}}
Expected signature: e95a0ff6bddd36b309329cec7ca22145ea3c0c7825e089130ec158483aa2538d