Eclipse Initiated Notifications (Webhooks)

There are certain circumstances under which Eclipse will initiate API calls to a tenant's domain. Typical examples of this are notification of transactions on a particular store-of-value within Eclipse, or in cases where the Eclipse wallet is a facade to an external SoV on the tenant's platform and and API call is needed to authorise a debit or a credit. This section will describe the typical use cases where such calls are used, how they are configured and their corresponding payloads (models).

Security

It must be noted that all calls to 3rd party platform in these use cases will have an Eclipse-Signature header, as described in the 'Encryption and Integrity section''. It is vital that all tenant endpoints implement a check to be sure all their endpoints expecting calls from Eclipse are from Eclipse before processing them.

Source IP Addresses

Callbacks / webhooks (from Eclipse) in case tenants whish to whitelist, are as follows:
79.125.40.127
54.220.185.240
18.134.2.182
3.11.195.109

For Sandbox, the source IP for callbacks / webhooks is:
34.255.93.65

Wallet Movement Notification

This webhook is triggered to a 3rd party URL for all movements on a wallet based on the wallet's configuration, as specified by it's type and type configuration. For every wallet, there is an associated wallet-type. Each wallet-type can have a configuration parameter called, 'walletMovementWebhookUrl' which can be populated with any URL that Eclipse is able to call. A screenshot illustrating this config can be seen below.

It is important to note that this API call is performed once only. Eclipse will asynchronously send the request and will not retry if the API call fails.

In some cases where the transfer uses eventual consistency (such as digital to PTS card wallets), the notification can be sent before PTS has updated its balance. To avoid this, a delay can be configured on the wallet type so that any notifications for that wallet type are delayed. This is set via 'walletMovementWebhookDelayMs' and can be set on PTS wallet types to something like 3000 so that the notification happens 3 seconds after the transaction and gives time for PTS to be aware of the transaction.

An example of the payload for this callback is as follows:

{
  "transactionId": "123",
  "walletId": 51588,
  "type": "Cr",
  "date": "2022-11-10T17:52:31.000+02:00",
  "amount": 1.00,
  "fee": 0,
  "currency": "ZMW",
  "description": "blah blah",
  "authorisationCode": "CBHVYZ",
  "externalId": "231409331575",
  "externalUniqueId": "baff409432820bee9092f49147a704f0"
}

A description of these fields and their validation criteria are as follows:

@NotNull(message = "Value '${validatedValue}' for Transaction's transactionId is invalid: It is mandatory")
    @Schema(description = "Unique identifier for the transaction generated by the wallet/card system")
    protected String transactionId;
    @NotNull(message = "Value '${validatedValue}' for Transaction's walletId is invalid: It is mandatory")
    @Positive(message = "Value '${validatedValue}' for Transaction's walletId is invalid: It must be positive")
    @Schema(description = "The walletId of the transaction")
    protected long walletId;
    @Schema(description = "Transaction type")
    protected String type;
    @Schema(description = "ISO 8601 of the transaction")
    protected ZonedDateTime date;
    @Schema(description = "Amount. Negative for debits, positive for credits. Amounts are in major currency unit")
    protected BigDecimal amount;
    @Schema(description = "Fee. The additional fee charged as part of the transaction")
    protected BigDecimal fee;
    @Schema(description = "Currency of the transaction. E.g. USD, ZAR, NGN etc")
    protected String currency;
    @Schema(description = "Closing balance in the currency of the wallet.")
    protected BigDecimal balance;
    @Schema(description = "Transaction description")
    protected String description;
    @Schema(description = "Auth code for card transactions")
    protected String authorisationCode;
    @Schema(description = "External identifier for the transaction which can be used for reconcilliation. Need not be unique")
    protected String externalId;
    @Schema(description = "The externally provided unique identifier for the transaction")
    protected String externalUniqueId;
    @Schema(description = "If this was a transfer then this field will contain the other walletId which was debited/credited")
    protected Long otherWalletId;
    @Schema(description = "Any location information about the transaction such as IP address or GPS")
    protected String location;

Wallet Movement SMS and Email Notifications

Eclipse offers a straightforward way to configure SMS and Email notifications for both successful and failed wallet transactions. In the case of SMS, the message will be sent to the phone number of the user associated to the particular wallet. For email the email address of the user associated to the particular wallet will be used.

Configuration Parameters

Wallet types can be configured with parameters:

  • walletMovementSMSTemplate – Specifies the global property that defines the Mustache template used for SMS notifications on successful transactions.
  • failedWalletMovementSMSTemplate – Specifies the global property that defines the Mustache template used for SMS notifications on failed transactions.
  • walletMovementEmailTemplate – Specifies the global property that defines the Mustache template used for Email notifications on successful transactions.
  • failedWalletMovementEmailTemplate – Specifies the global property that defines the Mustache template used for Email notifications on failed transactions.

📘

Note

Mustache templates for SMS and Email support basic text substitution. For more advanced logic (e.g. conditionals), you can use the Handlebars variant by specifying #Type:Handlebars in the template header.

The following data fields are available for use in the SMS and Email mustache templates:

FieldDescription
amountAmount of the related transaction
balanceBalance of the wallet being transacted on
currencyCurrency of the related transaction
dateDate of the transaction
descriptionThe description of the transaction
externalIdThe externalId field of the transaction
externalUniqueIdThe externalUniqueId field of the transaction
locationLocation of the transaction
otherWalletIdThe destination wallet ID of the transaction
typeThe transaction type
walletIdThe wallet being transacted on
transactionIdThe ID of the transaction

Example simple mustache template for walletMovementSMSTemplate:

#UrlDataSource: wallet=/rest/v1/wallets/{{#round0}}{{data.otherWalletId}}{{/round0}}
#UrlDataSource: user=/rest/v1/users/{{#round0}}{{wallet.userId}}{{/round0}}
#To: {{user.phone1}}
Your wallet {{data.friendlyId}} has had a transaction of R{{#round0}}{{data.amount}}{{/round0}} .
Your closing balance is R{{#round0}}{{data.balance}}{{/round0}}

Example complex mustache template for walletMovementSMSTemplate:

#Type:Handlebars
#UrlDataSource: wallet=/rest/v1/wallets/{{data.walletId}}
#UrlDataSource: user=/rest/v1/users/{{wallet.userId}}
#To: {{user.phone1}}
<property>custom.sms.header.property</property>

{{#if (gt data.amount 0)}}
       You recieved R{{data.amount}}. Your account's available balance is R{{data.balance}}.
{{else}}
	You sent R{{data.amount}}. Your account's available balance is R{{data.balance}}.
{{/if}}

<property>custom.sms.footer.property</property>

Example simple mustache template for failedWalletMovementSMSTemplate:

#UrlDataSource: wallet=/rest/v1/wallets/{{#round0}}{{data.walletId}}{{/round0}}
#UrlDataSource: user=/rest/v1/users/{{#round0}}{{wallet.userId}}{{/round0}}
#To: {{user.phone1}}
Dear client, your payment of R{{#round0}}{{data.amount}}{{/round0}} at {{data.location}} was declined due to {{data.description}}.

Example mustache template for walletMovementEmailTemplate:

#Type: Handlebars
#UrlDataSource: wallet=/rest/v1/wallets/{{data.walletId}}
#UrlDataSource: user=/rest/v1/users/{{wallet.userId}}
#UrlDataSource: organisation=/rest/v1/organisations/{{wallet.organisationId}}
#From: ++comms.email.address++
#To: {{#if wallet.userId}}{{user.email1}}{{else}}{{organisation.email}}{{/if}}
#Subject: Payment transaction
  <p></p>

Wallet Movement Authorization

Related to wallet movement notifications is the ability to send debit notifications to the tenant for authorization before the debit is processed - if the tenant does not authorize then Eclipse will roll back any ledger changes done. This gives tenants the ability to auth transactions synchronously and supports the use case where a tenant has their own ledger and they want this to mirror the Eclipse wallet ledger.

There are 2 wallet-type configuration parameters that apply to this scenario:
1. preDebitTenantAuthUrl -URL that Eclipse will do a POST to with the details of any transfer debit from the wallet and only if an HTTP response code < 300 is returned will Eclipse process the transaction. This gives tenants the ability to auth transactions synchronously. The call must return within 3 seconds as this is the timeout of the request. The tenants URL path must have eclipseauthrequest in the path.
2. preDebitTenantAuthSkipForUserId - Transfers by this userId will not result in preDebitTenantAuthUrl being called even if it is configured. Tenants can use this to not to a back-to-back auth on transactions when not required (e.g. the tenant initiated the transfers themselves)

Event Driven Notifications

In addition to the notifications mentioned above, Eclipse provides a comprehensive event-driven notification framework, enabling notifications to be triggered by any event within the ecosystem.

This can be used for a wide range of use cases:

  • Dynamic customer notifications across multiple channels, based on ecosystem events.
  • Streaming events to external systems (e.g., SIEM, analytics platforms).

The framework works as follows:

  1. Eclipse publishes events for all activities within the ecosystem into the events framework
  2. Third parties can also publish events for all activities
  3. Notification listeners and notification templates can be configured per tenant - this defines what events should trigger which notifications
  4. Mustache or handlebar templates can be used to send notifications
  5. Handlers exist for SMS, Email, Push notifications and HTTP

Prerequisites:

  1. Tenant config eventPublishers - (default value in global property: impl.event.publishers). This property uses regex to define which events trigger notifications for this tenant.
^(wallet\.movement\.(credit|debit)|card\.movement|proxy\.wallet\.movement|postilion\.card\.(update|expiry|pin\.update))$=com.ukheshe.eclipse.conductor.listener.EclipseEventNotificationListener

This configuration will trigger notifications for the following events:

  • wallet.movement.credit
  • wallet.movement.debit
  • card.movement
  • proxy.wallet.movement
  • postilion.card.update
  • postilion.card.expiry
  • postilion.card.pin.update
  1. Tenant config events.notification.template.{event_name} - this property defines the mustache or handlebars template to send the notification. Note this supports the unified Handlebars template to dynamically send notifications based on preferences.

Example

To send dynamic notifications to a customer depending on the customer communication preference for all credit wallet movement events.

  1. Set tenant config impl.event.publishers to match wallet.movement.credit:
^(wallet\.movement\.(credit))$=com.ukheshe.eclipse.conductor.listener.EclipseEventNotificationListener
  1. Set tenant config events.notification.template.wallet.movement.credit to the template to use to send the notifications:
handlebar.template.walletMovementTemplate
  1. Define the handlebar template in global property handlebar.template.walletMovementTemplate:
#Type: Handlebars
#UrlDataSource: user=/rest/v1/users/{{data.userId}}

{{#if (or (eq user.communicationPreference "SMS") (eq user.communicationPreference null))}}
  #SMS-Start
  #To: {{user.phone1}}
  This is the sample SMS. Sent by {{data.sender}}
  #SMS-End
{{/if}}

{{#eq user.communicationPreference "EMAIL"}}
  #Email-Start
  #From: ++comms.email.address++
  #To: {{user.email1}}
  #Subject: dev-test-template-1
  This is Sample email Template. Sent by {{data.sender}}
  #Email-End
{{/eq}}

{{#eq user.communicationPreference "PUSH"}}
  #PushNotification-Start
  This is the PushNotification template Sent by {{data.sender}}
  #PushNotification-End
{{/eq}}

Sending Events Over HTTP

The events framework can publish events not only to the notifications framework for templating and sending emails and SMS etc, but can be used in a more raw form to send events to tenants in plain JSON. A primary use case of this is to publish Eclipse events to a SIEM (Security Information and Event Management). Again the same impl.event.publishers property (or eventPublishers tenant config) can be used to indicate what events must be analysed to send to tenants. E.g.:

.*=com.ukheshe.arch.impl.event.HttpUrlPerTenantEventPublisher

Then on the tenant config, set property eventWebhookUrl:

https://mybank.com/siem/events

Event Types

Events sent over HTTP follow the structure below:

{
  "eventType": {
    "type": "string",
    "description": "Type of the event, e.g. user.organisation.create"
  },
  "traceId": {
    "type": "string",
    "description": "Unique trace identifier for tracking the event"
  },
  "tenantId": {
    "type": "integer",
    "description": "Tenant identifier related to the event"
  },
  "created": {
    "type": "string",
    "format": "date-time",
    "description": "Timestamp when the event was created"
  },
  "data": {
    "type": "object",
    "description": "Event-specific payload"
  },
  "serverId": {
    "type": "string",
    "description": "Unique identifier of the server where the event originated"
  },
  "serverIp": {
    "type": "string",
    "format": "ipv4",
    "description": "IP address of the server where the event originated"
  },
  "instigator": {
    "type": "object",
    "description": "Details of the user or system that initiated the event",
    "properties": {
      "identity": { "type": "string" },
      "roles": {
        "type": "array",
        "items": { "type": "string" }
      },
      "positions": {
        "type": "array",
        "items": {}
      },
      "userId": { "type": "integer" },
      "locale": { "type": "string" },
      "remoteAddr": { "type": "string", "format": "ipv4" },
      "sessionId": { "type": "string" },
      "source": { "type": "string" },
      "attributes": { "type": "object" },
      "scope": {
        "type": "array",
        "items": {}
      },
      "system": { "type": "boolean" },
      "anonymous": { "type": "boolean" }
    },
    "required": [
      "identity",
      "roles",
      "userId",
      "locale",
      "remoteAddr",
      "sessionId",
      "source",
      "system",
      "anonymous"
    ]
  },
  "threadName": {
    "type": "string",
    "description": "Thread name on which the event was processed"
  },
  "softwareVersion": {
    "type": "string",
    "description": "Version of the software handling the event"
  },
  "associatedEntityId": {
    "type": "string",
    "description": "Identifier of the entity associated with the event"
  },
  "associatedEntityType": {
    "type": "string",
    "description": "Type of the entity associated with the event, e.g. organisation"
  },
  "schemaVersion": {
    "type": "integer",
    "description": "Version of the schema used for this event"
  }
}

Example event for a newly created organisation:

{
  "eventType": "user.organisation.create",
  "traceId": "baab12a7dfbd1e1cc4f56c796706613f",
  "tenantId": 11224145,
  "created": "2025-08-12T10:23:17.757Z",
  "data": {
    "organisation": {
      "organisationId": 11245917,
      "name": "Test",
      "phone1": "1234567890",
      "status": 1,
      "externalId": "e2df5a70-4461-4e38-b45c-320258b41226",
      "created": "2025-08-12T10:23:18.000Z",
      "lastModified": "2025-08-12T10:23:18.000Z",
      "version": 0,
      "bankDetails": [],
      "organisationAddresses": [],
      "organisationDocuments": [],
      "tenantId": 11224145,
      "preferences": []
    }
  },
  "serverId": "e5b7c655-302c-4709-ad78-f867ce36b28d",
  "serverIp": "172.31.34.124",
  "instigator": {
    "identity": "[email protected]",
    "roles": [
      "GLOBAL_ADMIN"
    ],
    "positions": [],
    "userId": 28603,
    "locale": "en-US",
    "remoteAddr": "105.233.41.30",
    "sessionId": "e69a8767-ab47-44f3-8cdc-e168c41b0a31",
    "source": "Auth-RP",
    "attributes": {},
    "scope": [],
    "system": false,
    "anonymous": false
  },
  "threadName": "v-quarkus-905",
  "softwareVersion": "sandbox.6aa6dd9.2025-08-12T10-17-13",
  "associatedEntityId": "11245917",
  "associatedEntityType": "organisation",
  "schemaVersion": 1
}

Common Event Types

GroupEvent TypeDescription
awsaws.liveness.sessionA liveness session has been created in AWS.
cardcard.createdA new card has been created.
cardcard.updatedA card has been updated.
cardcard.deletedA card has been deleted.
cardcard.lockedA card has been locked.
cardcard.unlockedA card has been unlocked.
cardcard.expiredA card has expired.
paymentpayment.createdA new payment has been created.
paymentpayment.completedA payment has been completed.
paymentpayment.failedA payment has failed.
paymentpayment.reversedA payment has been reversed.
postilionpostilion.transaction.decline.isomessageA Postilion transaction was declined, and an ISO 8583 message was generated.
postilionpostilion.transaction.declineA Postilion transaction was declined.
postilionpostilion.settlement.requestA settlement request is being sent to Postilion.
postilionpostilion.incoming.transactionAn incoming transaction from Postilion is being processed.
postilionpostilion.incoming.transaction.isomessageAn incoming transaction from Postilion is being processed, with ISO 8583.
postilionpostilion.card.update.remoteA card update is being sent to Postilion.
postilionpostilion.transaction.createdA new Postilion transaction has been created.
postilionpostilion.transaction.completedA Postilion transaction has been completed.
smssms.send.successAn SMS message was sent successfully.
smssms.send.failureAn SMS message failed to send.
tenanttenant.createdA new tenant has been created.
tenanttenant.updatedA tenant has been updated.
tenanttenant.deletedA tenant has been deleted.
useruser.user.updateA user's profile has been updated.
useruser.createdA new user has been created.
useruser.updatedA user has been updated.
useruser.deletedA user has been deleted.
useruser.lockedA user has been locked.
useruser.unlockedA user has been unlocked.
useruser.password.changedA user's password has been changed.
useruser.role.addedA role has been added to a user.
useruser.role.removedA role has been removed from a user.
useruser.group.addedA group has been added to a user.
useruser.group.removedA group has been removed from a user.
useruser.permission.addedA permission has been added to a user.
useruser.permission.removedA permission has been removed from a user.
useruser.session.startedA user session has started.
useruser.session.endedA user session has ended.
walletwallet.transfer.auth.successA wallet transfer authorization was successful.
walletwallet.transfer.auth.fail.businessA wallet transfer authorization failed for a business reason.
walletwallet.transfer.auth.fail.systemA wallet transfer authorization failed for a system reason.
walletwallet.mapping.createA new wallet mapping has been created.
walletwallet.createdA new wallet has been created.
walletwallet.updatedA wallet has been updated.
walletwallet.lockedA wallet has been locked.
walletwallet.unlockedA wallet has been unlocked.
walletwallet.closedA wallet has been closed.
walletwallet.transfer.createdA wallet transfer has been created.
walletwallet.transfer.completedA wallet transfer has been completed.
walletwallet.transfer.failedA wallet transfer has failed.

📘

Note

  • The data field contains the object relevant to the event.
    For example:

    • user.created → the data field will contain the user object.
  • For detailed specifications of event objects, please contact [email protected].

Consolidated Callbacks for Unsolicited Deposits/Payments

For longer running transactions like payments, topups and withdrawals callbacks on transaction completion including transaction details and status are important for tenants to receive. Typically for tenant initiated payments, withdrawals or topups a callback URL can be specified by the tenant when initiating that transaction and the full transaction object is posted to that URL when the transaction completes.

However there are certain transactions that are not tenant initiated, that tenants would also like to be notified on - for example an EFT payment into a wallet or a QR code payment into a wallet.

To be notified on any unsolicited deposits/payments (e.g. an EFT payment is done into a wallet, or a QR code is paid into a wallet), the completed transaction object can be posted to a URL. This URL can be set at an organisation level by including an attachment of type consolidatedCallbacks that specifies the notification URL. If the payment is for a wallet associated to that organisation then the notification will be sent to that URL. Alternatively this can be set at a tenant level by setting tenant configuration consolidatedCallbacks to the specific URL.

POST /eclipse-conductor/rest/v1/tenants/{tenantId}/organisations/{organisationId}/attachments
{
  "attachmentType": "consolidatedCallbacks",
  "mediaType": "text/plain",
  "type": "consolidatedCallbacks",
  "info": "https://webhook.site/91368c6c-6581-4434-9500-06ae5d36694b"
}

An example of the payload for the callback when an EFT payment is done is as follows:

{
  "externalUniqueId": "+VNp9bmNHwnjjskFPTWInv2aUD2rg/HYGjqPRHWQ8vU=",
  "walletId": 3129,
  "amount": 10.000000000,
  "created": "2023-11-01T09:26:00Z",
  "paymentReference": "JVAHS6G3",
  "fee": 0.10000000000000000000000000000,
  "description": "JVAHS6G3",
  "retrievalReferenceNumber": 0,
  "paymentType": "ATM",
  "paymentId": 10491,
  "customerId": 3761,
  "currency": "ZAR",
  "status": "SUCCESSFUL"
}

An example of the payload for the callback when a QR payment is done is as follows:

{
    "paymentId": 74881,
    "externalUniqueId": "MP-IN-4589207-SUCCESS",
    "status": "SUCCESSFUL",
    "amount": 100.000000000,
    "description":"QR Code Paymeny",
    "currency": "ZAR",
    "additionalFields": [],
    "acceptedCardSchemes": [],
    "cardPhone": "9726999279",
    "phone": "9726999279",
    "acceptedPaymentMechanisms": [],
    "paymentType": "CARD",
    "authCode": " DEBUG",
    "retrievalReferenceNumber": 809260,
    "created": "2023-11-22T10:26:59.000Z",
    "paymentData": "6244523710",
    "paymentInstrumentInfo": {
        "cardBin": "424242",
        "cardType": "CREDIT",
        "cardLast4": "4242",
        "cardName": "TESTING",
        "cardPhone": "9726999279"
    },
    "fee": 0E-9,
    "paymentReference": "7165124d-5e53-49b6-b644-b3474c8d6ccb",
    "walletId": 204149,
    "organisationId": 11869,
    "associatedPaymentId": 74878,
    "gatewayTransactionId": "MP-IN-4589207-SUCCESS"
}