Payments Example

Introduction

In this example we’ll go through all of the steps and give examples on what you’ll need to do to implement a call to pay out funds to customers. Once you’ve read through this guide you’ll know everything that is required by us for you to use our payout system.

Let’s assume you operate in the US, and would like to use our site to send funds to Nigerian bank accounts. You have a user registered and KYC’d on your site called Jane Doe. She is a recurring user on your site, and her user ID in your system is Sender:US:234523. She creates a transaction on your site asking you to send 100 USD to her partner in Nigeria John Doe. She is going to fund the transaction ins USD. Let’s assume the transaction created in your system has an ID of Transaction:NGN:17523.

Once you have the details collected on your end, you can then initiate the transaction in our system. This example will guide you through the details you need to set and what calls you need to make. Since the easiest way to access our system is through our SDKs this guide is mainly aimed at users of our SDKs.

Warning! We require that all implementations to our site pass our onboarding test before we allow users to access our prouction site. This example shows the preferred solution to all requirements we have, so following this example will make sure you will adhere to our requirements and will hopefully pass our onboarding test.

Note! This guide assumes that you are doing a full KYC on your senders which we have approved. If not some of the steps can be slightly more complex than shown in this guide, most notably you’ll need to create senders separately and wait for them to get approved before you can create transactions. Please check our KYC guide for more info.

Authentication

First thing is that you’ll need to authenticate to use our site. You can get your API key and secret by logging in to the TransferZero developer portal.

Note! You need to contact us before you are allowed to use our sandbox environment. Once you reach out to us you’ll also be invited to our Slack channel where you can post questions about our API that will be answered by our Engineering team. We prefer technical communication to go through this Slack channel.

Once you obtain your api key and secret you’ll need to set up your environment with them:

See the authentication documentation for more details on the authentication process if you’re not using our SDKs

Configuration configuration = new Configuration();
configuration.ApiKey = "<key>";
configuration.ApiSecret = "<secret>";
configuration.BasePath = "https://api-sandbox.transferzero.com/v1";
Dim configuration As Configuration = New Configuration()
configuration.ApiKey = "KEY"
configuration.ApiSecret = "SECRET"
configuration.BasePath = "https://api-sandbox.transferzero.com/v1"
ApiClient apiClient = new ApiClient();
apiClient.setApiKey("<key>");
apiClient.setApiSecret("<secret>");
apiClient.setBasePath("https://api-sandbox.transferzero.com/v1");
const apiClient = new ApiClient();
apiClient.apiKey = '<key>';
apiClient.apiSecret = '<secret>';
apiClient.basePath = 'https://api-sandbox.transferzero.com/v1';
TransferZero\Configuration::getDefaultConfiguration()
    ->setHost("https://api-sandbox.transferzero.com/v1")
    ->setApiKey("<key>")
    ->setApiSecret("<secret>");
TransferZero.configure do |config|
    config.api_key = '<key>'
    config.api_secret = '<secret>'
    config.host = 'https://api-sandbox.transferzero.com/v1'
end

Sender details

Next step is setting up the sender details. Generally you’ll need to send us the following information for every sender:

  • name
  • country
  • phone number
  • address
  • birth date
  • email address

Note! Dependent on your KYC processes you might be able opt in to use WTR2 rules for your senders in which case the requirements are slightly relaxed. Please contact us if this is of any interest to you!

You should also send us the ID you use in your local system to link to this sender, in this example Sender:US:234523. By doing so you’ll be able to handle senders much easier in our system.

Warning! Being able to re-use senders is one of our main requirements for successful onboarding. By linking the senders from your system to our system you can make sure you won’t have issues fulfilling this dependency.

{
    "first_name": "Jane",
    "last_name": "Doe",

    "phone_country": "US",
    "phone_number": "5555551234",

    "country": "US",
    "city": "New York",
    "street": "20 W 34th St",
    "postal_code": "10001",
    "address_description": "",

    "birth_date": "1974-12-24",

    // you can usually use your company's contact email address here
    "email": "info@transferzero.com",

    "external_id": "Sender:US:234523",

    // these fields are mandatory, but you can usually leave them with the following default values:
    "documents": [ ],
    "ip": "127.0.0.1",
    "metadata": {}
}
Sender sender = new Sender(
  firstName: "Jane",
  lastName: "Doe",

  phoneCountry: "US",
  phoneNumber: "5555551234",

  country: "US",
  city: "New York",
  street: "20 W 34th St",
  postalCode: "10001",
  addressDescription: "",

  birthDate: DateTime.Parse("1974-12-24"),

// you can usually use your company's contact email address here
  email: "info@transferzero.com",

  externalId: "Sender:US:234523",

// you'll need to set these fields but usually you can leave them the default
  ip: "127.0.0.1",
  documents: new List<Document>());
Dim sender as Sender = New Sender(
  firstName:="Jane",
  lastName:="Doe",

  phoneCountry:="US",
  phoneNumber:="5555551234",

  country:="US",
  city:="New York",
  street:="20 W 34th St",
  postalCode:="10001",
  addressDescription:="",

  birthDate:=DateTime.Parse("1974-12-24"),

' you can usually use your company's contact email address here
  email:="info@transferzero.com",

  externalId:="Sender:US:234523",

' you'll need to set these fields but usually you can leave them the default
  ip:="127.0.0.1",
  documents:=New List(Of Document)()))
Sender sender = new Sender();
sender.setFirstName("Jane");
sender.setLastName("Doe");

sender.setPhoneCountry("US");
sender.setPhoneNumber("5555551234");

sender.setCountry("US");
sender.setCity("New York");
sender.setStreet("20 W 34th St");
sender.setPostalCode("10001");
sender.setAddressDescription("");

sender.setBirthDate(LocalDate.parse("1974-12-24"));

// you can usually use your company's contact email address here
sender.setEmail("info@transferzero.com");

sender.setExternalId("Sender:US:234523");

// you'll need to set these fields but usually you can leave them the default
sender.setIp("127.0.0.1");
sender.setDocuments(new ArrayList<>());
const sender = new TransferZeroSdk.Sender();
sender.first_name = "Jane";
sender.last_name = "Doe";

sender.phone_country = "US";
sender.phone_number = "5555551234";

sender.country = "US";
sender.city = "New York";
sender.street = "20 W 34th St";
sender.postal_code = "10001";
sender.address_description = "";

sender.birth_date = "1974-12-24";

// you can usually use your company's contact email address here
sender.email = "info@transferzero.com";

sender.external_id = "Sender:US:234523";

// you'll need to set these fields but usually you can leave them the default
sender.ip = "127.0.0.1";
sender.documents = [];
$sender = new Sender();
$sender->setFirstName("Jane");
$sender->setLastName("Doe");

$sender->setPhoneCountry("US");
$sender->setPhoneNumber("5555551234");

$sender->setCountry("US");
$sender->setCity("New York");
$sender->setStreet("20 W 34th St");
$sender->setPostalCode("10001");
$sender->setAddressDescription("");

$sender->setBirthDate("1974-12-24");

// you can usually use your company's contact email address here
$sender->setEmail("info@transferzero.com");

$sender->setExternalId("Sender:US:234523");

// you'll need to set these fields but usually you can leave them the default
$sender->setIp("127.0.0.1");
$sender->setDocuments([]);
sender = TransferZero::Sender.new
sender.first_name = "Jane"
sender.last_name = "Doe"

sender.phone_country = "US"
sender.phone_number = "5555551234"

sender.country = "US"
sender.city = "New York"
sender.street = "20 W 34th St"
sender.postal_code = "10001"
sender.address_description = ""

sender.birth_date = "1974-12-24"

# you can usually use your company's contact email address here
sender.email = "info@transferzero.com"

sender.external_id = "Sender:US:234523"

# you'll need to set these fields but usually you can leave them the default
sender.ip = "127.0.0.1"
sender.documents = []

Recipient details

Once you have the sender let’s set up the recipient as well. In this example we’re going to do an NGN::Bank payout, which requires a name and the bank account details. Other payout providers might have different requirements, to check them please see our payout details documentation. You’ll also need to send in how much you wish to send. In this example we’re sending $100 worth of funds, that are going to be received in NGN - we’ll calculate the proper amount based on the exchange rates.

{
    "requested_amount": "100",
    "requested_currency": "USD",
    "payout_method": {
        "type": "NGN::Bank",
        "details": {
            "first_name": "John",
            "last_name": "Doe",
            "bank_account": "1234567890",
            "bank_code": "082",
            "bank_account_type": "20"
        }
    }
}
PayoutMethodDetails details = new PayoutMethodDetails(
  firstName: "John",
  lastName: "Doe",
  bankAccount: "1234567890",
  bankCode: "082",
  bankAccountType: PayoutMethodBankAccountTypeEnum._20);

PayoutMethod payout = new PayoutMethod(
  type: "NGN::Bank",
  details: details);

Recipient recipient = new Recipient(
  requestedAmount: 100000,
  requestedCurrency: "NGN",
  payoutMethod: payout);
Dim details as PayoutMethodDetails = New PayoutMethodDetails(
  firstName:="John",
  lastName:="Doe",
  bankAccount:="1234567890",
  bankCode:="082",
  bankAccountType:=PayoutMethodBankAccountTypeEnum._20)

Dim payout as PayoutMethod = New PayoutMethod(
  type:="NGN::Bank",
  details:=details)

Dim recipient as Recipient = New Recipient(
  requestedAmount:=100000,
  requestedCurrency:="NGN",
  payoutMethod:=payout)
PayoutMethodDetails details = new PayoutMethodDetails();
details.setFirstName("John");
details.setLastName("Doe");
details.setBankAccount("1234567890");
details.setBankCode("082");
details.setBankAccountType(PayoutMethodBankAccountTypeEnum._20);

PayoutMethod payout = new PayoutMethod();
payout.setType("NGN::Bank");
payout.setDetails(details);

Recipient recipient = new Recipient();
recipient.setRequestedAmount(new BigDecimal("100000"));
recipient.setRequestedCurrency("NGN");
recipient.setPayoutMethod(payout);
const details = new TransferZeroSdk.PayoutMethodDetails();
details.first_name = "John";
details.last_name = "Doe";
details.bank_account = "1234567890";
details.bank_code = "082";
details.bank_account_type = "20";

const payout = new TransferZeroSdk.PayoutMethod();
payout.type = "NGN::Bank";
payout.details = details;

const recipient = new TransferZeroSdk.Recipient();
recipient.requested_amount = 100000;
recipient.requested_currency = "NGN";
recipient.payout_method = payout;
$details = new PayoutMethodDetails();
$details->setFirstName("John");
$details->setLastName("Doe");
$details->setBankAccount("1234567890");
$details->setBankCode("082");
$details->setBankAccountType("20");

$payout = new PayoutMethod();
$payout->setType("NGN::Bank");
$payout->setDetails($details);

$recipient = new Recipient();
$recipient->setRequestedAmount(100000);
$recipient->setRequestedCurrency("NGN");
$recipient->setPayoutMethod($payout);
details = TransferZero::PayoutMethodDetails.new
details.first_name = "John"
details.last_name = "Doe"
details.bank_account = "1234567890"
details.bank_code = "082"
details.bank_account_type = "20"

payout = TransferZero::PayoutMethod.new
payout.type = "NGN::Bank"
payout.details = details

recipient = TransferZero::Recipient.new
recipient.requested_amount = 100000
recipient.requested_currency = "NGN"
recipient.payout_method = payout

Tying all together

Finally we need to tie the sender and recipient together into a transaction. The are only two extra details required: the external ID of this transaction (how you refer to this transaction in your system), and the currency this transaction will be funded (the currency you held your balance with us).

Note! While the external ID is generally optional, sending it can help tie transactions between our systems easier. Also you can use it to block duplicate transactions as we won’t allow you to send in another transaction with the same external ID in the future.

{
    "sender": {
        // sender details from the previous section
    },
    "recipients": [{
        // recipient details from the previous section
    }],
    "input_currency": "USD",
    "external_id": "Transaction:NGN:17523",
    "metadata": {}
}
Transaction transaction = new Transaction(
  sender: sender,
  recipients: new List<Recipient>() { recipient },
  inputCurrency: "NGN",
  externalId: "Transaction:NGN:17523");
Dim transaction as Transaction = New Transaction(
  sender:=sender,
  recipients:=New List(Of Recipient)() From { recipient },
  inputCurrency:="NGN",
  externalId:="Transaction:NGN:17523")
Transaction transaction = new Transaction();
transaction.setSender(sender);
transaction.addRecipientsItem(recipient);
transaction.setInputCurrency("NGN");
transaction.setExternalId("Transaction:NGN:17523");
const transaction = new TransferZeroSdk.Transaction();
transaction.sender = sender;
transaction.recipients = [recipient];
transaction.input_currency = "NGN";
transaction.external_id = "Transaction:NGN:17523";
$transaction = new Transaction();
$transaction->setSender($sender);
$transaction->setRecipients([recipient]);
$transaction->setInputCurrency("NGN");
$transaction->setExternalId("Transaction:NGN:17523");
transaction = TransferZero::Transaction.new
transaction.sender = sender
transaction.recipients = [recipient]
transaction.input_currency = "NGN"
transaction.external_id = "Transaction:NGN:17523"

Creating and funding the transaction

Now that we have generated the objects, we’ll need to call the endpoint. The easiest way is to use our create_and_fund endpoint which will both create the transaction and immediately deduct funds from your internal wallet to fund it.

Warning! Using this endpoint means that you implicitly approve the exchange rate of the transaction. You can also opt in to create and fund the transactions separately, and double check the amounts inside the transaction after it has been created, and only fund it afterwards. Transactions will never initate payments before they are funded. There is also a calculate endpoint you can use to mock transaction creation and get the exchange rates used without creating the transaction itself.

POST /v1/transactions/create_and_fund

{
    "transaction": {
        // full transaction details from the previous section
    }
}
TransactionRequest request = new TransactionRequest(
  transaction: transaction);

TransactionsApi api = new TransactionsApi(configuration);

try {
  TransactionResponse response = api.CreateAndFundTransaction(request);
} catch (ApiException e) {
  if (e.IsValidationError)
    TransactionResponse response = e.ParseObject<TransactionResponse>();
    // Process validation error
  }
  throw e;
}
Dim request as TransactionRequest = New TransactionRequest(
  transaction:=transaction)

Dim api as TransactionsApi = New TransactionsApi(configuration)

Try
  Dim response as TransactionResponse = api.CreateAndFundTransaction(request)
Catch e As ApiException
  If e.IsValidationError Then
    Dim response as TransactionResponse = e.ParseObject(Of TransactionResponse)()
    ' Process validation error
  End If
  Throw e
End Try
TransactionRequest request = new TransactionRequest();
request.setTransaction(transaction);

TransactionsApi api = new TransactionsApi(apiClient);

try {
  TransactionResponse response = api.createAndFundTransaction(request);
} catch (ApiException e) {
  if (e.isValidationError()) {
    TransactionResponse response = e.getResponseObject(TransactionResponse.class);
    // Process validation error
  }
  throw e;
};
const request = new TransferZeroSdk.TransactionRequest();
request.transaction = transaction;

const api = new TransferZeroSdk.TransactionsApi(apiClient);

try {
  const response = await api.createAndFundTransaction(request);
} catch (e) {
  if (e.isValidationError) {
    const response = e.getResponseObject();
    // Process validation error
  }
  throw e;
}
$request = new TransactionRequest();
$request->setTransaction($transaction);

$api = new TransactionsApi();

try {
  $response = api->createAndFundTransaction($request);
} catch (ApiException $e) {
  if ($e->isValidationError()) {
    $response = $e->getResponseObject();
    // Process validation error
  }
  throw $e;
}
request = TransferZero::TransactionRequest.new
request.transaction = transaction

api = TransferZero::TransactionsApi.new

begin
  response = api.create_and_fund_transaction(request)
rescue TransferZero::ApiError => e
  if e.validation_error
    response = e.response_object("TransactionResponse")
    # Process validation error
  end
  raise e
end

Info! Please note that as you can usually always expect validation errors calling any of our endpoints, so you should always check for errors when calling our APIs. A transaction which has validation errors is never created, you have to re-create it once the problems with the details have been fixed.

Waiting for the transaction to finish

Once the transaction is created and funded it goes into our processing queue. During this processing we will constantly try to pay out the funds. Dependent on the payment corridor and the recipient this can take anything from couple of minutes (for example for NGN Bank payments) to a couple of days (for example for MAD cash pickup transactions). In order to let you know when the transaction has finished paying out we will send out a notification to your registered webhook address, to let you know that the transaction has finished processing.

To set up webhooks you can use the TransferZero developer portal. We will always call the endpoint you set up here using POST, and the request will always contain the same authentication headers you’ll need to use to call our APIs, so you can use them to validate that the request is coming from us.

See the webhooks documentation for more details on what you can expect to receive on the endpoint you register with us.

Once setting up an endpoint where you’ll be receiving callbacks you can use the following code snippet to both verify that the webhook we sent out is legitimate, and then parse it’s contents regardless of type.

The details you need to provide Is

  • the body of the webhook/callback you received as a string
  • the url of your webhook, where you are awaiting the callbacks - this has to be the full URL
  • the authentication headers you have received on your webhook endpoint - as a dictionary
string webhookContent = "{ full body of the webhook }";
string url = "<full url of the webhook>";

Dictionary<string, string> headers = new Dictionary<string, string>();
headers.Add("Authorization-Nonce", "<nonce from the webhook headers>");
headers.Add("Authorization-Signature", "<signature from the webhook headers>");
headers.Add("Authorization-Key", "<key from the webhook headers>");

if (configuration.ValidWebhookRequest(url, webhookContent, headers))
{
    Webhook webhook = configuration.ParseString<Webhook>(webhookContent);
    if (webhook.Event.StartsWith("transaction")) {
        TransactionWebhook transactionWebhook = configuration.ParseString<TransactionWebhook>(webhookContent);
        Guid transactionId = transactionWebhook.Object.Id;
        string externalId = transactionWebhook.Object.ExternalId;
        if (webhook.Event == "transaction.paid_out") {
            // handle transaction was successful event
        } else if (webhook.Event == "transaction.refunded") {
            // handle transaction has been cancelled and will not process further event
        }
    } else if (webhook.Event.StartsWith("recipient")) {
        RecipientWebhook recipientWebhook = configuration.ParseString<RecipientWebhook>(webhookContent);
        if (webhook.Event == "recipient.error") {
            string errorMessage = recipientWebhook.Object.StateReason;
            Guid transactionId = recipientWebhook.Object.TransactionId;
            // handle showing the error message to customer servic. Note: transaction is still not cancellede
        }
    } else if (webhook.Event.StartsWith("sender")) {
        SenderWebhook senderWebhook = configuration.ParseString<SenderWebhook>(webhookContent);
        // handle sender events
    }
}

Once setting up an endpoint where you’ll be receiving callbacks you can use the following code snippet to both verify that the webhook we sent out is legitimate, and then parse it’s contents regardless of type.

The details you need to provide Is

  • the body of the webhook/callback you received as a string
  • the url of your webhook, where you are awaiting the callbacks - this has to be the full URL
  • the authentication headers you have received on your webhook endpoint - as a dictionary
Dim webhookContent As String = "{ full body of the webhook }"
Dim url As String = "<full url of the webhook>"

Dim headers As Dictionary(Of String, String) = New Dictionary(Of String, String)()
headers.Add("Authorization-Nonce", "<nonce from webhook headers>")
headers.Add("Authorization-Signature", "<signature from webhook headers>")
headers.Add("Authorization-Key", "<key from webhook headers>")

If configuration.ValidWebhookRequest(url, webhookContent, headers) Then
    Dim webhook As Webhook = configuration.ParseString(Of Webhook)(webhookContent)
    If webhook.[Event].StartsWith("transaction") Then
        Dim transactionWebhook As TransactionWebhook = configuration.ParseString(Of TransactionWebhook)(webhookContent)
        Dim transactionId As Guid = transactionWebhook.Object.Id
        Dim externalId As String = transactionWebhook.Object.ExternalId
        If webhook.[Event].Equals("transaction.paid_out") Then
          ' handle transaction was successful event
        ElseIf webhook.[Event].Equals("transaction.refunded") Then
          ' handle transaction has been cancelled and will not process further event
        End If
    ElseIf webhook.[Event].StartsWith("recipient") Then
        Dim recipientWebhook As RecipientWebhook = configuration.ParseString(Of RecipientWebhook)(webhookContent)
        If webhook.[Event].Equals("recipient.error") Then
            Dim errorMessage As String = recipientWebhook.Object.StateReason;
            Dim transactionId As Guid = recipientWebhook.Object.TransactionId;
            ' handle showing the error message to customer servic. Note: transaction is still not cancellede
        End If
    ElseIf webhook.[Event].StartsWith("sender") Then
        Dim senderWebhook As SenderWebhook = configuration.ParseString(Of SenderWebhook)(webhookContent)
        ' handle sender webhooks
    End If
End If

Once setting up an endpoint where you’ll be receiving callbacks you can use the following code snippet to both verify that the webhook we sent out is legitimate, and then parse it’s contents regardless of type.

The details you need to provide Is

  • the body of the webhook/callback you received as a string
  • the url of your webhook, where you are awaiting the callbacks - this has to be the full URL
  • the authentication headers you have received on your webhook endpoint - as a map
String webhookBody = "{ full body of the webhook }";
String webhookUrl = "<full url of the webhook>";

Map<String, String> webhookHeaders = new HashMap<String, String>();
webhookHeaders.put("Authorization-Nonce", "<nonce from webhook headers>");
webhookHeaders.put("Authorization-Key", "<key from webhook headers>");
webhookHeaders.put("Authorization-Signature", "<signature from webhook headers>");

if (apiClient.validateWebhookRequest(webhookUrl, webhookBody, webhookHeaders)) {
    Webhook webhook = apiClient.parseResponseString(webhookBody, Webhook.class);
    if (webhook.getEvent().startsWith("transaction")) {
        TransactionWebhook transactionWebhook = apiClient.parseResponseString(webhookBody, TransactionWebhook.class);
        UUID transactionId = transactionWebhook.getObject().getId();
        String externalId = transactionWebhook.getObject().getExternalId();
        if (webhook.getEvent().equals("transaction.paid_out")) {
            // handle transaction was successful event
        } else if (webhook.getEvent().equals("transaction.refunded")) {
            // handle transaction has been cancelled and will not process further event
        }
    } else if (webhook.getEvent().startsWith("recipient")) {
        RecipientWebhook recipientWebhook = apiClient.parseResponseString(webhookBody, RecipientWebhook.class);
        if (webhook.getEvent().equals("recipient.error")) {
            String errorMessage = transactionWebhook.getObject().getStateReason();
            UUID transactionId = transactionWebhook.getObject().getTransactionId();
            // handle showing the error message to customer servic. Note: transaction is still not cancellede
        }
    } else if (webhook.getEvent().startsWith("sender")) {
        SenderWebhook senderWebhook = apiClient.parseResponseString(webhookBody, SenderWebhook.class);
        // handle sender webhooks
    }
}

Once setting up an endpoint where you’ll be receiving callbacks you can use the following code snippet to both verify that the webhook we sent out is legitimate, and then parse it’s contents regardless of type.

The details you need to provide Is

  • the body of the webhook/callback you received as a string
  • the url of your webhook, where you are awaiting the callbacks - this has to be the full URL
  • the authentication headers you have received on your webhook endpoint - as an object
const webhookContent = `{ full body of the webhook }`;
const webhookUrl = "<full url of the webhook>";

const webhookHeader = {
  "Authorization-Nonce": "<nonce from webhook headers>",
  "Authorization-Key": "<key from webhook headers>",
  "Authorization-Signature": "<signature from webhook headers>"};

if (apiClient.validateRequest(webhookUrl, webhookContent, webhookHeader)) {
  const webhook = apiClient.parseResponseString(webhookContent,TransferZeroSdk.Webhook);
  if (webhook.event.startsWith('transaction')) {
    const transactionWebhook = apiClient.parseResponseString(webhookContent, TransferZeroSdk.TransactionWebhook);
    const transactionId = transactionWebhook.object.id;
    const externalId = transactionWebhook.object.external_id;
    if (webhook.event == 'transaction.paid_out') {
      // handle transaction was successful event
    } else if (webhook.event == 'transaction.refunded') {
      // handle transaction has been cancelled and will not process further event
    }
  } else if (webhook.event.startsWith('recipient')) {
    const recipientWebhook = apiClient.parseResponseString(webhookContent, TransferZeroSdk.RecipientWebhook);
    if (webhook.event == 'recipient.error') {
      const errorMessage = recipientWebhook.object.state_reason;
      const transactionId = recipientWebhook.object.transaction_id;
      // handle showing the error message to customer servic. Note: transaction is still not cancellede
    }
  } else if (webhook.event.startsWith('sender')) {
    const senderWebhook = apiClient.parseResponseString(webhookContent, TransferZeroSdk.SenderWebhook);
    // handle sender webhook
  }
}

Once setting up an endpoint where you’ll be receiving callbacks you can use the following code snippet to both verify that the webhook we sent out is legitimate, and then parse it’s contents regardless of type.

The details you need to provide Is

  • the body of the webhook/callback you received as a string
  • the url of your webhook, where you are awaiting the callbacks - this has to be the full URL
  • the authentication headers you have received on your webhook endpoint - as an associative array
$webhook_content = "{ full body of the webhook }";
$webhook_url = "<full url of the webhook>";

$webhook_headers = [
    "Authorization-Nonce" => "<nonce from the webhook headers>",
    "Authorization-Key" => "<key from the webhook headers>",
    "Authorization-Signature" => "<signature from the webhook headers>"];

if (new WebhooksApi()->validateWebhookRequest($webhook_url, $webhook_content, $webhook_headers)) {
    $webhook = new WebhooksApi()->parseResponseString($webhook_content, 'Webhook');
    if (strpos($webhook->getEvent(), 'transaction') === 0) {
        $transactionWebhook = $webhooksApi->parseResponseString($webhook_content, 'TransactionWebhook');
        $transacionId = $transactionWebhook->getObject()->getId();
        $externalId = $transactionWebhook->getObject()->getExternalId();
        if ($webhook->getEvent() == 'transaction.paid_out') {
            // handle transaction was successful event
        } elseif ($webhook->getEvent() == 'transaction.refunded') {
            // handle transaction has been cancelled and will not process further event
        }
    } elseif (strpos($webhook->getEvent(), 'recipient') === 0) {
        $recipientWebhook = $webhooksApi->parseResponseString($webhook_content, 'RecipientWebhook');
        if ($webhook->getEvent() == 'recipient.error') {
            $errorMessage = $recipientWebhook->getObject()->getStateReason();
            $transactionId = $recipientWebhook->getObject()->getTransactionId();
            // handle showing the error message to customer servic. Note: transaction is still not cancellede
        }
    } elseif (strpos($webhook->getEvent(), 'sender') === 0) {
        $senderWebhook = $webhooksApi->parseResponseString($webhook_content, 'SenderWebhook');
        // handle sender webhook
    }
}

Once setting up an endpoint where you’ll be receiving callbacks you can use the following code snippet to both verify that the webhook we sent out is legitimate, and then parse it’s contents regardless of type.

The details you need to provide Is

  • the body of the webhook/callback you received as a string
  • the url of your webhook, where you are awaiting the callbacks - this has to be the full URL
  • the authentication headers you have received on your webhook endpoint - as a hash
body = "{ full body of the webhook }"
webhook_url = "<full url of the webhook>"

headers = {
    "Authorization-Nonce": "<nonce from webhook headers>",
    "Authorization-Key": "<key from webhook headers>",
    "Authorization-Signature": "<signature from webhook headers>"}

if TransferZero::ApiClient.new.validate_webhook_request(webhook_url, body, headers)
  webhook = TransferZero::ApiClient.new.parse_response(body, "Webhook")
  if webhook['event'].start_with?('transaction')
    transaction_webhook = webhook_api.parse_response(body, 'TransactionWebhook')
    transaction_id = transaction_webhook.object.id
    external_id = transaction_webhook.object.external_id
    if webhook['event'] == 'transaction.paid_out'
      # handle transaction was successful event
    elsif webhook['event'] == 'transaction.refunded'
      # handle transaction has been cancelled and will not process further event
    end
  elsif webhook['event'].start_with?('recipient')
    recipient_webhook = webhook_api.parse_response(body, 'RecipientWebhook')
    if webhook['event'] == 'recipient.error'
      error_message = recipient_webhook.object.state_reason
      transaction_id = recipient_webhook.object.transaction_id
      # handle showing the error message to customer service. Note: transaction is still not cancelled
    end
  elsif webhook['event'].start_with?('sender')
    sender_webhook = webhook_api.parse_response(body, 'SenderWebhook')
    # handle sender webhook
  end
end

You will need to handle three major webhook events namely transaction.paid_out, transaction.refunded and recipient.error. The first one will notify you if the transaction was succesfully paid out to the recipient. The second one will notify you if a transaction was cancelled and the funds have been returned to your balance. Finally the last one will notify you if there was a problem while trying to pay out the recipient.

If you don’t do full KYC then you should also sign up for the sender.approved event which will notify you if a sender is approved and can start transacting, and the sender.rejected event which signals if there is a problem with the sender.

If the transaction was paid there is nothing more to do from our end, you can update the system on your end to let your customer know that the transaction has paid out. On cancellation you’ll need to update your system to know that this transaction will not process further.

If there was an error you can find the error description in the state_reason field of the recipient. Note that this error is usually technical in nature hence we don’t suggest to showing this to your customer verbatim. It can be shown to your customer support staff however.

Warning! Due to how the markets we operate in work we will contantly retry to pay out transactions, so even after you receive a recipient.error error webhook we will still keep trying, until it has either paid out successfully (and you receive a transaction.paid_out event), or you explicitly cancel the transaction by calling the cancel recipient endpoint. We will also cancel transactions we could not pay out within the first 24 hours, in which case you’ll receive a transaction.refunded webhook once the cancellation is processed, but you can opt out of this feature if you’d like to manage cancellation yourself.

You can read more about transaction errors in our error handling documentation.

Note! The recipient.error webhook only returns the recipient and not the full transaction details. You will receive the transaction id however and you’ll be able to query that separately if you need the transaction details as well.

Getting the transaction status manually

While we prefer that you primarily use the webhook facility to get notified about state changes, occasionally you might want to query a transaction’s state in our system so you’ll be able to update it on your end. You can either use the TransferZero Transaction ID for this, or the External Id we set up earlier. In this example we’ll use the External ID

GET /v1/transactions?external_id=Transaction:NGN:17523
TransactionsApi api = new TransactionsApi(configuration);

TransactionListResponse response = api.GetTransactions(externalId: "Transaction:NGN:17523");

if (response.Object.Count > 0) {
  Transaction transaction = response.Object[0];
} else {
  // handle not found scenario
}
Dim api as TransactionsApi = New TransactionsApi(configuration)

Dim response as TransactionListResponse = api.GetTransactions(externalId:="Transaction:NGN:17523")

If response.Object.Count > 0 Then
    Dim transaction As Transaction = response.Object(0)
Else
    ' handle not found scenario
End If
TransactionsApi api = new TransactionsApi(apiClient);

TransactionListResponse response = api.getTransactions().externalId("Transaction:NGN:17523").execute();

if (response.getObject().size() > 0) {
  Transaction transaction = response.getObject().get(0);
} else {
  // handle not found scenario
}
const api = new TransferZeroSdk.TransactionsApi(apiClient);

const response = await api.getTransactions({external_id: "Transaction:NGN:17523"});

if (response.object.length > 0) {
  const transaction = response.object[0];
} else {
  // handle not found scenario
}
$api = new TransactionsApi();

$response = api->getTransactions([external_id => "Transaction:NGN:17523"]);

if (count(response->getObject()) > 0) {
  $transaction = response->getObject()[0];
} else {
  // handle not found scenario
}
api = TransferZero::TransactionsApi.new

response = api.get_transactions({external_id: "Transaction:NGN:17523"})

if !response.object.empty?
  transaction = response.object.first
else
  # handle not found scenario
end

Cancelling transactions

Finally as mentioned above sometimes payment processing can fail for various reasons. While we try as hard as possible to pay out the funds to the intended recipient, occasionally you wish to stop this process and get a refund.

Warning! Unless the auto cancel feature is enabled for you we will never cancel funded transactions. You should make sure to only cancel and refund transactions on your end, once you have cancelled transactions inside TransferZero and got the refund confirmation through the webhook. We are constantly retrying transactions that are not cancelled in our system - even if the error message suggest that they are unlikely to pay out in the future (and we’ve seen that they could indeed pay out days later)

Note! As shown below cancellations are done on the recipient object and not the transaction.

DELETE /v1/recipients/e77f89fa-8371-496c-ae73-e56697fc08d8
RecipientsApi api = new RecipientsApi(configuration);

Recipient recipient = transaction.Recipients[0];
Guid recipientid = recipient.Id;

try {
  api.DeleteRecipient(recipientid);
} catch (ApiException e) {
  if (e.IsValidationError)
    // Process validation error
  }
  throw e;
}
Dim api as RecipientsApi = New RecipientsApi(configuration)

Dim recipient as Recipient = transaction.Recipients[0]
Dim recipientid as Guid = recipient.Id

Try
  api.DeleteRecipient(recipientid)
Catch e As ApiException
  If e.IsValidationError Then
    ' Process validation error
  End If
  Throw e
End Try
RecipientsApi api = new RecipientsApi(apiClient);

Recipient recipient = transaction.getRecipients().get(0);
UUID recipientid = recipient.getId();

try {
  api.deleteRecipient(recipientid);
} catch (ApiException e) {
  if (e.isValidationError()) {
    // Process validation error
  }
  throw e;
};
const api = new TransferZeroSdk.RecipientsApi(apiClient);

const recipient = transaction.recipients[0];
const recipientid = recipient.id;

try {
  await api.deleteRecipient(recipientid);
} catch (e) {
  if (e.isValidationError) {
    // Process validation error
  }
  throw e;
}
$api = new RecipientsApi();

$recipient = transaction->getRecipients[0];
$recipientid = recipient->getId;

try {
  api->deleteRecipient($recipientid);
} catch (ApiException $e) {
  if ($e->isValidationError()) {
    // Process validation error
  }
  throw $e;
}
api = TransferZero::RecipientsApi.new

recipient = transaction.recipients.first
recipientid = recipient.id

begin
  api.delete_recipient(recipientid)
rescue TransferZero::ApiError => e
  if e.validation_error
    # Process validation error
  end
  raise e
end

Note! Even if you have the auto cancellation feature enabled we still require you to be able to cancel transactions in our system, so you won’t need to wait for the 24 hours to elapse in case you wish to stop processing - for example if you know that the recipient details are incorrect

Schedule the onboarding call

Once you have implemented all of the steps based on the example above you have fulfilled our requirements for a successful integration. Please schedule an onboarding call with us and we’ll check your implementation and allow access to our production environment if all is good.


Improve this page