Skip to main content
Migrate to Polaris

Version 2025-07 is the last API version to support React-based UI components. Later versions use web components, native UI elements with built-in accessibility, better performance, and consistent styling with Shopify's design system. Check out the upgrade guide to upgrade your extension.

Draft orders

Draft orders allow merchants to create orders on behalf of customers for scenarios like phone orders, wholesale quotes, custom orders, and manual invoicing. Draft order pages display information about individual draft orders and draft order lists. Extensions on these pages help merchants enhance these workflows with custom functionality.

  • External order management: Sync draft orders with external order management systems, ERPs, or wholesale platforms to maintain consistent order data across systems and support B2B workflows.
  • Custom pricing and quotes: Display custom pricing, apply special discounts, calculate complex wholesale pricing, or generate professional quotes with pricing from external systems before converting draft orders.
  • Order validation and verification: Validate draft order data against external systems, verify customer credit limits, check inventory availability across warehouses, or flag potential issues before order completion.
  • Payment processing workflows: Integrate custom payment workflows, generate payment links for draft orders, process deposits or partial payments, or send payment requests to customers through external payment gateways.
  • Bulk draft order operations: Process multiple draft orders at once for operations like bulk conversion, batch invoice generation, mass updates, or exporting draft order data to accounting systems.
Shopify admin draft order pages showing all available extension target locations.

Anchor to Draft order details targetsDraft order details targets

Use action and block targets to extend the draft order details page. Add workflows and contextual information that help merchants manage individual draft orders and improve order creation processes.

Action targets open as modal overlays from the More actions menu, while block targets display as inline cards. The examples demonstrate fetching data from Shopify's direct API or your app's backend.

admin.draft-order-details.action.render

Renders an admin action extension on the draft order details page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on individual draft orders, such as syncing with external systems, generating quotes, processing payments, or applying custom pricing.

Extensions at this target can access information about the draft order through the data property in the Action Extension API. The action renders in a modal overlay, providing space for multi-step workflows, forms, and confirmations.

Examples
import React from 'react';
import {useState, useEffect} from 'react';
import {
reactExtension,
useApi,
AdminAction,
Banner,
Section,
BlockStack,
Box,
Checkbox,
TextField,
Text,
Button,
ProgressIndicator,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.draft-order-details.action.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const {data, close, query} = useApi(TARGET);
const [loading, setLoading] = useState(false);
const [fetching, setFetching] = useState(true);
const [draftOrder, setDraftOrder] = useState(null);
const [sendEmail, setSendEmail] = useState(true);
const [success, setSuccess] = useState(false);
const [error, setError] = useState(false);
const [paymentLink, setPaymentLink] = useState('');

useEffect(() => {
const fetchDraftOrder = async () => {
const draftOrderId = data.selected[0].id;

try {
// Fetch draft order details from GraphQL Admin API
const {data: draftOrderData} = await query(
`
query GetDraftOrder($id: ID!) {
draftOrder(id: $id) {
id
name
customer {
email
firstName
lastName
}
totalPriceSet {
presentmentMoney {
amount
currencyCode
}
}
}
}
`,
{variables: {id: draftOrderId}}
);

setDraftOrder(draftOrderData.draftOrder);
} catch (err) {
console.error('Error fetching draft order:', err);
} finally {
setFetching(false);
}
};

fetchDraftOrder();
}, [data, query]);

const handleGenerate = async () => {
setLoading(true);
setSuccess(false);
setError(false);
const draftOrderId = data.selected[0].id;

try {
// Generate payment link through your app's backend
const response = await fetch('https://your-app.com/api/generate-payment-link', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
draftOrderId,
sendEmail,
}),
});

if (response.ok) {
const {paymentUrl} = await response.json();
setPaymentLink(paymentUrl);
setSuccess(true);
} else {
setError(true);
}
} catch (err) {
setError(true);
} finally {
setLoading(false);
}
};

if (fetching) {
return (
<AdminAction title="Generate Payment Link">
<BlockStack>
<ProgressIndicator size="small-100" />
<Text>Loading draft order details...</Text>
</BlockStack>
</AdminAction>
);
}

return (
<AdminAction
title="Generate Payment Link"
primaryAction={
<Button
onPress={handleGenerate}
disabled={loading || success}
>
{loading ? 'Generating...' : 'Generate Link'}
</Button>
}
secondaryAction={
<Button onPress={close}>
{success ? 'Close' : 'Cancel'}
</Button>
}
>
{success && (
<Banner tone="success" dismissible>
Payment link generated successfully!
{sendEmail && ' Email sent to customer.'}
</Banner>
)}
{error && (
<Banner tone="critical" dismissible>
Failed to generate payment link. Please try again.
</Banner>
)}

<Section heading="Draft order information">
<BlockStack>
<Box>
<Text fontWeight="bold">Draft order: </Text>
<Text>{draftOrder.name}</Text>
</Box>

{draftOrder.customer && (
<Box>
<Text fontWeight="bold">Customer: </Text>
<Text>{draftOrder.customer.firstName} {draftOrder.customer.lastName}</Text>
<Text tone="subdued"> ({draftOrder.customer.email})</Text>
</Box>
)}

<Box>
<Text fontWeight="bold">Total: </Text>
<Text>{draftOrder.totalPriceSet.presentmentMoney.currencyCode} {draftOrder.totalPriceSet.presentmentMoney.amount}</Text>
</Box>
</BlockStack>
</Section>

<Section heading="Payment link options">
<BlockStack>
<Checkbox
checked={sendEmail}
onChange={setSendEmail}
disabled={!draftOrder.customer?.email}
>
Send payment link to customer via email
</Checkbox>
{!draftOrder.customer?.email && (
<Text tone="subdued">No customer email available</Text>
)}
</BlockStack>
</Section>

{paymentLink && (
<Section heading="Payment link">
<BlockStack>
<TextField
label="Payment URL"
value={paymentLink}
readOnly
/>
<Button
onPress={() => navigator.clipboard.writeText(paymentLink)}
variant="secondary"
>
Copy to clipboard
</Button>
</BlockStack>
</Section>
)}
</AdminAction>
);
}

Anchor to Draft order details action (should render) ,[object Object]Draft order details action (should render) target

admin.draft-order-details.action.should-render

Controls the render state of an admin action extension on the draft order details page. Use this target to conditionally show or hide your action extension based on the draft order's properties, such as status, customer type, or total amount.

This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension is evaluated each time the page loads.

Support
Components (0)
APIs (1)

Supported components

-

Available APIs

Examples
import {extension} from '@shopify/ui-extensions/admin';

export default extension(
'admin.draft-order-details.action.should-render',
async (root, api) => {
const draftOrderId = api.data.selected[0].id;

try {
// Fetch draft order customer information
const {data} = await api.query(
`
query GetDraftOrder($id: ID!) {
draftOrder(id: $id) {
customer {
id
}
}
}
`,
{variables: {id: draftOrderId}}
);

// Only show action if draft order has a customer
return {render: !!data.draftOrder.customer};
} catch (err) {
console.error('Error fetching draft order:', err);
return {render: false};
}
}
);

admin.draft-order-details.block.render

Renders an admin block extension inline on the draft order details page. Use this target to display contextual information, validation status, external system data, or payment status related to the draft order without requiring merchants to open a modal.

Extensions at this target appear as cards on the page and can show real-time data, insights, or quick actions. Blocks provide persistent visibility and are ideal for displaying information merchants need to see at a glance.

Examples
import React from 'react';
import {useState, useEffect} from 'react';
import {
reactExtension,
useApi,
AdminBlock,
BlockStack,
Box,
Badge,
Heading,
Text,
Divider,
Button,
ProgressIndicator,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.draft-order-details.block.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const {data} = useApi(TARGET);
const [orderStatus, setOrderStatus] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchOrderStatus = async () => {
const draftOrderId = data.selected[0].id;

try {
// Fetch order status from your app's backend
const response = await fetch(
`https://your-app.com/api/draft-order-status?draftOrderId=${draftOrderId}`
);
const statusData = await response.json();
setOrderStatus(statusData);
} catch (err) {
console.error('Error fetching order status:', err);
} finally {
setLoading(false);
}
};

fetchOrderStatus();
}, [data]);

if (loading) {
return (
<AdminBlock title="External Order Status">
<BlockStack>
<ProgressIndicator size="small-100" />
<Text>Loading order status...</Text>
</BlockStack>
</AdminBlock>
);
}

if (!orderStatus) {
return (
<AdminBlock title="External Order Status">
<Text tone="subdued">Not synced with external system</Text>
</AdminBlock>
);
}

return (
<AdminBlock title="External Order Status">
<BlockStack>
<Box>
<Heading>Sync Status</Heading>
<Badge tone={orderStatus.synced ? 'success' : 'warning'}>
{orderStatus.synced ? 'Synced' : 'Pending'}
</Badge>
</Box>

<Divider />

<Box>
<Heading>External Order ID</Heading>
<Text>{orderStatus.externalOrderId || 'Not assigned'}</Text>
</Box>

<Divider />

<Box>
<Heading>Processing Status</Heading>
<Text>{orderStatus.processingStatus}</Text>
</Box>

{orderStatus.warehouse && (
<>
<Divider />
<Box>
<Heading>Assigned Warehouse</Heading>
<Text>{orderStatus.warehouse}</Text>
</Box>
</>
)}

{orderStatus.lastSyncedAt && (
<>
<Divider />
<Box>
<Heading>Last Synced</Heading>
<Text tone="subdued">{orderStatus.lastSyncedAt}</Text>
</Box>
</>
)}

{orderStatus.externalUrl && (
<Button
onPress={() => window.open(orderStatus.externalUrl, '_blank')}
variant="secondary"
>
View in external system
</Button>
)}
</BlockStack>
</AdminBlock>
);
}

Anchor to Draft order index targetsDraft order index targets

Use action targets to extend the draft order index page with bulk operations and workflows that help merchants manage multiple draft orders efficiently.

admin.draft-order-index.action.render

Renders an admin action extension on the draft order index page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on the draft order list, such as batch processing, bulk exports, or generating reports.

Extensions at this target can access the page context through the Action Extension API. The action renders in a modal overlay, providing space for configuration and execution of list-wide operations.

Examples
import React from 'react';
import {useState} from 'react';
import {
reactExtension,
useApi,
AdminAction,
Banner,
Section,
BlockStack,
Select,
Text,
Button,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.draft-order-index.action.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const {close} = useApi(TARGET);
const [loading, setLoading] = useState(false);
const [exportType, setExportType] = useState('all');
const [dateRange, setDateRange] = useState('7days');
const [success, setSuccess] = useState(false);
const [error, setError] = useState(false);

const exportTypes = [
{value: 'all', label: 'All draft orders'},
{value: 'open', label: 'Open draft orders only'},
{value: 'completed', label: 'Completed draft orders only'},
{value: 'invoiced', label: 'Invoiced draft orders only'},
];

const dateRanges = [
{value: '7days', label: 'Last 7 days'},
{value: '30days', label: 'Last 30 days'},
{value: '90days', label: 'Last 90 days'},
{value: '1year', label: 'Last year'},
{value: 'all', label: 'All time'},
];

const handleExport = async () => {
setLoading(true);
setSuccess(false);
setError(false);

try {
// Export draft orders through your app's backend
const response = await fetch('https://your-app.com/api/export-draft-orders', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
exportType,
dateRange,
}),
});

if (response.ok) {
setSuccess(true);
close();
} else {
setError(true);
}
} catch (err) {
setError(true);
} finally {
setLoading(false);
}
};

return (
<AdminAction
title="Export to ERP System"
primaryAction={
<Button
onPress={handleExport}
disabled={loading || success}
>
{loading ? 'Starting Export...' : 'Start Export'}
</Button>
}
secondaryAction={
<Button onPress={close}>
Cancel
</Button>
}
>
{success && (
<Banner tone="success" dismissible>
Draft order export initiated! You'll receive an email when complete.
</Banner>
)}
{error && (
<Banner tone="critical" dismissible>
Failed to initiate export. Please try again.
</Banner>
)}

<Section heading="Export settings">
<BlockStack>
<Select
label="Export type"
value={exportType}
onChange={setExportType}
options={exportTypes}
/>

<Select
label="Date range"
value={dateRange}
onChange={setDateRange}
options={dateRanges}
/>

<Banner tone="info">
This will export all matching draft orders to your ERP system. Large exports may take several minutes to complete.
</Banner>
</BlockStack>
</Section>
</AdminAction>
);
}

Anchor to Draft order index action (should render) ,[object Object]Draft order index action (should render) target

admin.draft-order-index.action.should-render

Controls the render state of an admin action extension on the draft order index page. Use this target to conditionally show or hide your action extension based on business logic, user permissions, or app configuration.

This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension is evaluated each time the page loads.

Support
Components (0)
APIs (1)

Supported components

-

Available APIs

Examples
import {extension} from '@shopify/ui-extensions/admin';

export default extension(
'admin.draft-order-index.action.should-render',
async (root, api) => {
try {
// Check if app is configured through your backend
const response = await fetch(
'https://your-app.com/api/check-configuration'
);
const {configured, erpConnected} = await response.json();

// Only show action if app is configured and ERP is connected
return {render: configured && erpConnected};
} catch (err) {
console.error('Error checking configuration:', err);
return {render: false};
}
}
);

Anchor to Draft order index selection action ,[object Object]Draft order index selection action target

admin.draft-order-index.selection-action.render

Renders a selection action extension on the draft order index page when multiple draft orders are selected. Merchants can access this extension from the More actions menu of the resource list. Use this target to provide bulk operations on selected draft orders, such as bulk conversion, batch invoice generation, or bulk status updates.

Extensions at this target can access the IDs of selected draft orders through the data property in the Action Extension API. The action renders in a modal overlay designed for batch processing.

Examples
import React from 'react';
import {useState, useEffect} from 'react';
import {
reactExtension,
useApi,
AdminAction,
Banner,
Section,
BlockStack,
Box,
Select,
Text,
Button,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.draft-order-index.selection-action.render';

export default reactExtension(TARGET, () => <App />);

function App() {
const {data, close} = useApi(TARGET);
const [loading, setLoading] = useState(false);
const [draftOrderCount, setDraftOrderCount] = useState(0);
const [sendMethod, setSendMethod] = useState('email');
const [emailTemplate, setEmailTemplate] = useState('default');
const [success, setSuccess] = useState(false);
const [error, setError] = useState(false);
const [eligibleCount, setEligibleCount] = useState(0);

const sendMethods = [
{value: 'email', label: 'Email'},
{value: 'sms', label: 'SMS (if available)'},
];

const emailTemplates = [
{value: 'default', label: 'Default payment request'},
{value: 'friendly', label: 'Friendly reminder'},
{value: 'professional', label: 'Professional invoice'},
{value: 'urgent', label: 'Urgent payment request'},
];

useEffect(() => {
const checkEligibility = async () => {
const selectedDraftOrders = data.selected || [];
setDraftOrderCount(selectedDraftOrders.length);

// Check how many draft orders have customers with email addresses
try {
const draftOrderIds = selectedDraftOrders.map((draftOrder) => draftOrder.id);
const response = await fetch('https://your-app.com/api/check-payment-eligibility', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({draftOrderIds}),
});
const {eligibleCount: count} = await response.json();
setEligibleCount(count);
} catch (err) {
console.error('Error checking eligibility:', err);
}
};

checkEligibility();
}, [data]);

const handleSend = async () => {
setLoading(true);
setSuccess(false);
setError(false);
const draftOrderIds = data.selected.map((draftOrder) => draftOrder.id);

try {
// Send payment requests through your app's backend
const response = await fetch('https://your-app.com/api/send-payment-requests', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
draftOrderIds,
sendMethod,
emailTemplate,
}),
});

if (response.ok) {
setSuccess(true);
close();
} else {
setError(true);
}
} catch (err) {
setError(true);
} finally {
setLoading(false);
}
};

return (
<AdminAction
title="Send Payment Requests"
primaryAction={
<Button
onPress={handleSend}
disabled={loading || success || eligibleCount === 0}
>
{loading ? 'Sending...' : `Send ${eligibleCount} Payment Requests`}
</Button>
}
secondaryAction={
<Button onPress={close}>
Cancel
</Button>
}
>
{success && (
<Banner tone="success" dismissible>
Payment requests sent successfully to {eligibleCount} customers!
</Banner>
)}
{error && (
<Banner tone="critical" dismissible>
Failed to send payment requests. Please try again.
</Banner>
)}

<Section heading="Payment request settings">
<BlockStack>
<Box>
<Text fontWeight="bold">Selected draft orders: </Text>
<Text>{draftOrderCount}</Text>
</Box>

<Box>
<Text fontWeight="bold">Eligible for payment requests: </Text>
<Text>{eligibleCount}</Text>
<Text tone="subdued"> (with customer email)</Text>
</Box>

<Select
label="Delivery method"
value={sendMethod}
onChange={setSendMethod}
options={sendMethods}
/>

<Select
label="Email template"
value={emailTemplate}
onChange={setEmailTemplate}
options={emailTemplates}
/>

{eligibleCount < draftOrderCount && (
<Banner tone="warning">
{draftOrderCount - eligibleCount} draft orders will be skipped (missing customer email).
</Banner>
)}
</BlockStack>
</Section>
</AdminAction>
);
}

Anchor to Draft order index selection action (should render) ,[object Object]Draft order index selection action (should render) target

admin.draft-order-index.selection-action.should-render

Controls the render state of a selection action extension on the draft order index page when multiple draft orders are selected. Use this target to conditionally show or hide your bulk action extension based on the number of selected draft orders, their properties, or app configuration.

This target returns a boolean value that determines whether the corresponding selection action extension appears in the More actions menu. The extension is evaluated each time the selection changes.

Support
Components (0)
APIs (1)

Supported components

-

Available APIs

Examples
import {extension} from '@shopify/ui-extensions/admin';

export default extension(
'admin.draft-order-index.selection-action.should-render',
async (root, api) => {
const selectedCount = api.data.selected?.length || 0;

// Only show action if between 1 and 50 draft orders are selected
return {render: selectedCount > 0 && selectedCount <= 50};
}
);

  • Handle draft order states properly: Draft orders can be in different states (open, invoice sent, completed). Always check the draft order status before performing operations, and provide clear feedback when operations aren't applicable to certain states.
  • Validate customer information: Many draft order workflows require customer information. Always validate that required customer data (for example, email and address) exists before attempting operations like sending payment links or converting to orders.
  • Provide clear conversion workflows: When building workflows that convert draft orders to orders, provide clear confirmation steps and explain what will happen. Draft order conversion is a significant action that merchants need to understand.
  • Respect payment status: Be mindful of existing payment requests and payment status. Avoid sending duplicate payment requests or conflicting payment workflows that could confuse customers.

  • Single target per module: Each [[extensions.targeting]] entry in your TOML configuration maps one target to one module file.
  • Data retention: Draft orders created on or after April 1, 2025 are automatically purged after one year of inactivity.
  • Block target visibility: Block extensions must be manually added and pinned by merchants before they appear.
  • Block collapse behavior: Returning null from a block extension collapses the block rather than removing it from the page. Blocks can't be fully hidden at runtime.

Was this page helpful?