Appearance
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/configAuthentication: OAuth Token
Scope: kitchenclick:payments.read
Rate Limit: 200/min per client
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
location_organization_id | string | No | Location 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
| Field | Type | Description |
|---|---|---|
publishable_key | string | Stripe publishable key for initializing Stripe.js |
connected_account_id | string | Stripe Connect account ID for the location (nullable) |
Notes
- Call this before creating payment intents to get the correct Stripe publishable key
- The
connected_account_idis 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-intentAuthentication: 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
| Field | Type | Required | Description |
|---|---|---|---|
quote_token_id | string | Yes | A saved quote token from Calculate Order (call it with save_quote: true) |
payment_plan | string | No | full (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
}
}| Field | Type | Description |
|---|---|---|
client_secret | string | Confirm the payment client-side with Stripe.js |
payment_intent_id | string | Pass to Create Order with Stripe Payment |
publishable_key | string | Stripe publishable key for this account/location |
amount | integer | Charge 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/stripeAuthentication: 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
| Field | Type | Required | Description |
|---|---|---|---|
quote_token_id | string | Yes | The quote the PaymentIntent was created for |
customer | object | Yes | { name (req), phone (req), email, notify_on_ready } |
payment | object | Yes | Payment details |
payment.method | string | Yes | stripe, terminal, cash, or pay_later |
payment.payment_intent_id | string | Cond. | Required when payment.method is stripe |
special_instructions | string | No | Order-level note |
concept_id | string | No | Required 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/presentmentAuthentication: OAuth Token
Scope: kitchenclick:payments.create
Rate Limit: 30/min per client
Request Body
json
{
"quote_token_id": "kqt_00000a1B2c3D4e5"
}| Field | Type | Required | Description |
|---|---|---|---|
quote_token_id | string | Yes | A 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}/statusAuthentication: 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
| Status | Description |
|---|---|
pending | Awaiting payment |
processing | Payment in progress |
completed | Payment successful, order created |
failed | Payment failed |
expired | Presentment expired (5 min timeout) |
cancelled | Cancelled by user |
Simulate Payment (Non-Production)
For testing in development/staging environments.
POST /v1/ecommerce/payment/presentment/{presentmentToken}/simulateAuthentication: OAuth Token
Scope: kitchenclick:payments.create
Environments: Development, Staging only
Request Body
json
{
"approve": true
}| Field | Type | Required | Description |
|---|---|---|---|
approve | boolean | Yes | true 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
| Code | Description |
|---|---|
card_declined | Card was declined |
insufficient_funds | Insufficient funds |
expired_card | Card has expired |
invalid_card | Invalid card number |
processing_error | Payment processor error |
presentment_expired | Payment 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
- Never log full card numbers - Only store last 4 digits
- Use HTTPS only - All payment requests must be encrypted
- Validate amounts server-side - Don't trust client-submitted totals
- Implement idempotency - Use payment intent IDs to prevent duplicate charges
- Handle webhooks - Set up Stripe webhooks for payment status updates
Changelog
| Date | Change |
|---|---|
| 2026-06-17 | Corrected 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-14 | Added e-commerce API endpoints. |
| 2026-01-15 | Initial publication. |