Skip to content

Payments

The Payments API handles payment processing for orders, with support for Stripe integration.

Payment Flow Overview

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Client    │     │ KitchenClick│     │   Stripe    │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │                   │                   │
       │ POST /payment/    │                   │
       │ stripe/create-    │                   │
       │ intent            │                   │
       │──────────────────>│                   │
       │                   │   Create Intent   │
       │                   │──────────────────>│
       │                   │   clientSecret    │
       │                   │<──────────────────│
       │   clientSecret    │                   │
       │<──────────────────│                   │
       │                   │                   │
       │ Stripe.js confirm │                   │
       │─────────────────────────────────────>│
       │           Payment result              │
       │<─────────────────────────────────────│
       │                   │                   │
       │ POST /orders/     │                   │
       │ stripe            │                   │
       │──────────────────>│                   │
       │   Order created   │                   │
       │<──────────────────│                   │

Get Stripe Config

Retrieve the Stripe publishable key and optional connected account ID needed to initialize Stripe.js on the client.

GET /v1/ecommerce/payment/stripe/config

Authentication: OAuth Token

Scope: kitchenclick:payments.read

Rate Limit: 200/min per client

Query Parameters

ParameterTypeRequiredDescription
location_organization_idstringNoLocation identifier to get the connected Stripe account for that location

Response

json
{
  "status": "success",
  "data": {
    "publishable_key": "pk_live_xxxxx",
    "connected_account_id": "acct_xxxxx"
  }
}

Response Fields

FieldTypeDescription
publishable_keystringStripe publishable key for initializing Stripe.js
connected_account_idstringStripe Connect account ID for the location (nullable)

Notes

  • Call this before creating payment intents to get the correct Stripe publishable key
  • The connected_account_id is returned when a location has its own Stripe Connect account
  • Use the publishable key with loadStripe() on the client side

Create Stripe Payment Intent

Create a Stripe PaymentIntent for processing card payments.

POST /v1/ecommerce/payment/stripe/create-intent

Authentication: OAuth Token

Scope: kitchenclick:payments.create

Rate Limit: 30/min per client

The intent is created from a saved quote — the charge amount is resolved server-side from the quote, so you never pass an amount.

Request Body

json
{
  "quote_token_id": "kqt_00000a1B2c3D4e5",
  "payment_plan": "full"
}

Request Fields

FieldTypeRequiredDescription
quote_token_idstringYesA saved quote token from Calculate Order (call it with save_quote: true)
payment_planstringNofull (default) or deposit. deposit is only valid for catering quotes the server marked deposit-eligible.

Response

json
{
  "status": "success",
  "data": {
    "client_secret": "pi_3abc123_secret_xyz789",
    "payment_intent_id": "pi_3abc123",
    "publishable_key": "pk_live_xxxxx",
    "amount": 3685
  }
}
FieldTypeDescription
client_secretstringConfirm the payment client-side with Stripe.js
payment_intent_idstringPass to Create Order with Stripe Payment
publishable_keystringStripe publishable key for this account/location
amountintegerCharge amount in cents, resolved server-side (full total or catering deposit)

Create Order with Stripe Payment

Submit an order after successful Stripe payment.

POST /v1/ecommerce/orders/stripe

Authentication: OAuth Token

Scope: kitchenclick:orders.create

Rate Limit: 30/min per client

The order is built from the quote (items, totals, location). You supply the customer and the verified payment intent.

Request Body

json
{
  "quote_token_id": "kqt_00000a1B2c3D4e5",
  "customer": {
    "name": "John Doe",
    "phone": "(555) 123-4567",
    "email": "john@example.com",
    "notify_on_ready": true
  },
  "payment": {
    "method": "stripe",
    "payment_intent_id": "pi_3abc123"
  },
  "special_instructions": "No onions",
  "concept_id": "con_00000a1B2c3D4e5"
}

Request Fields

FieldTypeRequiredDescription
quote_token_idstringYesThe quote the PaymentIntent was created for
customerobjectYes{ name (req), phone (req), email, notify_on_ready }
paymentobjectYesPayment details
payment.methodstringYesstripe, terminal, cash, or pay_later
payment.payment_intent_idstringCond.Required when payment.method is stripe
special_instructionsstringNoOrder-level note
concept_idstringNoRequired for catering orders

Server-side verification

The PaymentIntent must have status succeeded, must be the intent created for this exact quote, and must not have been used before. Its amount must match the quote. KitchenClick re-verifies all of this and rejects mismatches. pay_later is only accepted when the location has pay-in-store enabled and the order is not delivery.

Response

Returns the same object as Create Order (order_id, order_number, status, totals, tracking_url). For catering orders the response additionally includes a payment object (payment_status, amount_paid, balance_due), the store timezone, and an invoice object (invoice_id, invoice_number, public_token, status, total, amount_paid, balance_due) for collecting the remaining balance — see Invoices.


Payment Presentment (Terminal/Kiosk)

For payment terminals or kiosks that handle payment externally.

Create Presentment

POST /v1/ecommerce/payment/presentment

Authentication: OAuth Token

Scope: kitchenclick:payments.create

Rate Limit: 30/min per client

Request Body

json
{
  "quote_token_id": "kqt_00000a1B2c3D4e5"
}
FieldTypeRequiredDescription
quote_token_idstringYesA saved quote token from Calculate Order

Response

json
{
  "status": "success",
  "data": {
    "presentment_token": "pres_00000x1Y2z3A4b5",
    "amount": 3420,
    "currency": "usd",
    "expires_at": "2024-01-15T15:00:00Z",
    "status": "pending"
  }
}

Check Presentment Status

GET /v1/ecommerce/payment/presentment/{presentmentToken}/status

Authentication: OAuth Token

Scope: kitchenclick:payments.read

Rate Limit: 200/min per client

Response

json
{
  "status": "success",
  "data": {
    "presentment_token": "pres_00000x1Y2z3A4b5",
    "status": "completed",
    "amount": 3420,
    "payment_method": "credit_card",
    "card_brand": "visa",
    "card_last4": "4242",
    "completed_at": "2024-01-15T14:45:30Z",
    "order_hashkey": "ord_00000x1Y2z3A4b5"
  }
}

Presentment Statuses

StatusDescription
pendingAwaiting payment
processingPayment in progress
completedPayment successful, order created
failedPayment failed
expiredPresentment expired (5 min timeout)
cancelledCancelled by user

Simulate Payment (Non-Production)

For testing in development/staging environments.

POST /v1/ecommerce/payment/presentment/{presentmentToken}/simulate

Authentication: OAuth Token

Scope: kitchenclick:payments.create

Environments: Development, Staging only

Request Body

json
{
  "approve": true
}
FieldTypeRequiredDescription
approvebooleanYestrue to simulate an approved payment, false to simulate a decline

Error Handling

Payment Failed

json
{
  "status": "error",
  "message": "Payment failed",
  "errors": {
    "payment": ["Card was declined. Please try a different payment method."]
  },
  "error_code": "card_declined"
}

Common Error Codes

CodeDescription
card_declinedCard was declined
insufficient_fundsInsufficient funds
expired_cardCard has expired
invalid_cardInvalid card number
processing_errorPayment processor error
presentment_expiredPayment presentment expired

Client-Side Integration

Stripe.js Example

javascript
import { loadStripe } from '@stripe/stripe-js';

const stripePromise = loadStripe('pk_live_xxxxx');

async function processPayment(quoteTokenId) {
  const token = await tokenManager.getToken();

  // 1. Create PaymentIntent from the saved quote
  const response = await fetch('/api/v1/ecommerce/payment/stripe/create-intent', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      quote_token_id: quoteTokenId, // from POST /orders/calculate (save_quote: true)
      payment_plan: 'full',
    }),
  });

  const { data } = await response.json();
  const stripe = await stripePromise;

  // 2. Confirm payment with Stripe.js
  const { error, paymentIntent } = await stripe.confirmCardPayment(
    data.client_secret,
    {
      payment_method: {
        card: cardElement, // From Stripe Elements
        billing_details: {
          name: 'John Doe',
        },
      },
    }
  );

  if (error) {
    throw new Error(error.message);
  }

  // 3. Create order with successful payment
  const orderResponse = await fetch('/api/v1/ecommerce/orders/stripe', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      quote_token_id: quoteTokenId,
      customer: { name: 'John Doe', phone: '555-123-4567' },
      payment: { method: 'stripe', payment_intent_id: paymentIntent.id },
    }),
  });

  return orderResponse.json();
}

React Native Example

javascript
import { useStripe } from '@stripe/stripe-react-native';

function PaymentScreen({ cart, location }) {
  const { confirmPayment } = useStripe();

  const handlePayment = async () => {
    const token = await getToken();

    // Create PaymentIntent
    const response = await fetch('/api/v1/ecommerce/payment/stripe/create-intent', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        quote_token_id: cart.quoteTokenId, // from POST /orders/calculate (save_quote: true)
        payment_plan: 'full',
      }),
    });

    const { data } = await response.json();

    // Confirm with Stripe SDK
    const { error, paymentIntent } = await confirmPayment(data.client_secret, {
      paymentMethodType: 'Card',
    });

    if (error) {
      Alert.alert('Payment Failed', error.message);
      return;
    }

    // Submit order
    const order = await submitOrder(paymentIntent.id, cart, location);
    navigation.navigate('OrderConfirmation', { order });
  };

  return (
    <Button title="Pay Now" onPress={handlePayment} />
  );
}

Security Considerations

  1. Never log full card numbers - Only store last 4 digits
  2. Use HTTPS only - All payment requests must be encrypted
  3. Validate amounts server-side - Don't trust client-submitted totals
  4. Implement idempotency - Use payment intent IDs to prevent duplicate charges
  5. Handle webhooks - Set up Stripe webhooks for payment status updates

Changelog
DateChange
2026-06-17Corrected the Stripe Payment Intent and Create-Order-with-Stripe-Payment request shapes to the real quote-based flow (quote_token_id + payment_plan; payment.method/payment.payment_intent_id); documented the server-side amount/intent verification; fixed presentment and simulate request bodies.
2026-03-14Added e-commerce API endpoints.
2026-01-15Initial publication.

ShopHero CommerceCore Platform