Webhooks

If you integrate directly with April, you are able to use webhooks to be notified when an event happens on your account. Webhooks are particularly useful for asynchronous events such as a payout being paid to a merchant's bank account, a sub-merchant completing onboarding or a transaction being paid.

How to receive webhooks

Webhook endpoints can be registered via the Create Webhook Subscription API call. Subscriptions can be made at either the marketplace or merchant level. Each subscription registers a fully qualified HTTPS endpoint that will accept POST calls over the public internet.

Existing subscriptions can be suspended or activated via Update Webhook Subscription or queried using Query Webhook Subscriptions

Handling incoming webhooks

Webhook events are delivered to your registered HTTPS endpoint as a POST request with the event details provided in JSON format in the request body.

Event payload

The overall structure of the event messages will be the same regardless of the type of events subscribed.

Copy
Copied
{
  "marketplaceId":"mktp_ZJOpaegtuDrIXjgm",
  "metadata":{
    "service":"dispatchment"
  },
  "webhookSubscriptionId":"webhook_ZJOpaugtuDrIXjgs",
  "webhookEventType":"TransactionPaid",
  "requestId": "a0a3e2b6-99bd-4a7d-ae30-d3f7340c59e5",
  "event":{ ... },
  "timestamp":"2023-06-22T01:52:52.096861Z"
}

The top level JSON object in the request body will contain the following fields:

  • marketplaceId or merchantId - if the subscription is at the marketplace level, the matching marketplaceId will be provided, otherwise the merchantId will be provided.
  • metadata - this is an optional JSON object that you can provide on the Create Webhook Subscription request that you can use to provide some context to this subscription.
  • webhookSubscriptionId - unique system identifier for the webhook subscription this event triggered for
  • webhookEventType - the category of event included in this request
  • requestId - the unique identifier for the API call that triggered the generation of this event. This corresponds to the Request-Id response header on the API call itself.
  • event - Details of the event. The structure of the event is specific to the category of the event as details below.
  • timestamp - When the webhook event was generated in ISO-8601 format.

Acknowledging receipt and retries

April marks an event as successfully delivered if your service returns a 2xx status code. If your service is not reachable or returns a different status code, then April will retry up to 100 times, doubling the interval between each retry up to a maximum interval of one hour.

Verifying the sender

Since webhooks require you to expose a public endpoint, it's possible for third parties to impersonate April requests. Every legitimate webhook request from April includes a payload-signature request header.

Copy
Copied
payload-signature: FfB76MV8JQGiCpyttEa8k2u8kYQy1k/B5WatzlgWf9w=

This is a cryptographic signature of the event payload generated using the secret provided in the response when creating the corresponding webhook subscription.

The following code examples show you how to reproduce the signature yourself from the event to confirm it was sent by April. If the payload-signature header is missing, or the signature you generate doesn't match the provided one, the event is not a genuine one.

In each of the following examples the body variable contains the String based content of the request.

javascriptjavapython
Copy
Copied
const crypto = require('crypto');

function generateSignature(body, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(body);
  return hmac.digest('base64');
}
Copy
Copied
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

public String generateSignature(String body, String secret) {
    var bytes = body.getBytes(StandardCharsets.UTF_8);
    String algorithm = "HmacSHA256";
    try {
        Mac mac = Mac.getInstance(algorithm);
        SecretKeySpec key = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), algorithm);
        mac.init(key);
        byte[] hash = mac.doFinal(bytes);

        return Base64.getEncoder().encodeToString(hash);
    } catch (Exception ex) {
    throw new RuntimeException(ex);
    }
}
Copy
Copied
import hmac
import hashlib
import base64

def generate_signature(body, secret):
    hmac_hash = hmac.new(secret.encode('utf-8'), body.encode('utf-8'), hashlib.sha256)
    return base64.b64encode(hmac_hash.digest()).decode('utf-8')

Supported webhook event types

A subscription is for one or more of the following event types. For a given subscription, all events matching the subscribed event types will be sent to the registered URL.

Merchant events

  • MerchantCreated - A sub-merchant of a marketplace has been created and has started the onboarding process.
  • MerchantActivated - A merchant has been fully onboarded and is activated for accepting payments.
  • MerchantSuspended - A merchant has been suspended, disabling creation of new payments.
Copy
Copied
{
  "MerchantEvent": {
    "marketplaceId": "mktp_ZJLKZegtuDrIXjfk",
    "createdAt": "2023-06-21T10:01:09.931Z",
    "merchantId": "mcht_ZJLKZugtuDrIXjfs",
    "businessName": "Los Pollos Hermanos",
    "referenceMerchantId": "chickenbros-11a",
    "status": "Pending",
    "updatedAt": "2023-06-21T10:01:09.931Z"
  }
}

Pay Plan events

  • PayPlanCreated - The pay plan object has been created, but has not been successfully associated with an order yet.
  • PayPlanActivated - The pay plan has successfully been established, the initial payment, if any, has been received and one or more future instalments are yet to be paid.
  • PayPlanCompleted - The payment plan has been completely paid, including any fees/penalties.
  • PayPlanInDefault - The plan is overdue and has not been paid after several soft collection attempts.
Copy
Copied
{
  "PayPlanEvent": {
    "outstandingAmount": 120000,
    "orderId": "ordr_ZJK0jegtuDrIXjfB",
    "payPlanVariant": "payplan_30_days_0_100",
    "transactionId": null,
    "marketplaceId": "mktp_ZJK0hegtuDrIXjeS",
    "createdAt": "2023-06-21T08:27:56.458Z",
    "referenceOrderId": "ORD23489ff2a",
    "merchantId": "mcht_ZJK0hugtuDrIXjeY",
    "initialAmount": 40000,
    "customerId": "cust_ZJK0iugtuDrIXje3",
    "currency": "AUD",
    "payPlanId": "plan_ZJK0jOgtuDrIXje9",
    "state": "Active",
    "planAmount": 120000,
    "updatedAt": "2023-06-21T08:27:57.456Z"
  }
}

Full Payment events

  • PayInFullCreated - A payment token representing a full payment has been created. No money transfer has occurred at this point.
  • PayInFullAccepted - A pay in full payment has been accepted by the customer but the payment method is asynchronous (e.g. direct debit or bank transfer).
  • PayInFullAuthorised - A card based pay in full payment has authorised a payment (reserving the funds), but it has not been captured yet.
  • PayInFullCompleted - A pay in full payment has been successfully paid.
Copy
Copied
{
  "PayInFullEvent": {
    "orderType": "paybylink",
    "orderMetadata": null,
    "amount": 111650,
    "orderId": "ordr_YwRMKLamGhOoW0Ym",
    "transactionId": "tran_ZJq00QJX-CjIe7_x",
    "marketplaceId": "mktp_Yj0NLDzhf1ITHYWc",
    "createdAt": "2022-08-23T03:40:24.669Z",
    "referenceOrderId": "Pipe order",
    "merchantId": "mcht_YSxT5hTjzVNgADKk",
    "customerId": null,
    "currency": "AUD",
    "state": "Completed",
    "updatedAt": "2023-06-27T10:07:17.821Z"
  }
}

Transaction events

  • TransactionPaid - An order has been completed from the merchants perspective. For pay in full payments, the payment has been successfully completed. For pay plans, the plan has been successfully activated.
  • TransactionFailed - An attempt to pay for an order has failed.
Copy
Copied
{
  "TransactionEvent": {
    "orderType": "online",
    "orderMetadata": {
      "invoiceId": "AB44111"
    },
    "amount": 120000,
    "transactionStatus": "pending",
    "orderId": "ordr_ZJKjYegtuDrIXjd_",
    "transactionId": "tran_ZJKjYegtuDrIXjeB",
    "referenceMerchantId": "chickenbros-11a",
    "referenceCustomerId": "SMATA-ish-strata-07a08d93-f7d8-4f40-8294-fac9c96c257f",
    "marketplaceId": "mktp_ZJKjVegtuDrIXjdQ",
    "createdAt": "2023-06-21T07:14:41.543Z",
    "referenceOrderId": "referenceOrderId-5c80e9fa-f869-4c73-9ede-d85de0cb9669",
    "payType": "PayPlan",
    "merchantId": "mcht_ZJKjV-gtuDrIXjdW",
    "customerId": "cust_ZJKjXOgtuDrIXjd1",
    "currency": "AUD",
    "updatedAt": "2023-06-21T07:14:42.205Z"
  }
}

Dispute events

  • DisputeCreated - A customer has raised a dispute for a payment with their financial provider.
  • DisputeUpdated - An update has been received on an existing dispute.
Copy
Copied
{
  "DisputeEvent": {
    "disputeId": "dispute_ZJK0jOgtuDrIXje9",
    "disputeAmount": 45131,
    "disputeFee": 1500,
    "transactionId": "tran_ZJKjYegtuDrIXjeB",
    "orderId": "ordr_ZJKjYegtuDrIXjd_",
    "reason": "fraudulent",
    "state": "needs_response",
    "outcome": "pending",
    "payType": "PayPlan",
    "marketplaceId": "mktp_ZJKjVegtuDrIXjdQ",
    "merchantId": "mcht_ZJKjV-gtuDrIXjdW",
    "customerId": "cust_ZJKjXOgtuDrIXjd1",
    "createdAt": "2023-06-21T07:14:42.205Z",
    "updatedAt": "2023-06-27T10:07:17.821Z"
  }
}

Notable fields in the event

  • disputeAmount - The disputed amount in minor currency units
  • disputeFee - The dispute fee charged to the merchant in minor currency units
  • reason - The reason provided by the customer for raising the dispute
  • state - The internal state of the dispute
  • outcome - The overall state of the dispute. Will be one of pending , lost or won .

Payout events

  • PayoutFailed - We were unable to make a payout to a merchant's primary bank account.
  • PayoutPaid - A payout was successfully delivered to a merchant's primary bank account.
Copy
Copied
{
  "PayoutEvent": {
    "payoutId": "payout_ZJK0jOgtuDrgXje3",
    "payoutAmount": 981458,
    "currency": "AUD",
    "openingBalance": 0,
    "closingBalance": 0,
    "arrivalDate": "2023-06-27T22:15:43.000Z",
    "status": "paid",
    "failureReason": null,
    "marketplaceId": "mktp_ZJKjVegtuDrIXjdQ",
    "merchantId": "mcht_ZJKjV-gtuDrIXjdW",
    "customerId": "cust_ZJKjXOgtuDrIXjd1",
    "createdAt": "2023-06-21T07:14:42.205Z",
    "updatedAt": "2023-06-27T10:07:17.821Z"
  }
}

Notable fields in the event

  • payoutAmount - The amount paid out to the merchant in minor currency units
  • arrivalDate - When the payout is initiated on the April side. Typically, the funds are cleared within a few hours of this time.
  • status - One of pending (payout not yet initated), in_transit (on its way to the merchants bank account), paid (cleared successfully), failed (the payout failed to clear - typically due to incorrect bank account details), reversed (where a failed payout is credited to the next payout)
note

Most of the time openingBalance and closingBalance are both 0.

For payouts where the total from negative entries such as refunds is greater than the total from positive entries such as payments, then the closing balance will be a negative amount and no deposit to the merchant's bank account will be made.

The closing balance from that payout is then carried forward as the opening balance of the next payout and is included with the new entries when calculating the payout amount.

Copyright © April Solutions 2023. All right reserved.