Partner dashboard
API contract v2

Partner API

Build carts from product URLs, handle reviewed quotes as a normal asynchronous state, and pay server-authoritative orders.

Contract
Server-authoritative totals
Review
Asynchronous and resumable
Payment gate
`payment_ready` only

Authentication and authority

Keep credentials and all partner API calls on your server.

X-API-Key: sp3nd_...
X-API-Secret: sp3nd_sec_...
SP3ND resolves product facts and owns the final price, shipping, tax, fees, total, currency, payment memo, and recipient. Caller values are never payment authority.
Base URL
https://us-central1-sp3nddotshop-prod.cloudfunctions.net
schema_version
Version 2 is additive; existing response fields remain available.
payment_ready
The only supported signal that payment can begin.

Order lifecycle

A review state is a successful order creation, not an API failure.

Ready immediately
{
  "status": "Created",
  "checkout_role": "direct_payment",
  "pricing_status": "ready_for_payment",
  "requires_manual_quote": false,
  "payment_ready": true
}
Team review
{
  "status": "Awaiting Review",
  "checkout_role": "manual_review",
  "pricing_status": "awaiting_team_quote",
  "requires_manual_quote": true,
  "payment_ready": false
}
Shipping choice
{
  "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_role
Exactly `direct_payment` or `manual_review`; stored legacy roles are normalized.
pricing_status
Primary values are `awaiting_team_quote`, `shipping_selection_required`, and `ready_for_payment`; this field is informational.
payment_ready
The sole payment gate. No role, status, quote, or total authorizes payment by itself.

Create 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-Key
Preferred header. `idempotency_key` in the body is supported for compatibility.
same key + same request
Returns the original order and can include `idempotent_replay: true`.
same key + changed request
Returns `409 IDEMPOTENCY_CONFLICT`.

Review 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_revision
Identifies the current team quote.
quote_expires_at
After expiry, fetch a new quote before continuing.
shipping_options
Each option uses shipping_option_id and carries label, shipping_amount, tax_amount, total_amount, and currency; description and estimated_delivery are optional.
selected_shipping_option_id
The canonical opaque identifier for the selected option, or null while a choice is required.
selected_shipping_option
The current reviewed choice, normalized to the same shape as an option.
selected_manual_shipping_option
Deprecated compatibility alias containing the same normalized object as selected_shipping_option.

Quote 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.

Standard Partner API

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;
}
  1. 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.
  2. Treat `recipient_address` as the treasury owner. Derive both canonical ATAs from the Solana mainnet USDC mint and the standard Token/Associated Token programs.
  3. 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.
  4. Sign and broadcast through the partner's wallet and Solana RPC. Do not send `PAYMENT-SIGNATURE` or call a facilitator.
  5. 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.

Advanced legacy agentic-skill appendix

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.

MCP boundary: the public SP3ND MCP uses a separate internal `partnerPayment` prepare/submit bridge. `partnerPayment` is not a Partner API payment option. Do not copy MCP checkout payloads into either endpoint above.

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.

`ORDER_NOT_PAYMENT_READY` means keep the customer in review and poll the order. `PAYMENT_SETTLEMENT_IN_PROGRESS` also means poll without resubmitting payment. If `PAYMENT_SETTLEMENT_UNKNOWN` is returned, do not retry: retain the order ID and request manual reconciliation.

Supported endpoints

Cloud Function endpoint names are part of the public contract.

POSTcreatePartnerCartCreate a 30-minute cart
POSTaddItemToPartnerCart/{cartId}Add one item
PATCHupdateCartItemQuantity/{cartId}/{itemId}Change quantity
DELETEremoveCartItem/{cartId}/{itemId}Remove an item
PATCHupdateCartShippingAddress/{cartId}Change destination and reprice
GETgetPartnerCart/{cartId}Read a cart
POSTcreatePartnerOrderCreate an order
GETgetPartnerOrder?order_id=...Read one order
GETgetPartnerOrdersList orders
POSTselectPartnerOrderShippingOptionChoose reviewed shipping
POSTcreatePartnerTransactionStandard partner-managed payment attempt
POSTpayAgentOrderAdvanced legacy agentic-skill clients only

To 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_READY

Poll the order; do not submit payment.

ORDER_NOT_PAYABLE

Stop; this order cannot accept payment.

PAYMENT_SETTLEMENT_IN_PROGRESS

Do not resubmit payment; poll the order.

PAYMENT_SETTLEMENT_UNKNOWN

Do not retry; retain the order ID for manual reconciliation.

IDEMPOTENCY_CONFLICT

Use the original payload or a new checkout key.

QUOTE_EXPIRED

Fetch the current order and quote.

QUOTE_REVISION_CONFLICT

Refresh before selecting shipping.

CART_EXPIRED

Create and price a new cart.

RATE_LIMITED

Back off before retrying.

Migration checklist

Persist order_id, not only order_number.
Gate payment on payment_ready.
Treat Awaiting Review as resumable.
Poll getPartnerOrder with backoff.
Submit shipping option IDs unchanged.
Let SP3ND derive every payment field.
Use one idempotency key per checkout.
Handle mixed carts as one review order.

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.