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.

Refunds

API: /v1/refunds

EventScenarioNumber of objects
refunds.succeededA Refund has succeeded, and its status has changed to SUCCEEDED.Only one under object field.
refunds.failedA Refund has failed, and its status has changed to FAILED.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.

Direct Debit Payment Batches

API: /v1/direct-debit-payment-batches

EventScenarioNumber of objects
direct-debit-payment-batches.submittedThe state of a Direct Debit Payment Batch has changed to SUBMITTED after being submitted to the bank successfully.Only one under object field.
direct-debit-payment-batches.rejectedThe state of a Direct Debit Payment Batch has changed to FAILED after being rejected by the bank.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.

Maker-checker Payment Flow

API: /v1/payments/:id/approve and /v1/payments/:id/reject

  • When an API key is covered by a maker-checker policy, newly created payments enter a PENDING_APPROVAL state and emit a payments.pending-approval webhook.
  • A separate checker key must then approve or reject the Payment, which emits payments.approved or payments.approval-rejected.
  • A approval window expiry can be defined in the maker-checker policy. If no action is taken before the approval window expires, payments.approval-expired will be emitted.
  • Approved Payments continue through the normal lifecycle (payments.succeeded / payments.failed).
EventScenarioNumber of objects
payments.pending-approvalA Payment was created under a maker-checker policy and is awaiting approval. Status is PENDING_APPROVAL. The payload includes a review object with requestedBy and expiresAt.Only one under object field.
payments.approvedA pending Payment has been approved by a checker. The payload's review object includes approvedBy and approvedAt. Payment will subsequently transition to SUCCEEDED after settlement.Only one under object field.
payments.approval-rejectedA pending Payment has been rejected by a checker. Status becomes APPROVAL_REJECTED. The payload's review object includes rejectedBy, rejectedAt, and rejectionReason.Only one under object field.
payments.approval-expiredA pending Payment exceeded its approval window without action. Status becomes APPROVAL_EXPIRED.Only one under object 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: this webhook reports only the batch-level status. Each payment's outcome is delivered separately via the payments.succeeded and payments.failed webhooks , with multiple payments grouped under the objects array (up to 100 per webhook). Examples: Multiple successful payments

Tracked QR Payments

API: /v1/tracked-payment-qr-codes

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

Virtual Accounts

API: /v1/virtual-accounts

EventScenarioNumber of objects
virtual-accounts.succeededThe state of a Virtual Account has changed to SUCCEEDED.Only one under object field.
virtual-accounts.failedThe state of a Virtual Account has changed to FAILED.Only one under object field.

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