Dashboard ↗

You can use webhooks to respond to events that happen during transactions. Webhooks, which are also known as callbacks or Instant Payment Notifications (IPN), are HTTP POST requests that ProcessOut makes to your web server whenever an event occurs. Each POST contains an ID value that is unique to the event. Your code can pass this ID to our API to retrieve the full details of the event and then respond to it. You might use the details to begin delivery of an item after a successful payment, for example.

ProcessOut can use many different payment gateways and each one will probably have its own native format for event notifications. Our API is designed to abstract over these different formats to provide simple and consistent event data, regardless of where it originates from. For ease of use, the data includes information about the transaction state associated with the event.

Prerequisites

You must enable your webhook endpoint URL in the dashboard to receive notifications. See our guide to configuring webhooks for more information.

Note that the data in the body of the POST request is always in JSON format, so you should make sure your server accepts this. Also, you should remove all CSRF protection from your webhook endpoints to receive notifications. Most frameworks and CMS enable such protection by default for security reasons, but it can block ProcessOut from making POST requests to your endpoints.

You might find it useful to queue events and process several of them at once rather than responding fully to each event as soon as it arrives. Doing this allows you to acknowledge events quickly during busy periods and then process them later when things are quieter.

Retries

ProcessOut will retry webhook POST requests several times but it might be forced to give up if your server takes too long to acknowledge them. If this happens then you might miss important notifications.

We will automatically retry sending POST webhook event in an exponential backoff manner (delays between the next retries are exponentially increased). We keep retrying for at most 3 days with the initial interval equal to mathematical e constant (2.718...) in seconds. The table below presents the approximate intervals of subsequent retries (latency not included):

Retry no.Next delay intervalTime passed since 1st try
0 (first try)2.718 [s]0 [s]
17.389 [s]2.718 [s]
220 [s]10 [s]
355 [s]30 [s]
4148 [s]85 [s]
56 [m] 43 [s]3 [m] 53 [s]
618 [m] 17 [s]10 [m] 37 [s]
749 [m] 41 [s]28 [m] 53 [s]
8135 [m] 3 [s]78 [m] 34 [s] [s]
96 [h] 7 [m]3 [h] 33 [m]
1016 [h] 38 [m]9 [h] 41 [m]
1145 [h] 13[m]26 [h] 19 [m]
12--- (13th retry would exceed 3 days) ---71 [h] 31 [m]

Custom URL per invoice

curl -X POST https://api.processout.com/invoices \
    -u test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x:key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB \
    -d customer_id="cust_LvjCcLOVe6iWn2aeCNhNmK7RbbG6K8XF" \
    -d name="Amazing item" \
    -d amount="4.99" \
    -d currency="USD" \
    -d webhook_url="https://superstore.com/webhooks"
var ProcessOut = require("processout");
var client = new ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x",
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB");

client.newInvoice().create({
    customer_id:          "cust_LvjCcLOVe6iWn2aeCNhNmK7RbbG6K8XF",
    name:                 "Amazing item",
    amount:               "4.99",
    currency:             "USD",
    webhook_url:          "https://superstore.com/webhooks"
}).then(function(invoice) {
    //

}, function(err) {
    // An error occured

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

invoice = client.new_invoice().create({
    "customer_id":          "cust_LvjCcLOVe6iWn2aeCNhNmK7RbbG6K8XF",
    "name":                 "Amazing item",
    "amount":               "4.99",
    "currency":             "USD",
    "webhook_url":          "https://superstore.com/webhooks"
})
require "processout"
client = ProcessOut::Client.new(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB")

invoice = client.invoice.create(
    "customer_id":        "cust_LvjCcLOVe6iWn2aeCNhNmK7RbbG6K8XF",
    name:                 "Amazing item",
    amount:               "4.99",
    currency:             "USD",
    webhook_url:          "https://superstore.com/webhooks"
)
<?php
$client = new \ProcessOut\ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB");

$invoice = $client->newInvoice()->create(array(
    "customer_id"          => "cust_LvjCcLOVe6iWn2aeCNhNmK7RbbG6K8XF",
    "name"                 => "Amazing item",
    "amount"               => "4.99",
    "currency"             => "USD",
    "webhook_url"          => "https://superstore.com/webhooks"
));
import "github.com/processout/processout-go"

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

iv, err := client.NewInvoice().Create(processout.InvoiceCreateParameters{
    Invoice: &processout.Invoice{
        CustomerID:          processout.String("cust_LvjCcLOVe6iWn2aeCNhNmK7RbbG6K8XF"),
        Name:                processout.String("Amazing item"),
        Amount:              processout.String("4.99"),
        Currency:            processout.String("USD"),
        WebhookURL:          processout.String("https://superstore.com/webhooks"),
    },
})

Usage

Each webhook POST request from ProcessOut only contains the ID value of the associated event in the event_id field. Use this ID to fetch the full set of event data using our API.

The event_type allows you to filter received webhooks to update your system. The list of transaction events is available here.

Example of a received webhook

{
  "event_id":"ev_kHVj3R57mQBInbWwGy4co0235GTLIYTH",
  "event_type":"transaction.captured"
}

Consume webhooks

# Webhooks are not supported with cURL.
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 "invoice.completed":
        // Successful payment
        break;
    case "invoice.pending":
        // Payment still needs some time to be processed
        break;
    // ...
    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 data["name"] == "invoice.completed":
    # Successful payment
    pass

elif data["name"] == "invoice.pending":
    # Payment still needs some time to be processed
    pass

# ...

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 data["name"] == "invoice.completed"
    # Successful payment
elsif data["name"] == "invoice.pending"
    # Payment still needs some time to be processed

# ...

else
    # Shouldn't be here..
    puts "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($data["name"])
{
case "invoice.completed":
    // Successful payment
    break;
case "invoice.pending":
    // Payment still needs some time to be processed
    break;
// ...
default:
    echo "Unknown webhook action"; exit();
}
import "github.com/processout/processout-go"

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

// EventData is the definition of a ProcessOut Event data
type EventData struct {
    Name        string              `json:"name"`
    Sandbox     bool                `json:"sandbox"`
    Invoice     *processout.Invoice `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
    }

    e, _ := event.(EventData)
    switch e.Name {
    case "invoice.completed":
        // Successful payment

    case "invoice.pending":
        // Payment still needs some time to be processed

    // ...

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