Skip to content
On this page

Make line item quantities adjustable

Configure the Checkout Session so customers can adjust line item quantity during checkout.

Create a Checkout Session with adjustable_quantity enabled

Set adjustable_quantity on your line_items when creating a Checkout Session to enable your customers to update the quantity of an item during checkout.

You can customize the default settings for the minimum and maximum quantities allowed by setting adjustable_quantity.minimum and adjustable_quantity.maximum. By default, an item’s minimum adjustable quantity is 0 and the maximum adjustable quantity is 99. You can specify a value of up to 999999 for adjustable_quantity.maximum.

When using adjustable quantities with a line_items[].quantity value greater than 99 (the default adjustable maximum), set adjustable_quantity.maximum to be greater than or equal to that item’s quantity.

If you use adjustable quantities, change your configuration so that it uses adjustable_quantity.maximum when creating the Checkout Session to reserve inventory quantity instead of the line_items quantity.

Checkout prevents the customer from removing an item if it is the only item remaining.

console
curl https://api.payske.com/v1/checkout/sessions \
  -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
  -d "line_items[0][price_data][currency]"=usd \
  -d "line_items[0][price_data][product_data][name]"=T-shirt \
  -d "line_items[0][price_data][unit_amount]"=2000 \
  -d "line_items[0][price_data][tax_behavior]"=exclusive \
  -d "line_items[0][adjustable_quantity][enabled]"=true \
  -d "line_items[0][adjustable_quantity][minimum]"=1 \
  -d "line_items[0][adjustable_quantity][maximum]"=10 \
  -d "line_items[0][quantity]"=1 \
  -d "automatic_tax[enabled]"=true \
  -d mode=payment \
  -d success_url="https://example.com/success" \
  -d cancel_url="https://example.com/cancel"
console
payske checkout sessions create  \
  -d "line_items[0][price_data][currency]"=usd \
  -d "line_items[0][price_data][product_data][name]"="T-shirt" \
  -d "line_items[0][price_data][unit_amount]"=2000 \
  -d "line_items[0][price_data][tax_behavior]"=exclusive \
  -d "line_items[0][adjustable_quantity][enabled]"=true \
  -d "line_items[0][adjustable_quantity][minimum]"=1 \
  -d "line_items[0][adjustable_quantity][maximum]"=10 \
  -d "line_items[0][quantity]"=1 \
  -d "automatic_tax[enabled]"=true \
  --mode=payment \
  --success-url="https://example.com/success" \
  --cancel-url="https://example.com/cancel"
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'

Payske::Checkout::Session.create(
  {
    line_items: [
      {
        price_data: {
          currency: 'usd',
          product_data: {name: 'T-shirt'},
          unit_amount: 2000,
          tax_behavior: 'exclusive',
        },
        adjustable_quantity: {enabled: true, minimum: 1, maximum: 10},
        quantity: 1,
      },
    ],
    automatic_tax: {enabled: true},
    mode: 'payment',
    success_url: 'https://example.com/success',
    cancel_url: 'https://example.com/cancel',
  },
)
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
import payske
payske.api_key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"

payske.checkout.Session.create(
  line_items=[
    {
      "price_data": {
        "currency": "usd",
        "product_data": {"name": "T-shirt"},
        "unit_amount": 2000,
        "tax_behavior": "exclusive",
      },
      "adjustable_quantity": {"enabled": True, "minimum": 1, "maximum": 10},
      "quantity": 1,
    },
  ],
  automatic_tax={"enabled": True},
  mode="payment",
  success_url="https://example.com/success",
  cancel_url="https://example.com/cancel",
)
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 = new \Payske\PayskeClient('sk_test_4eC39HqLyjWDarjtT1zdp7dc');

$payske->checkout->sessions->create(
  [
    'line_items' => [
      [
        'price_data' => [
          'currency' => 'usd',
          'product_data' => ['name' => 'T-shirt'],
          'unit_amount' => 2000,
          'tax_behavior' => 'exclusive',
        ],
        'adjustable_quantity' => ['enabled' => true, 'minimum' => 1, 'maximum' => 10],
        'quantity' => 1,
      ],
    ],
    'automatic_tax' => ['enabled' => true],
    'mode' => 'payment',
    'success_url' => 'https://example.com/success',
    'cancel_url' => 'https://example.com/cancel',
  ]
);
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";

SessionCreateParams params =
  SessionCreateParams
    .builder()
    .addLineItem(
      SessionCreateParams.LineItem
        .builder()
        .setPriceData(
          SessionCreateParams.LineItem.PriceData
            .builder()
            .setCurrency("usd")
            .setProductData(
              SessionCreateParams.LineItem.PriceData.ProductData
                .builder()
                .setName("T-shirt")
                .build()
            )
            .setUnitAmount(2000L)
            .setTaxBehavior(SessionCreateParams.LineItem.PriceData.TaxBehavior.EXCLUSIVE)
            .build()
        )
        .setAdjustableQuantity(
          SessionCreateParams.LineItem.AdjustableQuantity
            .builder()
            .setEnabled(true)
            .setMinimum(1L)
            .setMaximum(10L)
            .build()
        )
        .setQuantity(1L)
        .build()
    )
    .setAutomaticTax(SessionCreateParams.AutomaticTax.builder().setEnabled(true).build())
    .setMode(SessionCreateParams.Mode.PAYMENT)
    .setSuccessUrl("https://example.com/success")
    .setCancelUrl("https://example.com/cancel")
    .build();

Session session = Session.create(params);
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');

const session = await payske.checkout.sessions.create({
  line_items: [
    {
      price_data: {
        currency: 'usd',
        product_data: {name: 'T-shirt'},
        unit_amount: 2000,
        tax_behavior: 'exclusive',
      },
      adjustable_quantity: {enabled: true, minimum: 1, maximum: 10},
      quantity: 1,
    },
  ],
  automatic_tax: {enabled: true},
  mode: 'payment',
  success_url: 'https://example.com/success',
  cancel_url: 'https://example.com/cancel',
});
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"

params := &payske.CheckoutSessionParams{
  LineItems: []*payske.CheckoutSessionLineItemParams{
    &payske.CheckoutSessionLineItemParams{
      PriceData: &payske.CheckoutSessionLineItemPriceDataParams{
        Currency: payske.String(string(payske.CurrencyUSD)),
        ProductData: &payske.CheckoutSessionLineItemPriceDataProductDataParams{
          Name: payske.String("T-shirt"),
        },
        UnitAmount: payske.Int64(2000),
        TaxBehavior: payske.String("exclusive"),
      },
      AdjustableQuantity: &payske.CheckoutSessionLineItemAdjustableQuantityParams{
        Enabled: payske.Bool(true),
        Minimum: payske.Int64(1),
        Maximum: payske.Int64(10),
      },
      Quantity: payske.Int64(1),
    },
  },
  AutomaticTax: &payske.CheckoutSessionAutomaticTaxParams{Enabled: payske.Bool(true)},
  Mode: payske.String(string(payske.CheckoutSessionModePayment)),
  SuccessURL: payske.String("https://example.com/success"),
  CancelURL: payske.String("https://example.com/cancel"),
};
result, _ := session.New(params);
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";

var options = new SessionCreateOptions
{
    LineItems = new List<SessionLineItemOptions>
    {
        new SessionLineItemOptions
        {
            PriceData = new SessionLineItemPriceDataOptions
            {
                Currency = "usd",
                ProductData = new SessionLineItemPriceDataProductDataOptions
                {
                    Name = "T-shirt",
                },
                UnitAmount = 2000,
                TaxBehavior = "exclusive",
            },
            AdjustableQuantity = new SessionLineItemAdjustableQuantityOptions
            {
                Enabled = true,
                Minimum = 1,
                Maximum = 10,
            },
            Quantity = 1,
        },
    },
    AutomaticTax = new SessionAutomaticTaxOptions { Enabled = true },
    Mode = "payment",
    SuccessUrl = "https://example.com/success",
    CancelUrl = "https://example.com/cancel",
};
var service = new SessionService();
service.Create(options);

Handling completed transactions

After the payment completes, you can make a request for the finalized line items and their quantities. If your customer removes a line item, it is also removed from the line items response. See the Fulfillment guide to learn how to create an event handler to handle completed Checkout Sessions.

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 your webhook settings
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'
    checkout_session = event['data']['object']

    line_items = Payske::Checkout::Session.list_line_items(checkout_session['id'], {limit: 100})

    # Fulfill the purchase...
    begin
      fulfill_order(checkout_session, line_items)
    rescue NotImplementedError => e
      return status 400
    end
  end

  status 200
end

def fulfill_order(checkout_session, line_items)
  # TODO: Remove error and implement...
  raise NotImplementedError.new(<<~MSG)
    Given the Checkout Session "#{checkout_session.id}" load your internal order from the database here.
    Then you can reconcile your order's quantities with the final line item quantity purchased. You can use `checkout_session.metadata` and `price.metadata` to store and later reference your internal order and item ids.
  MSG
end
python
import payske

# Using Django
from django.http import HttpResponse

# 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'

# 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':
    session = event['data']['object']

    line_items = payske.checkout.Session.list_line_items(session['id'], limit=100)

    # Fulfill the purchase...
    try:
      fulfill_order(session, line_items)
    except NotImplementedError as e:
      return HttpResponse(status=400)

  # Passed signature verification
  return HttpResponse(status=200)

def fulfill_order(session, line_items):
  # TODO: Remove error and implement...
  raise NotImplementedError("Given the Checkout Session \"" + session['id'] + "\", load your internal order from the database here.\nThen you can reconcile your order's quantities with the final line item quantity purchased.\nYou can use `checkout_session.metadata` and `price.metadata` to store and later reference your internal order and item ids.")
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($session, $line_items) {
  // TODO: Remove error and implement...
  throw new Exception("given the Checkout Session $session->id, load your internal order from the database here.\nThen you can reconcile your order's quantities with the final line item quantity purchased.\nYou can use `checkout_session.metadata` and `price.metadata` to store and later reference your internal order and item ids.")
}

// Handle the checkout.session.completed event
if ($event->type == 'checkout.session.completed') {
  $session = $event->data->object;
  $line_items = \Payske\Checkout\Session::allLineItems($session->id, ['limit' => 100]);
  // Fulfill the purchase...
  try {
    fulfill_order($session, $line_items);
  } catch(\Exception $e) {
    http_response_code(400);
    exit();
  }
}

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(Session session, LineItemCollection lineItems) {
  // TODO: Remove error and implement...
  String error = String.format("Given the Checkout Session \"%s\" load your internal order from the database here.\nThen you can reconcile your order's quantities with the final line item quantity purchased. You can use `checkout_session.metadata` and `price.metadata` to store and later reference your internal order and item ids.", session.getId());
  throw new UnsupportedOperationException(error);
}

// 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();
    Map<String, Object> params = new HashMap<>();
    params.put("limit", 100);
    LineItemCollection lineItems = session.listLineItems(params);

    // Fulfill the purchase...
    try {
      fulfillOrder(session, lineItems);
    } catch (NotImplementedException e) {
      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');

const fulfillOrder = (session, lineItems) => {
  // TODO: Remove error and implement...
  throw new Error(`
    Given the Checkout Session ${session.id}, load your internal order from the database here.
    Then you can reconcile your order's quantities with the final line item quantity purchased. You can use \`checkout_session.metadata\` and \`price.metadata\` to store and later reference your internal order and item ids.`
  );
}

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}`);
  }

  // Handle the checkout.session.completed event
  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;

    payske.checkout.sessions.listLineItems(
      session.id,
      { limit: 100 },
      function(err, lineItems) {
        // Fulfill the purchase...
        try {
          fulfillOrder(session, lineItems);
        } catch (err) {
          return response.status(400).send(`Fulfillment 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"

func fulfillOrder(session payske.CheckoutSession) error {
  params := &payske.CheckoutSessionListLineItemParams{
    ID: payske.String(session.Id),
  }
  // Grab the line items for the session
  params.Filters.AddFilter("limit", "", "100")
  i := session.ListLineItems(params)
  for i.Next() {
    // Access the line item using i.LineItem()
  }

  // TODO: Remove error and implement...
  return fmt.Errorf("given the Checkout Session %q, load your internal order from the database here.\nThen you can reconcile your order's quantities with the final line item quantity purchased.\nYou can use `checkout_session.metadata` and `price.metadata` to store and later reference your internal order and item ids.", session.Id)
}

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
    }

    // Fulfill the purchase...
    if err := fulfillOrder(session); err != nil {
      fmt.Fprintf(os.Stderr, "Error fulfilling order: %v\n", err)
      w.WriteHeader(http.StatusBadRequest)
      return
    }
  }

  w.WriteHeader(http.StatusOK)
})
csharp
using System;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using Payske;

// 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";

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;

          // Fulfill the purchase...
          try
          {
            this.FulfillOrder(session);
          }
          catch (NotImplementedException ex)
          {
            return BadRequest();
          }
        }

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

    private void FulfillOrder(Checkout.Session session) {
      var options = new SessionListLineItemsOptions
      {
        Limit = 100,
      };
      var service = new SessionService();
      PayskeList<LineItem> lineItems = service.ListLineItems(session.Id, options);

      // TODO: Remove error and implement...
      throw new NotImplementedException($"Given the Checkout Session \"{session.Id}\" load your internal order from the database here.\n Then you can reconcile your order's quantities with the final line item quantity purchased. You can use `checkout_session.metadata` and `price.metadata` to store and later reference your internal order and item ids.");
    }
  }
}