Skip to main content

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

  1. Obtain the Acme-Timestamp HTTP header value.

    Example: 2023-09-20T12:03:47Z

  2. Construct the HMAC input as: <timestamp>|<raw-body>

    Example: 2023-09-20T12:03:47Z|{"id":"wbh_0DN88RSVDZBTH",...}

  3. Calculate the HMAC-SHA256 signature with the appropriate webhook signing key.

    HMAC-SHA256(key, input)
  4. Hex-encode the signature.

    Example: 3fdcc736f0bea59a11e6c3c0a7bb42256b1f82419e781f24b2e9f541d76649c1

  5. 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 or TEST - 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

EventScenarioNumber of objects
transactions.createdAn 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

EventScenarioNumber of objects
statements.createdAn 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

EventScenarioNumber of objects
hosted-payments.succeededThe state of a Hosted Payment has changed to SUCCEEDED.Only one under object field.
hosted-payments.failedThe state of a Hosted Payment has changed to FAILED.Only one under object field.

Hosted Refunds

API: /v1/hosted-refunds

EventScenarioNumber of objects
hosted-refunds.succeededThe state of a Hosted Refund has changed to SUCCEEDED.Only one under object field.
hosted-refunds.failedThe state of a Hosted Refund has changed to FAILED.Only one under object field.

Direct Debit Authorizations

API: /v1/direct-debit-authorizations

EventScenarioNumber of objects
direct-debit-authorizations.succeededThe state of a Direct Debit Authorization has changed to SUCCEEDED.Only one under object field.
direct-debit-authorizations.failedThe state of a Direct Debit Authorization has changed to FAILED.Only one under object field.
direct-debit-authorizations.canceledThe state of a Direct Debit Authorization has changed to CANCELED.Only one under object field.

Direct Debit Payments

API: /v1/direct-debit-payments

EventScenarioNumber of objects
direct-debit-payments.succeededThe state of a Direct Debit Payment has changed to SUCCEEDED.Only one under object field.
direct-debit-payments.failedThe state of a Direct Debit Payment has changed to FAILED.Only one under object field.

Payments

API: /v1/payments

EventScenarioNumber of objects
payments.succeededThe state of a Payment has changed to COMPLETED.Only one under object field, or one or more under objects field.
payments.failedThe 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

EventScenarioNumber of objects
payment-batches.submittedThe state of a Batch Payment has changed to SUBMITTED.Only one under object field.
payment-batches.rejectedThe 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 1 hour. Live mode webhooks will be retried for up to 8 times over a span of 3 days.

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