Docs
Firmly Agentic Commerce
Set theme to dark (⇧+D)

1-Step Checkout

​​ Why 1-Step Checkout?

Firmly’s full Cart API walks through several steps:

  1. Add items (the cart is created implicitly) → 2. Set shipping → 3. Get merchant-calculated tax & totals → 4. Process payment → 5. Complete order

With Firmly’s place-order API, you can do all of this in one single API call.

​​ Benefits

Higher Conversion Reduce checkout abandonment with fewer steps
Faster Integration Implement in hours, not weeks
Less Complexity One endpoint to maintain instead of six

​​ How It Works

The place-order API combines cart creation, item addition, shipping calculation, and payment processing into a single atomic operation:

​​ Quick Implementation

​​ Collect Customer Information

Gather items, shipping address, and credit card details in your checkout form

​​ Encrypt Credit Card

Use Firmly’s public key to JWE-encrypt card details for secure transmission

​​ Call place-order API

Submit everything in one API call and handle the response

​​ Credit Card Encryption

The credit card data must be encrypted as a JWE token containing these fields:

Field Description Example
number Card number (PAN) 4111111111111111
name Cardholder name John Smith
verification_value CVV/CVC 123
month Expiry month (2 digits) 08
year Expiry year (4 digits) 2028

The plaintext object encrypted into the JWE looks like this:


{
"number": "4111111111111111",
"name": "John Smith",
"verification_value": "123",
"month": "08",
"year": "2028"
}

​​ Complete Example

Here’s a full implementation of 1-step checkout.


// First, install the jose library: npm install jose
import * as jose from 'jose';
async function oneStepCheckout(checkoutData, domain, authToken) {
// Step 1: Get public key for encryption
const publicKeyResponse = await fetch(
'https://cc.firmly.work/api/v1/payment/key',
{
headers: {
'x-firmly-authorization': authToken
}
}
);
const publicKeyJWK = await publicKeyResponse.json();
// Step 2: Encrypt credit card data using JWE
const cardData = {
number: checkoutData.cardNumber,
name: checkoutData.cardholderName,
verification_value: checkoutData.cvv,
month: checkoutData.expMonth, // e.g., "08"
year: checkoutData.expYear // e.g., "2028"
};
// Import the public key
const publicKey = await jose.importJWK(publicKeyJWK, 'RSA-OAEP-256');
// Create JWE encrypted card
const encryptedCard = await new jose.CompactEncrypt(
new TextEncoder().encode(JSON.stringify(cardData))
)
.setProtectedHeader({
alg: 'RSA-OAEP-256',
enc: 'A256GCM',
kid: publicKeyJWK.kid
})
.encrypt(publicKey);
// Step 3: Place order in one call
const orderResponse = await fetch(
`https://cc.firmly.work/api/v2/payment/domains/${domain}/place-order`,
{
method: 'POST',
headers: {
'x-firmly-authorization': authToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
items: [
{
add_to_cart_ref: { variant_id: 'WS12-XS-Orange' },
quantity: 2
}
],
shipping_info: {
first_name: 'John',
last_name: 'Doe',
address1: '123 Main St',
city: 'New York',
state_or_province: 'NY',
postal_code: '10001',
country: 'US',
email: 'john@staging.luma.gift',
phone: '555-1234'
},
billing_info: {
// Same structure as shipping_info (billing_info is required)
first_name: 'John',
last_name: 'Doe',
address1: '123 Main St',
city: 'New York',
state_or_province: 'NY',
postal_code: '10001',
country: 'US',
email: 'john@staging.luma.gift',
phone: '555-1234'
},
encrypted_card: encryptedCard
})
}
);
// Handle response
const result = await orderResponse.json();
// Success is signalled by cart_status === "submitted",
// NOT by the mere presence of cart_id.
if (result.cart_status === 'submitted') {
// Success! Order completed
return {
success: true,
cartId: result.cart_id,
orderNumber: result.platform_order_number
};
} else {
// Handle error
return {
success: false,
error: result.error
};
}
}
// Example usage:
const checkoutData = {
cardNumber: '4111111111111111',
cardholderName: 'John Doe',
cvv: '123',
expMonth: '12',
expYear: '2028'
};
const domain = 'staging.luma.gift';
const authToken = 'YOUR_AUTH_TOKEN'; // Get this from browser session
// Call the function
oneStepCheckout(checkoutData, domain, authToken)
.then(result => {
if (result.success) {
console.log('Order placed successfully!', result.orderNumber);
} else {
console.error('Order failed:', result.error);
}
});

import requests
import json
from joserfc.jwe import encrypt_compact
from joserfc.jwk import RSAKey
def one_step_checkout(checkout_data, domain, auth_token):
# Step 1: Get public key in JWK format (default)
public_key_response = requests.get(
"https://cc.firmly.work/api/v1/payment/key",
headers={
'x-firmly-authorization': auth_token
}
)
public_key_jwk = public_key_response.json()
# Step 2: Prepare card data
card_data = {
'number': checkout_data['card_number'],
'name': checkout_data['cardholder_name'],
'verification_value': checkout_data['cvv'],
'month': checkout_data['exp_month'], # e.g., "08"
'year': checkout_data['exp_year'] # e.g., "2028"
}
# Step 3: Encrypt using JWE with JWK
# Import the JWK
public_key = RSAKey.import_key(public_key_jwk)
# Create JWE encrypted card. joserfc's signature is
# encrypt_compact(protected, plaintext, public_key):
# protected header first, plaintext second, key third.
protected_header = {
"alg": "RSA-OAEP-256",
"enc": "A256GCM",
"kid": public_key_jwk['kid'] # Get kid from JWK response
}
encrypted_card = encrypt_compact(
protected_header,
json.dumps(card_data).encode('utf-8'),
public_key
)
# Step 4: Place order
order_data = {
'items': [
{
'add_to_cart_ref': {'variant_id': 'WS12-XS-Orange'},
'quantity': 2
}
],
'shipping_info': {
'first_name': 'John',
'last_name': 'Doe',
'address1': '123 Main St',
'city': 'New York',
'state_or_province': 'NY',
'postal_code': '10001',
'country': 'US',
'email': 'john@staging.luma.gift',
'phone': '555-1234'
},
'billing_info': {
# Same structure as shipping_info
'first_name': 'John',
'last_name': 'Doe',
'address1': '123 Main St',
'city': 'New York',
'state_or_province': 'NY',
'postal_code': '10001',
'country': 'US',
'email': 'john@staging.luma.gift',
'phone': '555-1234'
},
'encrypted_card': encrypted_card
}
response = requests.post(
f"https://cc.firmly.work/api/v2/payment/domains/{domain}/place-order",
headers={
'x-firmly-authorization': auth_token,
'Content-Type': 'application/json'
},
json=order_data
)
result = response.json()
# Success is signalled by cart_status == "submitted",
# NOT by the mere presence of cart_id.
if result.get('cart_status') == 'submitted':
return {
'success': True,
'cart_id': result.get('cart_id'),
'order_number': result.get('platform_order_number')
}
else:
return {
'success': False,
'error': result.get('error')
}
# Example usage:
checkout_data = {
'card_number': '4111111111111111',
'cardholder_name': 'John Doe',
'cvv': '123',
'exp_month': '12',
'exp_year': '2028'
}
domain = 'staging.luma.gift'
auth_token = 'YOUR_AUTH_TOKEN' # Get this from browser session
# Call the function
result = one_step_checkout(checkout_data, domain, auth_token)
if result['success']:
print(f"Order placed successfully! Order Number: {result['order_number']}")
else:
print(f"Order failed: {result['error']}")

// Dependencies:
// - com.nimbusds:nimbus-jose-jwt:9.31
// - com.fasterxml.jackson.core:jackson-databind:2.15.2
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSAEncrypter;
import com.nimbusds.jose.jwk.RSAKey;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
public class OneStepCheckout {
private static final String API_BASE = "https://cc.firmly.work";
private static final ObjectMapper mapper = new ObjectMapper();
public static CheckoutResult performCheckout(
CheckoutData checkoutData,
String domain,
String authToken
) throws Exception {
// Step 1: Get public key in JWK format
HttpClient client = HttpClient.newHttpClient();
HttpRequest keyRequest = HttpRequest.newBuilder()
.uri(URI.create(API_BASE + "/api/v1/payment/key"))
.header("x-firmly-authorization", authToken)
.GET()
.build();
HttpResponse<String> keyResponse = client.send(
keyRequest,
HttpResponse.BodyHandlers.ofString()
);
Map<String, Object> jwkData = mapper.readValue(
keyResponse.body(),
Map.class
);
// Step 2: Prepare card data
Map<String, String> cardData = new HashMap<>();
cardData.put("number", checkoutData.getCardNumber());
cardData.put("name", checkoutData.getCardholderName());
cardData.put("verification_value", checkoutData.getCvv());
cardData.put("month", checkoutData.getExpMonth()); // e.g., "08"
cardData.put("year", checkoutData.getExpYear()); // e.g., "2028"
// Step 3: Encrypt using JWE
RSAKey rsaKey = RSAKey.parse(mapper.writeValueAsString(jwkData));
// Create JWE object
JWEHeader header = new JWEHeader.Builder(
JWEAlgorithm.RSA_OAEP_256,
EncryptionMethod.A256GCM
)
.keyID(jwkData.get("kid").toString())
.build();
Payload payload = new Payload(mapper.writeValueAsString(cardData));
JWEObject jweObject = new JWEObject(header, payload);
// Encrypt
jweObject.encrypt(new RSAEncrypter(rsaKey));
String encryptedCard = jweObject.serialize();
// Step 4: Place order
Map<String, Object> orderData = new HashMap<>();
orderData.put("items", List.of(
Map.of(
"add_to_cart_ref", Map.of("variant_id", "WS12-XS-Orange"),
"quantity", 2
)
));
Map<String, String> shippingInfo = new HashMap<>();
shippingInfo.put("first_name", "John");
shippingInfo.put("last_name", "Doe");
shippingInfo.put("address1", "123 Main St");
shippingInfo.put("city", "New York");
shippingInfo.put("state_or_province", "NY");
shippingInfo.put("postal_code", "10001");
shippingInfo.put("country", "US");
shippingInfo.put("email", "john@staging.luma.gift");
shippingInfo.put("phone", "555-1234");
orderData.put("shipping_info", shippingInfo);
orderData.put("billing_info", shippingInfo); // Same as shipping
orderData.put("encrypted_card", encryptedCard);
HttpRequest orderRequest = HttpRequest.newBuilder()
.uri(URI.create(API_BASE + "/api/v2/payment/domains/" + domain + "/place-order"))
.header("x-firmly-authorization", authToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
mapper.writeValueAsString(orderData)
))
.build();
HttpResponse<String> orderResponse = client.send(
orderRequest,
HttpResponse.BodyHandlers.ofString()
);
Map<String, Object> result = mapper.readValue(
orderResponse.body(),
Map.class
);
// Success is signalled by cart_status == "submitted",
// NOT by the mere presence of cart_id.
if ("submitted".equals(String.valueOf(result.get("cart_status")))) {
return new CheckoutResult(
true,
String.valueOf(result.get("cart_id")),
String.valueOf(result.get("platform_order_number")),
null
);
} else {
return new CheckoutResult(
false,
null,
null,
result.get("error").toString()
);
}
}
}
// Supporting classes
class CheckoutData {
private String cardNumber;
private String cardholderName;
private String cvv;
private String expMonth;
private String expYear;
// ... getters and setters
}
class CheckoutResult {
private boolean success;
private String cartId;
private String orderNumber;
private String error;
public CheckoutResult(boolean success, String cartId,
String orderNumber, String error) {
this.success = success;
this.cartId = cartId;
this.orderNumber = orderNumber;
this.error = error;
}
// ... getters
}
// Example usage:
public class Main {
public static void main(String[] args) {
CheckoutData checkoutData = new CheckoutData();
checkoutData.setCardNumber("4111111111111111");
checkoutData.setCardholderName("John Doe");
checkoutData.setCvv("123");
checkoutData.setExpMonth("12");
checkoutData.setExpYear("2028");
String domain = "staging.luma.gift";
String authToken = "YOUR_AUTH_TOKEN"; // Get this from browser session
try {
CheckoutResult result = OneStepCheckout.performCheckout(
checkoutData,
domain,
authToken
);
if (result.isSuccess()) {
System.out.println("Order placed successfully! Order Number: " +
result.getOrderNumber());
} else {
System.out.println("Order failed: " + result.getError());
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
}

<?php
// First install: composer require web-token/jwt-framework
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP256;
use Jose\Component\Encryption\Algorithm\ContentEncryption\A256GCM;
use Jose\Component\Encryption\Compression\CompressionMethodManager;
use Jose\Component\Encryption\Compression\Deflate;
use Jose\Component\Encryption\JWEBuilder;
use Jose\Component\Encryption\Serializer\CompactSerializer;
function oneStepCheckout($checkoutData, $domain, $authToken) {
// Step 1: Get public key in JWK format
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,
"https://cc.firmly.work/api/v1/payment/key");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"x-firmly-authorization: {$authToken}"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$publicKeyResponse = curl_exec($ch);
$publicKeyData = json_decode($publicKeyResponse, true);
curl_close($ch);
// Step 2: Create JWK from public key
$jwk = new JWK($publicKeyData);
// Step 3: Prepare card data
$cardData = [
'number' => $checkoutData['cardNumber'],
'name' => $checkoutData['cardholderName'],
'verification_value' => $checkoutData['cvv'],
'month' => $checkoutData['expMonth'], // e.g., "08"
'year' => $checkoutData['expYear'] // e.g., "2028"
];
// Step 4: Create JWE
$algorithmManager = new AlgorithmManager([
new RSAOAEP256(),
new A256GCM(),
]);
$compressionMethodManager = new CompressionMethodManager([
new Deflate(),
]);
$jweBuilder = new JWEBuilder(
$algorithmManager,
$algorithmManager,
$compressionMethodManager
);
$payload = json_encode($cardData);
$jwe = $jweBuilder
->create()
->withPayload($payload)
->withSharedProtectedHeader([
'alg' => 'RSA-OAEP-256',
'enc' => 'A256GCM',
'kid' => $publicKeyData['kid']
])
->addRecipient($jwk)
->build();
$serializer = new CompactSerializer();
$encryptedCard = $serializer->serialize($jwe, 0);
// Step 5: Place order
$orderData = [
'items' => [
[
'add_to_cart_ref' => ['variant_id' => 'WS12-XS-Orange'],
'quantity' => 2
]
],
'shipping_info' => [
'first_name' => 'John',
'last_name' => 'Doe',
'address1' => '123 Main St',
'city' => 'New York',
'state_or_province' => 'NY',
'postal_code' => '10001',
'country' => 'US',
'email' => 'john@staging.luma.gift',
'phone' => '555-1234'
],
'billing_info' => [
// Same structure as shipping_info
'first_name' => 'John',
'last_name' => 'Doe',
'address1' => '123 Main St',
'city' => 'New York',
'state_or_province' => 'NY',
'postal_code' => '10001',
'country' => 'US',
'email' => 'john@staging.luma.gift',
'phone' => '555-1234'
],
'encrypted_card' => $encryptedCard
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,
"https://cc.firmly.work/api/v2/payment/domains/{$domain}/place-order");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"x-firmly-authorization: {$authToken}",
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($orderData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$result = json_decode($response, true);
curl_close($ch);
// Success is signalled by cart_status === "submitted",
// NOT by the mere presence of cart_id.
if (isset($result['cart_status']) && $result['cart_status'] === 'submitted') {
return [
'success' => true,
'cart_id' => $result['cart_id'] ?? null,
'order_number' => $result['platform_order_number'] ?? null
];
} else {
return [
'success' => false,
'error' => $result['error'] ?? 'Unknown error'
];
}
}
// Example usage:
$checkoutData = [
'cardNumber' => '4111111111111111',
'cardholderName' => 'John Doe',
'cvv' => '123',
'expMonth' => '12',
'expYear' => '2028'
];
$domain = 'staging.luma.gift';
$authToken = 'YOUR_AUTH_TOKEN'; // Get this from browser session
// Call the function
$result = oneStepCheckout($checkoutData, $domain, $authToken);
if ($result['success']) {
echo "Order placed successfully! Order Number: " . $result['order_number'];
} else {
echo "Order failed: " . $result['error'];
}
?>

​​ Important Security Notes

​​ Real-Time Progress with SSE

​​ Using SSE for Live Updates


// Option 1: Use SSE by setting Accept header
const domain = 'staging.luma.gift';
const authToken = 'YOUR_AUTH_TOKEN'; // Get this from browser session
const orderData = {
items: [{ add_to_cart_ref: { variant_id: 'WS12-XS-Orange' }, quantity: 1 }],
shipping_info: { /* ... */ },
billing_info: { /* ... */ },
encrypted_card: encryptedCard
};
const response = await fetch(
`https://cc.firmly.work/api/v2/payment/domains/${domain}/place-order`,
{
method: 'POST',
headers: {
'x-firmly-authorization': authToken,
'Content-Type': 'application/json',
'Accept': 'text/event-stream' // Enable SSE
},
body: JSON.stringify(orderData)
}
);
// Read the event stream
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// Parse SSE events from chunk
console.log('Event:', chunk);
}

​​ SSE Event Types

  • order_processing: Order is being validated and processed
  • payment_processing: Payment is being authorized
  • order_completed: Order successfully placed with order details
  • error: Processing failed with error details

​​ When to Use 1-Step Checkout

  • Single vendor stores
  • Standard shipping only
  • High-volume, low-complexity products
  • Mobile-first experiences
  • Multiple shipments needed
  • Complex shipping options (scheduled delivery)
  • B2B with special requirements
  • Require extensive customization

​​ Error Handling

The place-order API provides detailed error responses:


{
"code": 400,
"error": "InvalidShippingInfo",
"description": "Shipping address could not be validated"
}

The error envelope is { code, error, description } — code is the numeric HTTP status and error is the PascalCase wire name from the error catalog. Common error scenarios:

  • InvalidShippingInfo: Address validation failed (InvalidInputBody for malformed request bodies)
  • NotEnoughStockError: Product not available (409)
  • CreditCardDeclined: Card declined (422)
  • CreditCardInvalidNumber / CreditCardInvalidExpiry / CreditCardInvalidSecurityCode: Card details failed validation (422)

​​ Next Steps

​​ Support

Need help implementing 1-step checkout? Contact Firmly.