Webhooks

Don't call us, we'll call you

Webhooks are used to notify of status changes to a configured endpoint. The webhook details can be configured in the Developer area of Portal at the aggregator or payer level. If a non 20x response is returned, we will retry the webhook 2 additional times in 15 seconds, then repeat the cycle again within the next 2 hours.

Webhooks are sent for the following events:

  • Payment status updates
  • Transaction status updates
  • Account status updates
    • Prepaid Account created via API
    • Card Account successfully added via Widget UI
    • Bank Account successfully added via Widget UI
    • Bank Account status updated upon Chargeback/Reveral request

📘

Aggregator API Model & Webhooks

If the API Account is created at the Aggregator level, webhooks will be invoked based on the Aggregator webhook settings.

Webhook Idempotency

Webhook payloads will include a header X-Webhook-ID with a unique value that corresponds with the Aws-Api-Gateway-Requestid header response value from the originating Create request.

Webhook Payload schemas

PAYMENT WEBHOOK PAYLOAD

{
  "webhook_type": "PAYMENT",
  "payment_id": "String : Payment ID",
  "payment_status": "Enum : Payment Status",
  "reference_id": "String (optional) : Reference ID if included"
}

TRANSACTION WEBHOOK PAYLOAD

{
  "webhook_type": "TRANSACTION",
  "type": "Enum : Transaction Type",
  "method": "Enum: Transaction Method",
  "transaction_id": "String : Transaction ID",
  "transaction_status": "Enum : Transaction Status",
  "reference_id": "String (optional) : Reference ID if included",
  "webhook_created_date": "Timestamp",
  "network_approval_code": "String : Approval code for Instant Deposit"
}
{
  "webhook_type": "TRANSACTION",
  "transaction_id": "String : Transaction ID",
  "transaction_status": "Enum : Transaction Status",
  "reference_id": "String (optional) : Reference ID if included",
  "error_code": "String: error code",
  "error_message": "String: error detail"
}

PAYMENT ACCOUNT WEBHOOK PAYLOAD - Bank Account

{
  "widget_id": "String: Widget ID",
  "webhook_type": "PAYMENT_ACCOUNT",
  "account_id": "String : Account ID",
  "recipient_id": "String : Recipient ID",
  "account_number": "String : Last 4 of Account Number",
  "account_type": "String: CHECKING or SAVINGS",
  "bank_name": "String"
}
{
  "widget_id": "String: Widget ID",
  "webhook_type": "PAYMENT_ACCOUNT",
  "account_id": "String : Account ID",
  "recipient_id": "String : Recipient ID",
  "account_status": "String : Status of Bank Account"
}
{
  "widget_id": "String: Widget ID",
  "webhook_type": "PAYMENT_ACCOUNT",
  "account_id": "String : Account ID",
  "recipient_id": "String : Recipient ID",
  "error_code": "String: error code",
  "error_message": "String: error detail"
}

📘

Bank Account Webhook

The account_status value may contain the following:

BANK_ACCOUNT_BLOCKED: Account is blocked due to fraud controls

PLAID_REAUTH_REQUIRED: Account authorization with Plaid is due to expire or will expire. Once the authorization expires, a Bank Account Widget Request to reauthorize the account with Plaid will be required before transacting. The reauthorization process may take place at any point in time.

PLAID_PERMISSION_REVOKED: User has explicitly revoked permission for Plaid to interact with their bank account. A new Bank Account Widget Request to authorize the account is required before transacting.

PAYMENT ACCOUNT WEBHOOK PAYLOAD - Card Account

{
  "widget_id": "String: Widget ID",
  "webhook_type": "PAYMENT_ACCOUNT",
  "account_id": "String : Account ID",
  "recipient_id": "String : Recipient ID",
  "card_number": "String Masked card number"
}
{
  "widget_id": "String: Widget ID",
  "webhook_type": "PAYMENT_ACCOUNT",
  "account_id": "String : Account ID",
  "recipient_id": "String : Recipient ID",
  "card_number": "String : Masked card number",
  "error_code": "String : Error code for account sharing (ERR_ACCOUNT_SHARING_LIMIT_EXCEEDED)"
}
{
  "widget_id": "String: Widget ID",
  "webhook_type": "PAYMENT_ACCOUNT",
  "account_id": "String : Account ID",
  "recipient_id": "String : Recipient ID",
  "card_number": "String Masked card number",
  "verification_result" : {
  	"network": "String: VISA/MASTERCARD",
    "pan": "String: Match result",
    "cvv": "String: Match result",
    "avs": {
    	"street": "String: Match result",
      "zip": "String Match result"
    },
    "ani" {
     	"first_name": "String: Match result",
      "last_name": "String: Match result"
    }
  }
}

PAYMENT ACCOUNT WEBHOOK PAYLOAD - Prepaid Account

{
  "widget_id": "String: Widget ID",
  "webhook_type": "PAYMENT_ACCOUNT",
  "account_id": "String : Account ID",
  "recipient_id": "String : Recipient ID",
  "prepaid_account_status": "String : Account Status",
  "prepaid_account_error": "String : Account Error Detail"
}

Webhook Verification

Webhook payloads include an X-Verification header that contains a JSON Web Token (JWT) to verify the webhook contents and origin. It is recommend that the webhook signature and contents are verified before processing the payload.

Using the JWT to Verify Webhooks

1. Decode the X-Verification header JWT

Use your library of choice to decode the X-Verification header JWT.

X-Verification : eyJ0eXAiOiJKV1QiLCJraWQiOiI5OGQ1ZTA4ZS1mNTNlLTQ4MjEtYmZkZC0zZjYwZmIwYTRkMDgiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2NTc1NjkwNTAsInJlcXVlc3RfYm9keV9zaGEyNTZfaGFzaCI6IkI4QjBENDYxOEQ5RjdEMTkzREYwQUE4MTMyRjYzNTY2QkI3MUQ1N0NEM0RCODdGQjQ0RDBDMTEwRTAxMDM2NjYifQ.Kzb-awXSL0bZ0vldAP_VHQ8N3P_TMwaz8b2WICX9Kb1tgYhoIPxpEtTm6zRv3LEAZTHtegsiM5AR8vxK5JSJgzInOicUpG-JSbuHzgX6ofWHFrPJtGnHPOwxJn54s7Z2MYTe1AnSsgA_86IxD8Tqgka_UZAyr__KJigqONRaVxiEdGuMjrn61J-sbKefvCwqJUtP2HuMYZDf3dMSzo0NI6zWBeskPueaJXSuAjLRAWVwbnmOTYbGFxe2GhwQTkcLCGqtDwE2YPm3xp9zc30ite8Xy5JKxiSZr17QNE6oTFDKW9ykcg8QMZJoNu6WO4kdFDsyTcBRTJMmEaWifrud9VaVT1-GqoOsKlPIBE0l3dmaAajxNyeOXkbinpcIvxLkn291_sKRgPAbzajRT9DuTuR5YzuqV9uQYKKCWhkpPDcYs16kbOzmZCKstUCDldV5yIx1vCF3iq8A_S56RIgdr4D4_LNPyFSeWkf-e7ZOCcdXyOvB8n-X2o-70Gte0Tx-gPT6sr0MW2CESEVOxPe-MSxs54_Ns1yQc5LLCtuYMpzssjTy3JaLJvNFVP2Gw2EqsGY7cJEqSy2YGXCe1kDpCA889dnzXnwGTralCxyWgfDotqu48zZ6v1cBjlWNZGh_I9YwJvIUpRyp9VBSi2QzCfWNBeHoo5xjF0lUVC1h5dU

Decoding the example JWT X-Verification header results in the following JWT header and payload:

{
  "typ": "JWT",
  "kid": "98d5e08e-f53e-4821-bfdd-3f60fb0a4d08",
  "alg": "RS256"
}
{
  "iat": 1657569050,
  "request_body_sha256_hash": "B8B0D4618D9F7D193DF0AA8132F63566BB71D57CD3DB87FB44D0C110E0103666"
}

2. Get the Validation Key

Make a GET request to <<testSandboxUrl>>/api/v2/{payerId}/webhooks/get_validation_key/{kid}, replacing {payerId} with your payer id, and {kid} with the kid obtained from the JWT header. The response contains a public key to verify the authenticity of the JWT payload.

{
  "kty": "RSA",
  "e": "AQAB",
  "n": "ylNCSJtagLrhi2D0aITP8mdFl6sAo4LPePyUgqejemKdGQPtH1YDKuetMSTv5nDbMjdGPxA6VqJPAFFnMdbz3q4pYBPFTo18pMw7bbKN9P7tS_eQ6KZABclz2Fy7oyrJBv3NdVVNpEW8CkrJr8R3CUIpkJtv_z-hV7PMLxmBDDaDnU0bf4q_mFCAbk2uXc32CNxVntQL1bULKg5YYE6pJrpMpZ6pf7UuWF1Gw7OFVS1OBa9gcu_hzHR015Mu12b4HRNCDzULR3PBdM9XvjmXIk97AjWpPPGiHNUwflfEHGMf2bMIbM63tTYdi3yZIqTn8VeZRjym3Dw6gSj11WK_FEiNmHRzTmhw43iOU7Mq8rd0mSccBOEr1XyD2TkQ1ttFz5qgDTUTfzlucshCTkkiF8p8V_waKtb_5U5sxtZ65a4Xs_9HkIfIrEIXZzLYOeEInEDD1h_UWrOu8DrZN7l6SliYbnD0nBqLxszN9k17ZsA-PbrUyuTmrNj3m8puVTZWjpFbWQr_aKhQMBsbjkDksVb2oae0QKgwlB5qwkf4wb-hGzviGx0fpjHFcwomU1pU1trGgBQFXt33vTi-IRsrgBczOw0d0vLHhmqlnWlaHpSFIcYA63HBuijcgwK3Ufqc71-ytwd4HRpEhWxhPZXK0KgIRt2yVDoKgeyEkpE7FIM"
}

3. Verify the Authenticity of the JWT

Use the key obtained in step 2 to verify the signature of the JWT. Your JWT library should provide a convenient method to handle the verification. If the signature is not valid, do not process the webhook.

4. Read the JWT Payload and Confirm the Webhook Hash.

Finally, parse the contents of the JWT payload, example again below. The two keys,iat and request_body_sha256_hash are used to verify the time the webhook was sent and the hash of the webhook body. Compare the request_body_sha256_hash (the hash itself is case insensitive) with the SHA-256 hash of the webhook body to confirm they match. If these hashes don't match, do not process the webhook.

The iat value is the timestamp in seconds indicating when the webhook was created. For security reasons, the webhook should be rejected if the webhook is more than 5 minutes old. If verification is successful, process the webhook payload.

{
  "iat": 1657569050,
  "request_body_sha256_hash": "B8B0D4618D9F7D193DF0AA8132F63566BB71D57CD3DB87FB44D0C110E0103666"
}