Skip to content

Webhooks

Overview

The purpose of webhooks are to provide more realtime updates versus performing query requests. Currently webhooks can be triggered as transactions are processed within the gateway platform.

TIP

Transactions processed for invoices will include the invoice ID in the order_id and po_number fields of the webhook transaction data.

For subscriptions, the ID will be in the subscription_id field.

Enable

Webhooks can be enabled within the merchant control panel under "Manage" -> "Settings" -> "Webhooks".

Acknowledge and Retry

Important

Your webhook endpoint MUST return a HTTP 200 response to acknowledge successful receipt of the webhook. Failure to return a 200 response will result in the webhook being disabled after repeated failures.

Webhook notifications are processed within a few seconds of a transaction being processed as long as the response code is within the Approved or Declined range. Webhooks have a 5 second timeout and must be acknowledged with a HTTP 200 response.

If a webhook is not acknowledged with a 200 response, it will be rescheduled using a 5 minute exponential backoff and will be retried for 24 hours. After repeated failures, the webhook will be automatically disabled to prevent continued delivery attempts to non-responsive endpoints.

Control Panel Requirement

The control panel now requires a successful 200 response test before a webhook can be created or updated. This ensures your endpoint is properly configured to handle webhook notifications.

Note: Test webhooks will have "type": "test" in the payload to distinguish them from production webhook notifications.

Test Payload

json
{
  "status": "success",
  "msg": "success",
  "type": "test",
  "account_type": "merchant",
  "account_uid": "REDACTED"
}

Security

Webhook post requests are sent with a header "Signature" that is HMAC SHA 256 signed and then base64 url encoded.

To verify webhook post signatures, retrieve "Signature" from header. Base64 url decode and use HMAC SHA 256 to check using your signature UUID located in the control panel under the webhook previously created.

go
package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"os"
)

// testSignatureBase64 is an example Base64 RFC4648 encoded signature you would
// receive in the header of the webhook request.
const testSignatureBase64 = "JacUiw_ztpEZJWvOhhKoHTLBf4b-aZv9n_0YmJJxltc"

// clientSecret is an example secret specific to you, which we use to sign
// requests.
const clientSecret = "12345678-1234-1234-1234-123456789012"

func main() {
	// Set example HTTP request body.
	//
	// NOTE: Our API may include a linefeed character ('\n' = 10 = 0x0A).
	//       If you do not include this linefeed character but we do, your
	//       signature will not match. To test with this linefeed character,
	//       add '\n' to the end of the string.
	//
	//       For example, the string below would change to:
	//       var body = "{\"data\":\"this is test data\"}\n";
	//
	//       If you're reading in the request body, in byte form, then you
	//       shouldn't have to change anything as it should include this
	//       linefeed if sent in the body.
	body := `{"data":"this is test data"}`

	// Hash body to create signature.
	hash := hmac.New(sha256.New, []byte(clientSecret))
	_, err := hash.Write([]byte(body))
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	signature := hash.Sum(nil)

	// Base64 decode test signature.
	testSignature, err := base64.RawURLEncoding.DecodeString(testSignatureBase64)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	// Compare signatures.
	if !bytes.Equal(signature, testSignature) {
		fmt.Println("Signatures do not match")
		os.Exit(0)
	}

	fmt.Println("Signatures match")
}

Examples

Transaction

HTTP Headers

http
POST / HTTP/1.1
Host: localhost:9000
User-Agent: Go-http-client/1.1
Content-Length: 2372
Content-Type: application/json
Signature: Fs62H-ZaZH80Ccf-Ii9V4xC9AJLf8dQym7BFAbApJwc
Accept-Encoding: gzip

JSON Payload

json
{
  "data": {
    "amount": 450,
    "amount_authorized": 450,
    "amount_captured": 450,
    "amount_settled": 0,
    "billing_address": {
      "address_line_1": "",
      "address_line_2": "",
      "city": "",
      "company": "",
      "country": "US",
      "email": "",
      "fax": "",
      "first_name": "",
      "last_name": "",
      "phone": "",
      "postal_code": "",
      "state": ""
    },
    "captured_at": "2019-09-25T19:47:14.031268117Z",
    "created_at": "2019-09-25T19:47:14.001901427Z",
    "currency": "usd",
    "custom_fields": {
      "bm5s7im9ku6el2qvh0c0": ["placeat"],
      "bm5s7im9ku6el2qvh0cg": ["maxime"],
      "bm5s7im9ku6el2qvh0d0": ["est"],
      "bm5s7im9ku6el2qvh0f0": ["dolorum"],
      "bm5s7im9ku6el2qvh0g0": ["corporis"]
    },
    "customer_id": "",
    "customer_payment_ID": "",
    "customer_payment_type": "",
    "customer_vat_registration_number": "",
    "description": "",
    "discount_amount": 0,
    "duty_amount": 0,
    "email_address": "",
    "email_receipt": false,
    "id": "bm5s8gm9ku6ejcu15t9g",
    "idempotency_key": "",
    "idempotency_time": 0,
    "ip_address": "127.0.0.1",
    "line_items": null,
    "merchant_vat_registration_number": "",
    "national_tax_amount": 0,
    "order_id": "",
    "payment_adjustment": 350,
    "payment_method": "card",
    "payment_type": "card",
    "po_number": "",
    "processor_id": "bm5s7im9ku6el2qvgsrg",
    "processor_name": "TSYS true",
    "processor_type": "tsys_sierra",
    "referenced_transaction_id": "",
    "response": "approved",
    "response_body": {
      "card": {
        "auth_code": "TAS000",
        "avs_response_code": "",
        "card_type": "visa",
        "created_at": "2019-09-25T19:47:14.007998277Z",
        "cvv_response_code": "",
        "expiration_date": "12/20",
        "first_six": "411111",
        "id": "bm5s8gm9ku6ejcu15ta0",
        "last_four": "1111",
        "masked_card": "411111******1111",
        "processor_id": "bm5s7im9ku6el2qvgsrg",
        "processor_response_code": "00",
        "processor_response_text": "APPROVAL TAS000 ",
        "processor_specific": null,
        "processor_type": "tsys_sierra",
        "response": "approved",
        "response_code": 100,
        "updated_at": "2019-09-25T19:47:14.027775736Z"
      }
    },
    "response_code": 100,
    "settled_at": null,
    "settlement_batch_id": "",
    "ship_from_postal_code": "",
    "shipping_address": {
      "address_line_1": "",
      "address_line_2": "",
      "city": "",
      "company": "",
      "country": "US",
      "email": "",
      "fax": "",
      "first_name": "",
      "last_name": "",
      "phone": "",
      "postal_code": "",
      "state": ""
    },
    "shipping_amount": 0,
    "status": "pending_settlement",
    "subscription_id": "",
    "summary_commodity_code": "",
    "surcharge": 0,
    "tax_amount": 0,
    "tax_exempt": false,
    "tip_amount": 0,
    "transaction_source": "api",
    "type": "sale",
    "updated_at": "2019-09-25T19:47:14.031268348Z",
    "user_id": "testmerchant12345678",
    "user_name": "test_merchant"
  },
  "msg": "success",
  "status": "success"
}

Account Updater

json
{
  "account_type": "merchant",
  "account_type_id": "testmerchant12345678",
  "transaction_id": "",
  "action_at": "2020-10-08T18:45:39.053107-05:00",
  "data": {
    "card_id": "btvq916vvhfmlmgnfdh0",
    "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "expiration_date": "02/25",
    "flags": null,
    "generic_card_level": "",
    "instrument_type": "credit",
    "masked_number": "411111******1111",
    "processor_id": "",
    "record_id": "btvq916vvhfmlmgnfdhg",
    "status": "updated_card"
  },
  "msg": "success",
  "status": "success",
  "type": "transaction_automatic_account_updater_vault_update"
}

Settlement Batch

JSON
{
    "account_type": "merchant",
    "account_type_id": "testmerchant12345678",
    "action_at": "2024-06-05T15:45:17.788695-05:00",
    "data": {
        "amount_captured": 1630,
        "amount_credit": 0,
        "base_amount": 1630,
        "batch_date": "2024-06-05T15:45:17.728045-05:00",
        "batch_number": 1,
        "id": "cpgcsnbug2jm1i6kv4vg",
        "merchant_id": "testmerchant12345678",
        "net_amount": 1630,
        "net_deposit": 1630,
        "num_transactions": 2,
        "payment_adj_amount": 0,
        "processor_id": "cpg94brug2jhhon86rsg",
        "processor_name": "TSYS default",
        "processor_type": "tsys_sierra",
        "response_code": 100,
        "response_message": "ACCEPT",
        "surcharge_amount": 0
    },
    "msg": "success",
    "status": "success",
    "type": "settlement_batch"
}

The type field can change depending on the main webhook type, which is either transaction, settlement, or cardsync.

For transaction, the type can be - test, transaction_create, transaction_update, transaction_void, transaction_capture, transaction_settlement.
For cardsync, the type can be - transaction_automatic_account_updater_vault_update, transaction_automatic_account_updater_vault_iw.
For settlement, the type can be - settlement_batch.

Then, inside of the data object, the status field will change depending on what type of webhook is being sent.

For transaction webhook types, data.status can be - unknown, declined, authorized, pending_settlement, settled, voided, refunded, returned, late_return, pending, partially_refunded, flagged, flagged_partner.
For cardsync webhook types, data.status can be - updated_card.
For settlement webhook types, there is no data.status field.