Partner API
Build carts from product URLs, handle reviewed quotes as a normal asynchronous state, and pay server-authoritative orders.
Authentication and authority
Keep credentials and all partner API calls on your server.
X-API-Key: sp3nd_...
X-API-Secret: sp3nd_sec_...Base URLschema_versionpayment_readyOrder lifecycle
A review state is a successful order creation, not an API failure.
{
"status": "Created",
"checkout_role": "direct_payment",
"pricing_status": "ready_for_payment",
"requires_manual_quote": false,
"payment_ready": true
}{
"status": "Awaiting Review",
"checkout_role": "manual_review",
"pricing_status": "awaiting_team_quote",
"requires_manual_quote": true,
"payment_ready": false
}{
"checkout_role": "manual_review",
"pricing_status": "shipping_selection_required",
"payment_ready": false,
"selected_shipping_option_id": null
}If any item in a mixed cart needs manual review, the entire cart becomes one `Awaiting Review` order in v2. It is not split into child orders.
checkout_rolepricing_statuspayment_readyCreate a destination-aware cart
Send URLs and quantities. SP3ND resolves product metadata and commerce values.
const response = await fetch(
'https://us-central1-sp3nddotshop-prod.cloudfunctions.net/createPartnerCart',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.SP3ND_API_KEY,
'X-API-Secret': process.env.SP3ND_API_SECRET
},
body: JSON.stringify({
ship_to_country: 'United States',
ship_to_postal_code: '10007',
items: [{
product_url: 'https://www.ebay.com/itm/123456789',
quantity: 1
}]
})
}
);Carts expire after 30 minutes. A complete `shipping_address` can replace the country/postal shortcut. Legacy clients can send extra item metadata, but caller-supplied titles and prices are not treated as verified data.
Create an idempotent order
One stable idempotency key represents one checkout attempt.
const response = await fetch(
'https://us-central1-sp3nddotshop-prod.cloudfunctions.net/createPartnerOrder',
{
method: 'POST',
headers: {
...authHeaders,
'Idempotency-Key': checkoutAttemptId
},
body: JSON.stringify({
cart_id: cart.cart_id,
customer_email: 'buyer@example.com',
shipping_address: {
name: 'Ada Buyer',
recipient: 'Ada Buyer',
address1: '123 Main Street',
address2: 'Suite 4',
city: 'New York',
state: 'NY',
postalCode: '10007',
country: 'United States',
phone: '+12125550123'
}
})
}
);Idempotency-Keysame key + same requestsame key + changed requestReview quotes and shipping
Poll the order, present current choices, and submit an opaque option ID.
GET https://us-central1-sp3nddotshop-prod.cloudfunctions.net/getPartnerOrder?order_id=abc123
POST https://us-central1-sp3nddotshop-prod.cloudfunctions.net/selectPartnerOrderShippingOption
{
"order_id": "abc123",
"shipping_option_id": "standard"
}quote_revisionquote_expires_atshipping_optionsselected_shipping_option_idselected_shipping_optionselected_manual_shipping_optionQuote revision and expiry are enforced on selection. If the server returns a conflict, refresh the order and show the current options. Never calculate or patch the total client-side.
Pay a ready order
Normal Partner API integrations use createPartnerTransaction.
If you were issued Partner API credentials for a normal server integration, use `createPartnerTransaction`. Do not use `payAgentOrder`. Call the standard endpoint once, then build and submit the on-chain payment in your own wallet integration using only the authoritative values returned by SP3ND.
Never call `createPartnerTransaction` and then `payAgentOrder` for the same checkout. They are two different payment paths and must not share client-side settlement state.
Standard Partner API flow
POST https://us-central1-sp3nddotshop-prod.cloudfunctions.net/createPartnerTransaction
Content-Type: application/json
X-API-Key: <api-key>
X-API-Secret: <api-secret>
{
"order_id": "abc123",
"sender_address": "<buyer-solana-wallet>"
}
// 201 response: use these exact server-derived fields
{
"success": true,
"status": "pending",
"order_id": "abc123",
"order_number": "ORD-...",
"amount": 10.81,
"currency": "USDC",
"memo": "SP3ND Order: ORD-...",
"recipient_address": "<sp3nd-treasury>"
}`createPartnerTransaction` registers the authoritative payment attempt; it does not submit or sign a wallet transaction for you. Make that authenticated request on your server. Pass only its returned payment instructions to your trusted wallet signer.
Exact Solana USDC construction
import {
PublicKey,
Transaction,
TransactionInstruction,
} from '@solana/web3.js';
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
createTransferCheckedInstruction,
getAssociatedTokenAddressSync,
} from '@solana/spl-token';
const USDC_MINT = new PublicKey(
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
);
const MEMO_PROGRAM_ID = new PublicKey(
'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'
);
// 10.81 USDC -> 10_810_000n. Never multiply a JS number by 1e6.
function toUsdcAtomicUnits(decimalAmount) {
const match = /^(0|[1-9]\d*)(?:\.(\d{1,6}))?$/.exec(
String(decimalAmount)
);
if (!match) throw new Error('Invalid USDC amount from SP3ND');
const [, whole, fraction = ''] = match;
return BigInt(whole) * 1_000_000n +
BigInt(fraction.padEnd(6, '0'));
}
async function signAndBroadcastSp3ndPayment({
payment,
buyerPublicKey,
connection,
signTransaction,
}) {
if (payment.status === 'confirmed') return null; // Never pay again.
if (payment.status !== 'pending' || payment.currency !== 'USDC') {
throw new Error('SP3ND payment is not payable');
}
const recipientOwner = new PublicKey(payment.recipient_address);
const buyerUsdcAta = getAssociatedTokenAddressSync(
USDC_MINT, buyerPublicKey, false,
TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID
);
const recipientUsdcAta = getAssociatedTokenAddressSync(
USDC_MINT, recipientOwner, false,
TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID
);
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash('confirmed');
const transaction = new Transaction({
feePayer: buyerPublicKey,
recentBlockhash: blockhash,
}).add(
createTransferCheckedInstruction(
buyerUsdcAta,
USDC_MINT,
recipientUsdcAta,
buyerPublicKey,
toUsdcAtomicUnits(payment.amount),
6,
[],
TOKEN_PROGRAM_ID
),
new TransactionInstruction({
programId: MEMO_PROGRAM_ID,
keys: [],
data: Buffer.from(payment.memo, 'utf8'),
})
);
const signed = await signTransaction(transaction); // buyer signs
const signature = await connection.sendRawTransaction(signed.serialize());
await connection.confirmTransaction(
{ signature, blockhash, lastValidBlockHeight },
'confirmed'
);
return signature;
}- Use the returned amount, currency, memo, and recipient unchanged. Convert the decimal amount by padding to six places: `10.81` becomes exactly `10,810,000` atomic units, never a floating-point multiplication.
- Treat `recipient_address` as the treasury owner. Derive both canonical ATAs from the Solana mainnet USDC mint and the standard Token/Associated Token programs.
- Fetch a fresh blockhash and build a legacy `Transaction` with the buyer as fee payer and `TransferChecked` authority. Add `TransferChecked` followed by the Memo Program instruction using the exact memo bytes.
- Sign and broadcast through the partner's wallet and Solana RPC. Do not send `PAYMENT-SIGNATURE` or call a facilitator.
- Poll `getPartnerOrder` until SP3ND confirms `Paid`. Never send a second payment while confirmation is pending.
HTTP `200` with `status: confirmed` means the order was already paid and no transaction may be constructed. HTTP `201` with `status: pending` supplies the one payment above. On `409` or any other non-2xx response, do not broadcast; read the order and follow its lifecycle code.
Use `payAgentOrder` only if your client intentionally implements SP3ND's published legacy x402 agentic-skill contract. It is not the next step after `createPartnerTransaction`, and it is not the MCP payment path. Follow the separate published SP3ND agentic skill or contact SP3ND for the current legacy integration contract.
For both paths, `order_number` is optional and validated when sent. Caller-provided `amount`, `currency`, `memo`, and `recipient_address` are ignored. Payment can begin only when the latest response has `payment_ready: true`; `pricing_status` is informational and is never a second payment gate.
Supported endpoints
Cloud Function endpoint names are part of the public contract.
createPartnerCartCreate a 30-minute cartaddItemToPartnerCart/{cartId}Add one itemupdateCartItemQuantity/{cartId}/{itemId}Change quantityremoveCartItem/{cartId}/{itemId}Remove an itemupdateCartShippingAddress/{cartId}Change destination and repricegetPartnerCart/{cartId}Read a cartcreatePartnerOrderCreate an ordergetPartnerOrder?order_id=...Read one ordergetPartnerOrdersList ordersselectPartnerOrderShippingOptionChoose reviewed shippingcreatePartnerTransactionStandard partner-managed payment attemptpayAgentOrderAdvanced legacy agentic-skill clients onlyTo list all orders, omit `status` or use `status=all`. The plural `addItemsToPartnerCart` endpoint is not supported in v2.
Lifecycle conflicts
Use machine-readable codes and refresh state after quote conflicts.
ORDER_NOT_PAYMENT_READYPoll the order; do not submit payment.
ORDER_NOT_PAYABLEStop; this order cannot accept payment.
PAYMENT_SETTLEMENT_IN_PROGRESSDo not resubmit payment; poll the order.
PAYMENT_SETTLEMENT_UNKNOWNDo not retry; retain the order ID for manual reconciliation.
IDEMPOTENCY_CONFLICTUse the original payload or a new checkout key.
QUOTE_EXPIREDFetch the current order and quote.
QUOTE_REVISION_CONFLICTRefresh before selecting shipping.
CART_EXPIREDCreate and price a new cart.
RATE_LIMITEDBack off before retrying.
Migration checklist
Support
Email support@sp3nd.shop with your partner name, endpoint, timestamp, and `order_id` or `cart_id`. Never send an API secret or signed payment payload.