Skip to content
On this page

Checkout · Home > Payments > Payske Checkout

Fulfill orders with Checkout

Learn how to fulfill orders after a customer pays with Payske Checkout or Payske Payment Links.

After you integrate Payske Checkout or create a Payske Payment Link to take your customers to a payment form, you need notification that you can fulfill their order after they pay.

In this guide, you’ll learn how to:

  1. Receive an event notification when a customer pays you.
  2. Handle the event.
  1. Optionally, handle additional payment methods.
  2. Turn on your event handler in production.

....

  1. Create your event handler

In this section, you’ll create a small event handler so Payske can send you a checkout.session.completed event when a customer completes checkout.

First, create a new route for your event handler. Start by printing out the event you receive. You’ll verify that delivery is working in the next step:

ruby
# Set your secret key. Remember to switch to your live secret key in production.
# See your keys here: https://account.payske.com/api/key
Payske.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'

require 'sinatra'

post '/webhook' do
  payload = request.body.read

  # For now, you only need to print out the webhook payload so you can see
  # the structure.
  puts payload.inspect

  status 200
end
python
# Set your secret key. Remember to switch to your live secret key in production.
# See your keys here: https://account.payske.com/api/key
payske.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'

# Using Django
from django.http import HttpResponse

@csrf_exempt
def my_webhook_view(request):
  payload = request.body

  # For now, you only need to print out the webhook payload so you can see
  # the structure.
  print(payload)

  return HttpResponse(status=200)
php
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
\Payske\Payske::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc');

// Payske's webhooks are POST requests with a JSON body. The raw JSON can
// typically be read from stdin, but this may vary based on your server setup.
// The webhook data won't be available in the $_POST superglobal because
// Payske's webhook requests aren't sent in form-encoded format.
$payload = @file_get_contents('php://input');

// For now, you only need to log the webhook payload so you can see
// the structure.
error_log($payload);
java
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
Payske.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc";

// Using the Spark framework (http://sparkjava.com)
public Object handle(Request request, Response response) {
  String payload = request.body();

  System.out.println("Got payload: " + payload);

  response.status(200);
  return "";
}
typescript
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
const payske = require('payske')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');

// Using Express
const app = require('express')();

// Use body-parser to retrieve the raw body as a buffer
const bodyParser = require('body-parser');

app.post('/webhook', bodyParser.raw({type: 'application/json'}), (request, response) => {
  const payload = request.body;

  console.log("Got payload: " + payload);

  response.status(200).end();
});

app.listen(4242, () => console.log('Running on port 4242'));

go
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
payske.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"

http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) {
  const MaxBodyBytes = int64(65536)
  req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes)

  body, err := ioutil.ReadAll(req.Body)
  if err != nil {
    fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err)
    w.WriteHeader(http.StatusServiceUnavailable)
    return
  }

  fmt.Fprintf(os.Stdout, "Got body: %s\n", body)

  w.WriteHeader(http.StatusOK)
})

csharp

PayskeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc";

using System;
using System.IO;
using Microsoft.AspNetCore.Mvc;

namespace workspace.Controllers
{
  [Route("api/[controller]")]
  public class PayskeWebHook : Controller
  {
    [HttpPost]
    public async Task<IActionResult> Index()
    {
      var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();

      Console.WriteLine(json);

      return Ok();
    }
  }
}

Testing

Run your server (for example, on localhost:4242).

Next, go through Checkout as a customer:

  • Click your checkout button (you probably set this up in the Accept a payment guide)
  • Fill out your payment form with test data
    • Enter 4242 4242 4242 4242 as the card number
    • Enter any future date for card expiry
    • Enter any 3-digit number for CVV
    • Enter any billing postal code (90210)
  • Click the Pay button

You should see:

  • A print statement from your server’s event logs with the checkout.session.completed event

Now that you’ve verified event delivery, you can add a bit of security to make sure that events are only coming from Payske.

Verify events came from Payske

Anyone can POST data to your event handler. Before processing an event, always verify that it came from Payske before trusting it. The official Payske library has built-in support for verifying webhook events, which you’ll update your event handler with:

ruby
# Set your secret key. Remember to switch to your live secret key in production.
# See your keys here: https://account.payske.com/api/key
Payske.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'

require 'sinatra'

# You can find your endpoint's secret in the output of the `payske listen`
# command you ran earlier
endpoint_secret = 'whsec_...'

post '/webhook' do
  event = nil

  # Verify webhook signature and extract the event
  # See https://payske.com/docs/webhooks/signatures for more information.
  begin
    sig_header = request.env['HTTP_PAYSKE_SIGNATURE']
    payload = request.body.read
    event = Payske::Webhook.construct_event(payload, sig_header, endpoint_secret)
  rescue JSON::ParserError => e
    # Invalid payload
    return status 400
  rescue Payske::SignatureVerificationError => e
    # Invalid signature
    return status 400
  end

  # Print out the event so you can look at it
  puts event.inspect

  status 200
end
python
# Set your secret key. Remember to switch to your live secret key in production.
# See your keys here: https://account.payske.com/api/key
payske.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'

# Using Django
from django.http import HttpResponse

# You can find your endpoint's secret in your webhook settings
endpoint_secret = 'whsec_...'

@csrf_exempt
def my_webhook_view(request):
  payload = request.body
  sig_header = request.META['HTTP_PAYSKE_SIGNATURE']
  event = None

  try:
    event = payske.Webhook.construct_event(
      payload, sig_header, endpoint_secret
    )
  except ValueError as e:
    # Invalid payload
    return HttpResponse(status=400)
  except payske.error.SignatureVerificationError as e:
    # Invalid signature
    return HttpResponse(status=400)

  # Passed signature verification
  return HttpResponse(status=200)
php
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
\Payske\Payske::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc');

// You can find your endpoint's secret in your webhook settings
$endpoint_secret = 'whsec_...';

$payload = @file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_PAYSKE_SIGNATURE'];
$event = null;

try {
  $event = \Payske\Webhook::constructEvent(
    $payload, $sig_header, $endpoint_secret
  );
} catch(\UnexpectedValueException $e) {
  // Invalid payload
  http_response_code(400);
  exit();
} catch(\Payske\Exception\SignatureVerificationException $e) {
  // Invalid signature
  http_response_code(400);
  exit();
}

error_log("Passed signature verification!");
http_response_code(200);
java
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
Payske.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc";

// You can find your endpoint's secret in your webhook settings
String endpointSecret = "whsec_...";

// Using the Spark framework (http://sparkjava.com)
public Object handle(Request request, Response response) {
  String payload = request.body();
  String sigHeader = request.headers("Payske-Signature");
  Event event = null;

  try {
    event = Webhook.constructEvent(payload, sigHeader, endpointSecret);
  } catch (JsonSyntaxException e) {
    // Invalid payload
    response.status(400);
    return "";
  } catch (SignatureVerificationException e) {
    // Invalid signature
    response.status(400);
    return "";
  }

  response.status(200);
  return "";
}
typescript
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
const payske = require('payske')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');

// Find your endpoint's secret in your Dashboard's webhook settings
const endpointSecret = 'whsec_...';

// Using Express
const app = require('express')();

// Use body-parser to retrieve the raw body as a buffer
const bodyParser = require('body-parser');

app.post('/webhook', bodyParser.raw({type: 'application/json'}), (request, response) => {
  const payload = request.body;
  const sig = request.headers['payske-signature'];

  let event;

  try {
    event = payske.webhooks.constructEvent(payload, sig, endpointSecret);
  } catch (err) {
    return response.status(400).send(`Webhook Error: ${err.message}`);
  }

  response.status(200).end();
});

app.listen(4242, () => console.log('Running on port 4242'));
go
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
payske.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"

http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) {
  const MaxBodyBytes = int64(65536)
  req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes)

  body, err := ioutil.ReadAll(req.Body)
  if err != nil {
    fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err)
    w.WriteHeader(http.StatusServiceUnavailable)
    return
  }

  // Pass the request body and Payske-Signature header to ConstructEvent, along with the webhook signing key
  // You can find your endpoint's secret in your webhook settings
  endpointSecret := "whsec_...";
  event, err := webhook.ConstructEvent(body, req.Header.Get("Payske-Signature"), endpointSecret)

  if err != nil {
    fmt.Fprintf(os.Stderr, "Error verifying webhook signature: %v\n", err)
    w.WriteHeader(http.StatusBadRequest) // Return a 400 error on a bad signature
    return
  }

  w.WriteHeader(http.StatusOK)
})
csharp
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
PayskeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc";

using System;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using Payske;

namespace workspace.Controllers
{
  [Route("api/[controller]")]
  public class PayskeWebHook : Controller
  {
    // You can find your endpoint's secret in your webhook settings
    const string secret = "whsec_...";

    [HttpPost]
    public async Task<IActionResult> Index()
    {
      var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();

      try
      {
        var payskeEvent = EventUtility.ConstructEvent(
          json,
          Request.Headers["Payske-Signature"],
          secret
        );

        return Ok();
      }
      catch (PayskeException e)
      {
        return BadRequest();
      }
    }
  }
}

Testing

Go through the testing flow from the previous step. You should still see the checkout.session.completed event being printed out successfully.

Next, try hitting the endpoint with an unsigned request:

Command Line

console
curl -X POST \
  -H "Content-Type: application/json" \
  --data '{ "fake": "unsigned request" }' \
  -is http://localhost:4242/webhook


HTTP/1.1 400 Bad Request
... more headers

You should get a 400 Bad Request error, because you tried to send an unsigned request to your endpoint.

Now that the basics of the event handler are set up, you can move on to fulfilling the order.

  1. Fulfill the order Server-side

To fulfill the order, you’ll need to handle the checkout.session.completed event. Depending on which payment methods you accept (for example, cards, mobile wallets), you’ll also optionally handle a few extra events after this basic step.

Handle the checkout.session.completed event

Now that you have the basic structure and security in place to make sure any event you process came from Payske, you can handle the checkout.session.completed event. This event includes the Checkout Session object, which contains details about your customer and their payment.

When handling this event, you might also consider:

  • Saving a copy of the order in your own database.
  • Sending the customer a receipt email.
  • Reconciling the line items and quantity purchased by the customer if using line_item.adjustable_quantity. If the Checkout Session has many line items you can paginate through them with the line_items.

Add code to your event handler to fulfill the order:

ruby
# Set your secret key. Remember to switch to your live secret key in production.
# See your keys here: https://account.payske.com/api/key
Payske.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'

require 'sinatra'

# You can find your endpoint's secret in the output of the `payske listen`
# command you ran earlier
endpoint_secret = 'whsec_...'

post '/webhook' do
  event = nil

  # Verify webhook signature and extract the event
  # See https://payske.com/docs/webhooks/signatures for more information.
  begin
    sig_header = request.env['HTTP_PAYSKE_SIGNATURE']
    payload = request.body.read
    event = Payske::Webhook.construct_event(payload, sig_header, endpoint_secret)
  rescue JSON::ParserError => e
    # Invalid payload
    return status 400
  rescue Payske::SignatureVerificationError => e
    # Invalid signature
    return status 400
  end

  if event['type'] == 'checkout.session.completed'
    # Retrieve the session. If you require line items in the response, you may include them by expanding line_items.
    session = Payske::Checkout::Session.retrieve({
      id: event['data']['object']['id'],
      expand: ['line_items'],
    })

    line_items = session.line_items
    fulfill_order(line_items)
  end

  status 200
end

def fulfill_order(line_items)
  # TODO: fill in with your own logic
  puts "Fulfilling order for #{line_items.inspect}"
end
python
# Set your secret key. Remember to switch to your live secret key in production.
# See your keys here: https://account.payske.com/api/key
payske.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'

# Using Django
from django.http import HttpResponse

# You can find your endpoint's secret in your webhook settings
endpoint_secret = 'whsec_...'

@csrf_exempt
def my_webhook_view(request):
  payload = request.body
  sig_header = request.META['HTTP_PAYSKE_SIGNATURE']
  event = None

  try:
    event = payske.Webhook.construct_event(
      payload, sig_header, endpoint_secret
    )
  except ValueError as e:
    # Invalid payload
    return HttpResponse(status=400)
  except payske.error.SignatureVerificationError as e:
    # Invalid signature
    return HttpResponse(status=400)

  # Handle the checkout.session.completed event
  if event['type'] == 'checkout.session.completed':
    # Retrieve the session. If you require line items in the response, you may include them by expanding line_items.
    session = payske.checkout.Session.retrieve(
      event['data']['object']['id'],
      expand=['line_items'],
    )

    line_items = session.line_items
    # Fulfill the purchase...
    fulfill_order(line_items)

  # Passed signature verification
  return HttpResponse(status=200)

def fulfill_order(line_items):
  # TODO: fill me in
  print("Fulfilling order")
php
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
\Payske\Payske::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc');

// You can find your endpoint's secret in your webhook settings
$endpoint_secret = 'whsec_...';

$payload = @file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_PAYSKE_SIGNATURE'];
$event = null;

try {
  $event = \Payske\Webhook::constructEvent(
    $payload, $sig_header, $endpoint_secret
  );
} catch(\UnexpectedValueException $e) {
  // Invalid payload
  http_response_code(400);
  exit();
} catch(\Payske\Exception\SignatureVerificationException $e) {
  // Invalid signature
  http_response_code(400);
  exit();
}

function fulfill_order($line_items) {
  // TODO: fill me in
  error_log("Fulfilling order...");
  error_log($line_items);
}

// Handle the checkout.session.completed event
if ($event->type == 'checkout.session.completed') {
  // Retrieve the session. If you require line items in the response, you may include them by expanding line_items.
  $session = \Payske\Checkout\Session::retrieve([
    'id' => $event->data->object->id,
    'expand' => ['line_items'],
  ]);

  $line_items = $session->line_items;
  // Fulfill the purchase...
  fulfill_order($line_items);
}

http_response_code(200);
java
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
Payske.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc";

// You can find your endpoint's secret in your webhook settings
String endpointSecret = "whsec_...";

public void fulfillOrder(LineItemCollection lineItems) {
  // TODO: fill me in
  System.out.println("Fulfilling order...");
}

// Using the Spark framework (http://sparkjava.com)
public Object handle(Request request, Response response) {
  String payload = request.body();
  String sigHeader = request.headers("Payske-Signature");
  Event event = null;

  try {
    event = Webhook.constructEvent(payload, sigHeader, endpointSecret);
  } catch (JsonSyntaxException e) {
    // Invalid payload
    response.status(400);
    return "";
  } catch (SignatureVerificationException e) {
    // Invalid signature
    response.status(400);
    return "";
  }

  // Handle the checkout.session.completed event
  if ("checkout.session.completed".equals(event.getType())) {
    Session session = (Session) event.getDataObjectDeserializer().getObject();
    SessionRetrieveParams params =
      SessionRetrieveParams.builder()
        .addExpand("line_items")
        .build();

    Session session = Session.retrieve(session.getId(), params, null);

    SessionListLineItemsParams listLineItemsParams =
      SessionListLineItemsParams.builder()
        .build();

    // Retrieve the session. If you require line items in the response, you may include them by expanding line_items.
    LineItemCollection lineItems = session.listLineItems(listLineItemsParams);
    // Fulfill the purchase...
    fulfillOrder(lineItems);
  }

  response.status(200);
  return "";
}
typescript
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
const payske = require('payske')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');

// Find your endpoint's secret in your Dashboard's webhook settings
const endpointSecret = 'whsec_...';

// Using Express
const app = require('express')();

// Use body-parser to retrieve the raw body as a buffer
const bodyParser = require('body-parser');

const fulfillOrder = (lineItems) => {
  // TODO: fill me in
  console.log("Fulfilling order", lineItems);
}

app.post('/webhook', bodyParser.raw({type: 'application/json'}), async (request, response) => {
  const payload = request.body;
  const sig = request.headers['payske-signature'];

  let event;

  try {
    event = payske.webhooks.constructEvent(payload, sig, endpointSecret);
  } catch (err) {
    return response.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle the checkout.session.completed event
  if (event.type === 'checkout.session.completed') {
    // Retrieve the session. If you require line items in the response, you may include them by expanding line_items.
    const sessionWithLineItems = await payske.checkout.sessions.retrieve(
      session.id,
      {
        expand: ['line_items'],
      }
    );
    const lineItems = session.line_items;

    // Fulfill the purchase...
    fulfillOrder(lineItems);
  }

  response.status(200).end();
});

app.listen(4242, () => console.log('Running on port 4242'));
go
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
payske.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"

func FulfillOrder(LineItemList lineItems) {
  // TODO: fill me in
}

http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) {
  const MaxBodyBytes = int64(65536)
  req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes)

  body, err := ioutil.ReadAll(req.Body)
  if err != nil {
    fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err)
    w.WriteHeader(http.StatusServiceUnavailable)
    return
  }

  // Pass the request body and Payske-Signature header to ConstructEvent, along with the webhook signing key
  // You can find your endpoint's secret in your webhook settings
  endpointSecret := "whsec_...";
  event, err := webhook.ConstructEvent(body, req.Header.Get("Payske-Signature"), endpointSecret)

  if err != nil {
    fmt.Fprintf(os.Stderr, "Error verifying webhook signature: %v\n", err)
    w.WriteHeader(http.StatusBadRequest) // Return a 400 error on a bad signature
    return
  }

  // Handle the checkout.session.completed event
  if event.Type == "checkout.session.completed" {
    var session payske.CheckoutSession
    err := json.Unmarshal(event.Data.Raw, &session)
    if err != nil {
      fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
      w.WriteHeader(http.StatusBadRequest)
      return
    }

    params := &payske.CheckoutSessionParams{}
    params.AddExpand("line_items")

    // Retrieve the session. If you require line items in the response, you may include them by expanding line_items.
    sessionWithLineItems, _ := session.Get(session.ID, params)
    lineItems := sessionWithLineItems.LineItems
    // Fulfill the purchase...
    FulfillOrder(lineItems)
  }

  w.WriteHeader(http.StatusOK)
})
csharp
// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://account.payske.com/api/key
PayskeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc";

using System;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using Payske;

namespace workspace.Controllers
{
  [Route("api/[controller]")]
  public class PayskeWebHook : Controller
  {
    // You can find your endpoint's secret in your webhook settings
    const string secret = "whsec_...";

    [HttpPost]
    public async Task<IActionResult> Index()
    {
      var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();

      try
      {
        var payskeEvent = EventUtility.ConstructEvent(
          json,
          Request.Headers["Payske-Signature"],
          secret
        );

        // Handle the checkout.session.completed event
        if (payskeEvent.Type == Events.CheckoutSessionCompleted)
        {
          var session = payskeEvent.Data.Object as Checkout.Session;
          var options = new SessionGetOptions();
          options.AddExpand("line_items");

          var service = new SessionService();
          // Retrieve the session. If you require line items in the response, you may include them by expanding line_items.
          Session sessionWithLineItems = service.Get(session.Id, options);
          PayskeList<LineItem> lineItems = sessionWithLineItems.LineItems;

          // Fulfill the purchase...
          this.FulfillOrder(lineItems);
        }

        return Ok();
      }
      catch (PayskeException e)
      {
        return BadRequest();
      }
    }

    private void FulfillOrder(PayskeList<LineItem> lineItems) {
      // TODO: fill me in
    }
  }
}

Your webhook endpoint redirects your customer to the success_url when you acknowledge you received the event. In scenarios where your endpoint is down or the event isn’t acknowledged properly, your handler redirects the customer to the success_url 10 seconds after a successful payment.

Testing

Your event handler should receive a checkout.session.completed event, and you should have successfully handled it.