Handle status changes and webhooks

When using the capture endpoint, a Transaction is returned if there was no error when processing the payment. However, the payment may still be pending, and a few fail-safes should be put in place.


Why should I use webhooks

Transactions can get stuck in an in-between pending & captured states, which makes it crucial to set up a way to be notified when the status of an invoice and/or transaction gets updated.

Most of the time with payments done by credit card, the merchant will know instantly if the payment made it through. However, with most alternative payment methods this is not the case, and payments can sometimes take a few minutes to be completely processed. Because of this, it is not possible to synchronously tell the customer or the merchant if the payment completely made it through: the payment is still pending.

When a payment is done with a credit card, it is also possible that it was successfully processed but that the customer choses to chargeback the payment later. Handling webhooks will also make you able to handle this case and appropriately update your customer’s profile on your business.

Transaction statuses

During a transaction’s life, its status will change several times depending on multiple factors. For instance, when a payment is placed and confirmed, the transaction’s status will switch from pending to completed.

You may find the list of all the available transaction’s statuses below:

waiting
No payment has been placed yet
pending
The payment is pending confirmation by the payment gateway
authorized
The payment was authorized but not yet captured
completed
The payment was sucessfully completed
failed
The payment has been placed, but failed
voided
The payment was voided
disputed
The payment was previously completed but the customer filled a dispute
solved
The previous dispute has been resolved in your favor
reversed
The previous dispute has been resolved in your customer’s favor
partially-refunded
The transaction was partially refunded
refunded
The transaction was refunded

Handling webhooks & status changes

First, we’ll have to create an endpoint on our online service that accepts requests from the Internet. This is the address to which ProcessOut will POST JSON data to notify you of the new events. Any CSRF protection should also be removed from this endpoint to ensure the correct processing of the webhook.

Handling the webhook data is very simple: ProcessOut will post the ID of the event that was fired, and we’ll just have to fetch its data from the ProcessOut’s API. ProcessOut does not directly send the whole data of the event to ensure that the merchants checks the legitimicy of the event by first calling the API.

# cURL cannot be used to handle webhooks
var ProcessOut = require("processout");
var client = new ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x",
    "key_sandbox_jqSPvwq3AG5MlYAgqxlwwgOcAC3Zy7d8");

// req is filled with the decoded json data from the request body
client.newEvent().find(req["event_id"]).then(function(event) {
    // We may now access the event
    var data = event.getData();

    switch (data["name"]) {
    case "transaction.captured":
        // Successful payment
        break;
    case "transaction.authorized":
        // Payment was authorized but not yet captured
        break;
    // ...
    // Access data from within the event data:
    // data["transaction"].getId();
    default:
        console.log("Unknown webhook action");
        return;
    }

}, function(err) {
    // An error occured, most likely the event was coming from an
    // untrusted source

});
import processout
client = processout.ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_jqSPvwq3AG5MlYAgqxlwwgOcAC3Zy7d8")


# req is filled with the decoded json data from the request body
event = client.new_event().find(req["event_id"])
data  = event.data

if event.name == "transaction.captured":
    # Successful payment
    pass

elif event.name == "transaction.authorized":
    # Payment was authorized but not yet captured
    pass

# ...
# Access data from within the event data:
# data.transaction.id

else:
    # Shouldn't be here..
    print("Unknown webhook action")
require "processout"
client = ProcessOut::Client.new(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_jqSPvwq3AG5MlYAgqxlwwgOcAC3Zy7d8")


# req is filled with the decoded json data from the request body
event = client.event.find(req.event_id)
data  = event.data

if event.name == "transaction.captured"
    # Successful payment

elsif event.name == "transaction.authorized"
    # Payment was authorized but not yet captured

# ...
# Access data from within the event data:
# data.transaction.id

else
    # Shouldn't be here..
    print("Unknown webhook action")
end
<?php
$client = new \ProcessOut\ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_jqSPvwq3AG5MlYAgqxlwwgOcAC3Zy7d8");

$reqRaw = trim(file_get_contents("php://input"));
$req    = json_decode($reqRaw, true);

$event = $client->newEvent()->find($req["event_id"]);
$data  = $event->getData();

switch($event->getName())
{
case "transaction.captured":
    // Successful payment
    break;
case "transaction.authorized":
    // Payment was authorized but not yet captured
    break;
// ...
// Access data from within the event:
// $data["transaction"]->getId();
default:
    echo "Unknown webhook action"; exit();
}
import "gopkg.in/processout.v4"
client := processout.New(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_jqSPvwq3AG5MlYAgqxlwwgOcAC3Zy7d8")

// EventData is the definition of a ProcessOut Event data
type EventData struct {
    Transaction *processout.Transaction `json:"invoice"`
}

// ProcessOutWebhook is the definition of a ProcessOut webhook
type ProcessOutWebhook struct {
    EventID string `json:"event_id"`
}

func handleProcessOutWebhooks(w http.ResponseWriter,
    r *http.Request) {

    defer r.Body.Close()
    reqRaw, err := ioutil.ReadAll(r.Body)
    if err != nil {
        panic(err)
    }

    // Decode the webhook
    webhook := &ProcessOutWebhook{}
    json.Unmarshal(reqRaw, &webhook)

    // Fetching the associated event
    event, err := client.NewEvent().Find(webhook.EventID)
    if err != nil {
        // Webhook not found, most likely coming from an
        // insecure source
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    switch event.Name {
    case "transaction.captured":
        // Successful payment

    case "transaction.authorized":
        // Payment was authorized but not yet captured

    // ...
    // Access data from within the Data field
    // e, _ := event.Data.(EventData)
    // fmt.Println(e.Transaction)

    default:
        // Return an HTTP OK response so that unsuported
        // webhooks do not get sent again
        w.WriteHeader(http.StatusOK)
        return
    }
}

The full list of events can be found in our API reference.

Wrapping up by setting the URL in your Dashboard

In order to receive webhooks, you will need to go to your Dashboard ↗ › Events › and add a new webhook endpoint. This endpoint will be called by our servers to notify yours of the new Event. This endpoint should therefore made accessible for us to POST JSON data to it.

You should now be ready to handle any event!