Meshy API

Meshy's API, Credits and Test Mode

The API at https://api.meshy.ai/openapi/ 20,231 authenticates with Authorization: Bearer msy_... and is asynchronous: a POST returns a task ID, and you poll the task, stream it over Server-Sent Events or take a webhook until it is SUCCEEDED, FAILED or CANCELED. The documentation says it blocks browser (CORS) requests, but on 26 September 2026 it echoed a test page's origin in Access-Control-Allow-Origin: keep the key on a server anyway. Tasks cost prepaid credits: with Meshy 7.1 20,231 , a text-to-3D preview costs 20, a refine 10, an image-to-3D model with textures 30, a remesh 5 and a format conversion 1.

"Creating and managing API keys requires a paid plan." The Free plan's 100 monthly credits work in the web app and the Developer Playground, and its models are CC BY 4.0 (commercial use with attribution); paid plans make assets private and customer-owned, and API keys start with Pro (1,000 credits a month). In February 2025 Meshy added a test-mode key, msy_dummy_api_key_for_test_mode_12345678, that returned sample results for free. The current documentation no longer lists it, and on 26 September 2026 the API rejected it.

Meshy's asynchronous task states and what a month of Pro credits buysHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; background: #fafaf7; font: 11px system-ui, sans-serif; color: #263238; }
  svg { width: 100%; max-width: 600px; display: block; }
</style>
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<p><button id="go">Run a task</button> <button id="cancel">DELETE (cancel)</button>
  monthly credits <select id="plan"><option value="100">Free: 100 (web app only)</option><option value="1000" selected>Pro: 1,000</option></select></p>
<svg viewBox="0 0 600 290" font-size="11"></svg>
<p id="out"></p>
<script>
  const svg = d3.select('svg');
  const states = [['PENDING', 60, 60], ['IN_PROGRESS', 220, 60], ['SUCCEEDED', 400, 25], ['FAILED', 400, 70], ['CANCELED', 400, 115]];
  const pos = Object.fromEntries(states.map(([s, x, y]) => [s, [x, y]]));
  [['PENDING', 'IN_PROGRESS'], ['IN_PROGRESS', 'SUCCEEDED'], ['IN_PROGRESS', 'FAILED'], ['PENDING', 'CANCELED']].forEach(([a, b]) => {
    svg.append('line').attr('x1', pos[a][0] + 50).attr('y1', pos[a][1]).attr('x2', pos[b][0] - 50).attr('y2', pos[b][1]).attr('stroke', '#b0bec5');
  });
  const st = svg.selectAll('g.s').data(states).join('g').attr('class', 's').attr('transform', ([, x, y]) => `translate(${x},${y})`);
  st.append('rect').attr('x', -50).attr('y', -14).attr('width', 100).attr('height', 28).attr('rx', 14).attr('fill', '#fff').attr('stroke', '#90a4ae');
  st.append('text').attr('text-anchor', 'middle').attr('dy', '0.35em').text(([s]) => s);
  const dot = svg.append('circle').attr('r', 7).attr('fill', '#e09a10').attr('cx', 60).attr('cy', 60);

  // Credits per task (Meshy 7.1) and how many fit in a month
  const costs = [['text-to-3D preview', 20], ['refine (textures)', 10], ['image-to-3D + textures', 30], ['remesh', 5], ['format conversion', 1]];
  const y = d3.scaleBand(costs.map(c => c[0]), [160, 270]).padding(0.2), x = d3.scaleLinear([0, 30], [150, 330]);
  svg.append('text').attr('x', 0).attr('y', 150).attr('font-weight', 'bold').text('credits per task');
  const row = svg.selectAll('g.c').data(costs).join('g').attr('class', 'c').attr('transform', c => `translate(0,${y(c[0])})`);
  row.append('text').attr('x', 144).attr('y', y.bandwidth() / 2).attr('dy', '0.35em').attr('text-anchor', 'end').text(c => c[0]);
  row.append('rect').attr('x', 150).attr('height', y.bandwidth()).attr('width', c => x(c[1]) - 150).attr('fill', '#5b3f99');
  row.append('text').attr('x', c => x(c[1]) + 4).attr('y', y.bandwidth() / 2).attr('dy', '0.35em').text(c => c[1]);
  const month = svg.append('text').attr('x', 370).attr('y', 200).attr('font-size', 13);
  const month2 = svg.append('text').attr('x', 370).attr('y', 220);

  let timer = null;
  function move(state) { const [x, y] = pos[state]; dot.transition().duration(400).attr('cx', x).attr('cy', y); }
  function go() {
    timer?.stop();  move('PENDING');  d3.select('#out').text('POST → task id; polling GET /openapi/v2/text-to-3d/{id} every 5 s');
    let t = 0;
    timer = d3.interval(() => {
      t++;
      if (t === 2) move('IN_PROGRESS');
      if (t === 5) { move('SUCCEEDED');  d3.select('#out').text('SUCCEEDED: model_urls.glb is a signed URL; download it now (kept 3 days)');  timer.stop(); }
    }, 500);
  }
  d3.select('#go').on('click', go);
  d3.select('#cancel').on('click', () => {
    const [cx] = [+dot.attr('cx')];
    timer?.stop();
    if (cx < 100) { move('CANCELED');  d3.select('#out').text('Canceled while PENDING: credits refunded'); }
    else d3.select('#out').text('Already IN_PROGRESS (or done): a DELETE no longer refunds; set a monthly credit limit per key');
  });
  function plan() {
    const credits = +d3.select('#plan').property('value');
    month.text(`${Math.floor(credits / 30)} text-to-3D models (preview + refine)`);
    month2.text(credits === 100 ? 'Free plan: no API keys; models are CC BY 4.0' : 'Pro: API keys, private customer-owned assets');
  }
  d3.select('#plan').on('change', plan);
  plan();  go();
</script>