> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dispoiq.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Create your first deal page

> Create a customizable listing page with photos, pricing, and deal details to share with investors.

export const WorkflowDemo = ({workflow = 'map-search', clip = 'map-address', caption = ''}) => {
  const labels = {
    'map-address': 'Select a property address',
    'map-filters': 'Adjust the search radius',
    'map-result': 'Open a property result',
    'deal-details': 'Enter the property address',
    'deal-publish': 'Save and publish a prepared deal',
    'deal-buyer-copy': 'Review the listing content',
    'import-upload': 'Choose a CSV file',
    'import-map': 'Map the phone column',
    'import-start': 'Start the contact import',
    'import-report': 'Review the import report'
  };
  const title = labels[clip] || 'DispoIQ demonstration';
  const host = useRef(null);
  const viewport = useRef(null);
  const frame = useRef(null);
  const inView = useRef(false);
  const active = useRef(false);
  const wanted = useRef(false);
  const [visible, setVisible] = useState(false);
  const [motion, setMotion] = useState(null);
  const [paused, setPaused] = useState(false);
  const [ready, setReady] = useState(false);
  const [html, setHtml] = useState('');
  const [error, setError] = useState('');
  const [attempt, setAttempt] = useState(0);
  const [dimensions, setDimensions] = useState({
    width: 1320,
    height: 748
  });
  const [width, setWidth] = useState(720);
  const scale = Math.min(1, Math.max(1, width) / dimensions.width);
  const send = useCallback((command, extra = {}) => {
    frame.current?.contentWindow?.postMessage({
      source: 'dispoiq-docs-player',
      version: 1,
      demo: workflow,
      clip,
      command,
      ...extra
    }, window.location.origin);
  }, [workflow, clip]);
  useEffect(() => {
    const preference = window.matchMedia('(prefers-reduced-motion: reduce)');
    const change = () => {
      setMotion(preference.matches);
      setPaused(preference.matches);
    };
    change();
    preference.addEventListener('change', change);
    const observer = new IntersectionObserver(([entry]) => {
      inView.current = Boolean(entry?.isIntersecting);
      active.current = inView.current && !document.hidden;
      setVisible(inView.current);
      send('activity', {
        active: active.current
      });
      send(active.current && wanted.current ? 'play' : 'pause');
    }, {
      threshold: 0.15
    });
    if (host.current) observer.observe(host.current);
    const resize = new ResizeObserver(([entry]) => {
      if (entry) setWidth(entry.contentRect.width);
    });
    if (viewport.current) resize.observe(viewport.current);
    const visibility = () => {
      active.current = inView.current && !document.hidden;
      send('activity', {
        active: active.current
      });
      send(active.current && wanted.current ? 'play' : 'pause');
    };
    document.addEventListener('visibilitychange', visibility);
    return () => {
      observer.disconnect();
      resize.disconnect();
      preference.removeEventListener('change', change);
      document.removeEventListener('visibilitychange', visibility);
    };
  }, [send]);
  useEffect(() => {
    wanted.current = motion !== null && !paused;
    send('activity', {
      active: active.current
    });
    send(wanted.current && active.current ? 'play' : 'pause');
  }, [paused, motion, ready, send]);
  useEffect(() => {
    const receive = event => {
      const data = event.data;
      if (event.source !== frame.current?.contentWindow || event.origin !== window.location.origin) return;
      if (!data || data.source !== 'dispoiq-docs-demo' || data.version !== 1 || data.demo !== workflow || data.clip !== clip) return;
      if (data.type === 'error') {
        console.error('DispoIQ animation could not start:', String(data.message || 'Native scene error').replace(/(?:pk|sk)\.[A-Za-z0-9._-]+/g, '[token]').slice(0, 500));
        setError('This animation is unavailable. Follow the written instructions.');
        send('pause');
        return;
      }
      if (data.type !== 'ready' && data.type !== 'progress') return;
      if (data.type === 'ready') {
        setReady(true);
        setError('');
        send('activity', {
          active: active.current
        });
        if (wanted.current && active.current) send('play');
      }
    };
    window.addEventListener('message', receive);
    return () => window.removeEventListener('message', receive);
  }, [send, workflow, clip]);
  useEffect(() => {
    if (!visible || html || error || motion === null) return;
    let cancelled = false;
    const load = async () => {
      try {
        if (!window.__dispoiqInlineDemoPayload) {
          const promise = (async () => {
            const abort = new AbortController();
            const timeout = window.setTimeout(() => abort.abort(), 20000);
            try {
              const manifestResponse = await fetch('/demos/native/manifest.json', {
                signal: abort.signal,
                cache: 'no-cache'
              });
              if (!manifestResponse.ok) throw new Error('Manifest unavailable');
              const manifest = await manifestResponse.json();
              if (manifest.version !== 1 || manifest.payload !== 'runtime.json') throw new Error('Invalid manifest');
              const response = await fetch('/demos/native/runtime.json', {
                signal: abort.signal,
                cache: 'no-cache'
              });
              if (!response.ok) throw new Error('Runtime unavailable');
              const payload = await response.json();
              if (payload.version !== 1 || typeof payload.script !== 'string' || typeof payload.style !== 'string' || payload.script.length > 15000000 || payload.style.length > 2000000) throw new Error('Invalid runtime');
              return {
                manifest,
                payload
              };
            } finally {
              window.clearTimeout(timeout);
            }
          })();
          window.__dispoiqInlineDemoPayload = promise;
          promise.catch(() => {
            if (window.__dispoiqInlineDemoPayload === promise) delete window.__dispoiqInlineDemoPayload;
          });
        }
        const {manifest, payload} = await window.__dispoiqInlineDemoPayload;
        const definition = manifest.clips?.[clip];
        if (!definition || definition.workflow !== workflow || !labels[clip]) throw new Error('Invalid clip');
        const sceneWidth = Number(definition.width);
        const sceneHeight = Number(definition.height);
        if (!Number.isFinite(sceneWidth) || !Number.isFinite(sceneHeight) || sceneWidth < 320 || sceneWidth > 1600 || sceneHeight < 200 || sceneHeight > 1200) throw new Error('Invalid dimensions');
        if (cancelled) return;
        setDimensions({
          width: sceneWidth,
          height: sceneHeight
        });
        const origin = window.location.origin.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
        const script = payload.script.replace(/<\/script/gi, '<\\/script');
        const stylesheet = payload.style.replace(/<\/style/gi, '<\\/style');
        setHtml(`<!doctype html><html lang="en" data-demo="${workflow}" data-clip="${clip}" data-parent-origin="${origin}"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><base href="${origin}/demos/native/"/><style>${stylesheet}</style><style>html,body{margin:0;width:${sceneWidth}px;height:${sceneHeight}px;overflow:hidden}body{background:#fff}</style></head><body><div id="demo-root"></div><script type="module">${script}</script></body></html>`);
      } catch (failure) {
        if (!cancelled) console.error('DispoIQ animation could not load:', failure instanceof Error ? failure.message : 'Animation loading failed');
        if (!cancelled) setError('This animation is unavailable. Follow the written instructions.');
      }
    };
    load();
    return () => {
      cancelled = true;
    };
  }, [visible, motion, workflow, clip, html, error, attempt]);
  useEffect(() => {
    if (!html || ready || error) return;
    const timeout = window.setTimeout(() => {
      setError('This animation could not start. Follow the written instructions.');
      send('pause');
    }, 30000);
    return () => window.clearTimeout(timeout);
  }, [html, ready, error, send]);
  const retry = () => {
    setHtml('');
    setReady(false);
    setError('');
    setAttempt(value => value + 1);
  };
  return <figure ref={host} className="not-prose" aria-label={`${title} animation`} style={{
    margin: '20px 0 26px',
    maxWidth: '100%',
    minWidth: 0
  }}>
      <div ref={viewport} style={{
    width: '100%',
    aspectRatio: `${dimensions.width}/${dimensions.height}`,
    position: 'relative',
    overflow: 'hidden',
    borderRadius: 12,
    border: '1px solid #dbe4ef',
    background: '#f4f7fc',
    boxSizing: 'border-box'
  }}>
        {html && !error ? <iframe key={`${clip}-${attempt}`} ref={frame} title={`${title} using sample data`} srcDoc={html} sandbox="allow-scripts allow-same-origin" tabIndex={-1} aria-hidden="true" style={{
    display: 'block',
    border: 0,
    width: dimensions.width,
    height: dimensions.height,
    transform: `scale(${scale})`,
    transformOrigin: 'top left',
    pointerEvents: 'none',
    opacity: ready ? 1 : 0
  }} /> : <div role={error ? 'status' : undefined} style={{
    position: 'absolute',
    inset: 0,
    display: 'grid',
    placeContent: 'center',
    padding: 24,
    color: '#52657e',
    textAlign: 'center',
    fontSize: 14
  }}><span>{error || `Loading ${title.toLowerCase()}…`}</span>{error && <button type="button" onClick={retry} style={{
    marginTop: 12,
    padding: '8px 14px',
    background: '#fff',
    border: '1px solid #c9d7ea',
    borderRadius: 8,
    cursor: 'pointer'
  }}>Retry animation</button>}</div>}
        {html && !ready && !error && <div aria-live="polite" style={{
    position: 'absolute',
    inset: 0,
    display: 'grid',
    placeContent: 'center',
    padding: 24,
    color: '#52657e',
    fontSize: 14
  }}>Loading animation…</div>}
        {ready && !error && <button type="button" aria-label={`${paused ? 'Play' : 'Pause'} ${title.toLowerCase()} animation`} title={paused ? 'Play animation' : 'Pause animation'} aria-pressed={paused} onClick={() => setPaused(value => !value)} style={{
    position: 'absolute',
    left: 10,
    top: 10,
    width: 36,
    height: 36,
    display: 'grid',
    placeItems: 'center',
    border: '1px solid #d5deeb',
    borderRadius: 8,
    background: 'rgba(255,255,255,0.95)',
    color: '#18314f',
    cursor: 'pointer',
    zIndex: 2
  }}><span aria-hidden="true">{paused ? '▶' : 'Ⅱ'}</span></button>}
      </div>
      <figcaption style={{
    marginTop: 8,
    fontSize: 12,
    lineHeight: 1.6,
    color: '#64748b'
  }}>{caption || title}.{motion && paused && <span> Motion is paused; choose Play to watch.</span>}</figcaption>
    </figure>;
};

Use Deals to build your listing page, add the information buyers need, and review how it looks before publishing.

## Before you start

Have your property address, asking price, after-repair value (ARV), closing date, photos, and deal details ready.

## Create your deal

1. Open **Deals** and choose **Create deal**. Viewer accounts do not have this action.
2. In New deal, type the street number and name into **Property address**. Select the correct address from the suggestions. You can use the arrow keys and Enter to select a suggestion.
3. Choose **Next**. Under Deal terms, enter **Asking price** and **After-repair value** in whole dollars, then enter **Closing date** as `YYYY-MM-DD`. Both prices must be greater than zero.
4. Choose **Next**. Under Marketing, enter a **Deal headline**. Add an **Overview (optional)** if you have it ready.
5. Choose **Next** to Review. Check the address, prices, date, and headline. Use **Back** to correct an earlier step.
6. Choose **Create deal**. DispoIQ creates the draft and opens its editor.

Typing an address alone does not complete the property step. Choose a suggestion that includes the city, state, and ZIP.

## Add your deal details

Use the menu in the deal editor to add your property details, pricing, contract information, photos, and more.

<WorkflowDemo workflow="first-deal" clip="deal-details" caption="Enter the property address in Listing" />

| Section | What to review |
| - | - |
| **Listing** | Address, city, state, ZIP, neighborhood, beds, baths, heated square footage, year built, and tagline. |
| **Pricing** | Asking price and ARV; add rent figures and deal financials where relevant. |
| **Contract** | Contract and closing dates, inspection and assignment deadlines, walkthrough, occupancy, and title company. |
| **Condition** | Rate the roof, HVAC, plumbing, and cosmetic condition as Good, Fair, or Poor. Add condition notes, one per line. |
| **Listing content** | Add the property description, highlights, contact details, and instructions for interested buyers. |
| **Media** | Upload photos and supporting attachments. |
| **Location & comps** | Check the map coordinates and use **Add comp** to enter comparable sales. |

The header shows how many fields are still needed to publish. Section badges help you find those fields. Work through the missing property facts, prices, deadlines, title company, overview, and photos before the final save.

In **Listing content**, review the overview, highlights, next steps, and contact information buyers will see.

<WorkflowDemo workflow="first-deal" clip="deal-buyer-copy" />

## Add photos and attachments

In **Media**, choose **Upload photos** or drop files into the photo area. Supported formats are JPEG, PNG, HEIC, and WebP, up to 10 MB per photo.

Drag the tiles to reorder them. The first photo is marked Primary and supplies the listing thumbnail. Open a photo to add its caption or category.

Upload attachment accepts PDF, DOCX, and XLSX files, up to 20 MB each. Photo and attachment changes save separately from the main editor fields.

## Preview and publish your listing

Check the address, numbers, dates, copy, and photo order together. Preview opens the buyer page in a new tab and shows the last saved version. Unsaved editor changes do not appear there.

For a deal marked Not posted, Save and publish saves your changes and makes the deal Active. Publish when your listing is ready to share with investors. For other statuses, the action is Save and preserves the status selected in Listing.

<WorkflowDemo workflow="first-deal" clip="deal-publish" />

After saving, open **Preview** to review the saved version. Save your work before closing the editor. Discard returns the main form fields to their last saved values; it does not undo photo or attachment changes that already saved separately.

After sharing your listing page, use Deals to track buyer engagement.

## Troubleshooting

* **No matching address found:** keep typing a complete street address and select a suggestion.
* **Address lookup is unavailable:** try again before creating the deal. A manually typed address does not satisfy the wizard's address selection.
* **Deal not saved:** read the banner. If the draft was created but its details did not save, retry **Create deal**. The wizard reuses that draft. Cancel or closing the wizard takes you to its draft editor so you can finish there.
* **Save failed:** keep the editor open, check the error, and retry the save. Opening Preview will still show the last successfully saved version.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.