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
pending-capture
The payment gateway initiated a capture, but it wasn’t confirmed yet
completed
The payment was sucessfully completed
failed
The payment has been placed, but failed
voided
The payment was voided
refunded
The transaction was refunded (totally or partially)
in-review
The transaction is pending fraud review
blocked
The transaction was blocked by your blocking rules & by a fraud review
retrieval-request
The payment was previously completed but the customer does not recognize the payment and is asking for information
fraud-notification
The payment was previously completed but the bank is notifying you a chargeback might be on its way
chargeback-initiated
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

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_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB");

// 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_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB")


# 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_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB")


# 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_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB");

$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 "github.com/processout/processout-go"

var client = processout.New(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB",
)

// ProcessOutWebhook is the data received from a webhook request.
type ProcessOutWebhook struct {
    EventID string `json:"event_id"`
}

type TransactionEvent struct {
    Transaction *processout.Transaction `mapstructure:"transaction"`
}

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

    // Decode the webhook.
    var webhook ProcessOutWebhook
    defer r.Body.Close()
    if err := json.NewDecoder(r.Body).Decode(&webhook); err != nil {
        panic(err)
    }

    // Fetching the associated event.
    event, err := client.NewEvent().Find(webhook.EventID)
    if err != nil {
        // Webhook not found, possibly injected by a malicious third-party
        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:
        //
        // var ev TransactionEvent
        // if err := mapstructure.Decode(event.Data, &ev); err != nil {
        //  panic(err)
        // }
        // fmt.Println(*ev.Transaction.Amount, *ev.Transaction.Currency)

    }
    // Always return a HTTP 200 OK response to signal the event was received.
    w.WriteHeader(http.StatusOK)
}

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!