How to write a copywriting proposal and send it as a link

A copywriting proposal is one page that says what you will write, what it costs, how many revisions are included and what is not covered. Sent as a link, it can be adjusted for each client without resending files.

A copywriting proposal is a short offer: the pieces you will write, the price of each, how many rounds of changes are included, what is left out, and when it is done.

Put that on one page at a link, and your email only needs three lines.

"How do I write a quotation mail to client?" is a question asked on Quora. A quotation email has to carry a lot: prices in the body, scope in an attachment, and a slightly different version for every client.

Here is a sample page. Tick the add-ons and the total changes. The names and numbers are made up, so replace them with your own.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Copy proposal</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #eef1f0; color: #1d2625; line-height: 1.5; }
  .page { max-width: 640px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 22px 20px 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, .08); }
  .sample { display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: .6px; color: #8a4b00; background: #ffe9c7; border-radius: 4px; padding: 1px 7px; }
  h1 { margin: 8px 0 2px; font-size: 22px; line-height: 1.25; }
  .for { margin: 0; color: #5b6967; font-size: 14px; }
  .top { display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: baseline; margin: 16px 0 6px; padding: 14px 16px; background: #e8f4f1; border-radius: 10px; }
  .total { font-size: 30px; font-weight: 800; color: #0b5d51; line-height: 1; }
  .total small { display: block; font-size: 12px; font-weight: 600; color: #4b6a65; margin-bottom: 4px; }
  .meta { font-size: 14px; }
  .meta b { display: block; font-size: 12px; color: #4b6a65; font-weight: 600; }
  h2 { font-size: 14px; margin: 16px 0 4px; text-transform: uppercase; letter-spacing: .5px; color: #0b5d51; }
  .row { display: flex; justify-content: space-between; gap: 12px; padding: 7px 0; border-bottom: 1px solid #e6ebea; font-size: 15px; }
  .row .d { display: block; font-size: 13px; color: #5b6967; }
  .row .p { white-space: nowrap; font-weight: 700; }
  label.row { cursor: pointer; align-items: flex-start; }
  label.row .l { display: flex; gap: 10px; align-items: flex-start; }
  label.row input { margin-top: 5px; width: 18px; height: 18px; accent-color: #0b5d51; }
  .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0 22px; }
  ul { margin: 0; padding-left: 18px; font-size: 14px; }
  li { margin: 3px 0; }
  .next { margin-top: 16px; padding: 12px 14px; border-left: 4px solid #0b5d51; background: #f4f8f7; font-size: 14px; }
  @media (max-width: 520px) {
    body { padding: 8px; }
    .page { padding: 18px 14px; }
    .cols { gap: 0 14px; }
    h1 { font-size: 20px; }
  }
</style>
</head>
<body>
<div class="page">
  <span class="sample">SAMPLE - replace every name and number</span>
  <h1>Website copy: home page, product pages and welcome emails</h1>
  <p class="for">Prepared for Brightwell Tea Co. by Juno Marsh, freelance copywriter</p>

  <div class="top">
    <div class="total"><small>Total</small><span id="total">$0</span></div>
    <div class="meta"><b>Delivery</b>12 working days from deposit</div>
    <div class="meta"><b>Valid until</b>31 October 2026</div>
  </div>

  <h2>What you get</h2>
  <div id="items"></div>

  <h2>Optional add-ons</h2>
  <div id="addons"></div>

  <div class="cols">
    <div>
      <h2>Revisions</h2>
      <ul>
        <li>2 rounds of changes on each piece</li>
        <li>Extra rounds are an add-on</li>
      </ul>
    </div>
    <div>
      <h2>Not included</h2>
      <ul>
        <li>Photography and design</li>
        <li>Keyword research</li>
        <li>Translation</li>
      </ul>
    </div>
  </div>

  <div class="next"><b>Next step:</b> reply to my email with "Yes" and the add-ons you want. I will send the deposit details and a start date.</div>
</div>

<script>
  // Change these lists for each client. The page rebuilds itself.
  const items = [
    { name: 'Home page copy', detail: 'Headline, intro, three benefit blocks, call to action', price: 450 },
    { name: 'Product pages (up to 4)', detail: 'About 150 words each, plus a one-line tasting note', price: 520 },
    { name: 'Welcome email series', detail: '3 emails, 120 to 180 words each', price: 360 }
  ];
  const addons = [
    { name: 'About page', detail: 'Around 300 words', price: 280 },
    { name: 'Rush delivery', detail: 'Everything in 6 working days', price: 200 },
    { name: 'One extra revision round', detail: 'Across all pieces', price: 150 }
  ];

  const money = (n) => '$' + n.toLocaleString('en-US');
  const row = (x) => '<div class="row"><span>' + x.name + '<span class="d">' + x.detail + '</span></span><span class="p">' + money(x.price) + '</span></div>';

  document.getElementById('items').innerHTML = items.map(row).join('');
  document.getElementById('addons').innerHTML = addons.map((x, i) =>
    '<label class="row"><span class="l"><input type="checkbox" data-i="' + i + '"><span>' + x.name + '<span class="d">' + x.detail + '</span></span></span><span class="p">+ ' + money(x.price) + '</span></label>'
  ).join('');

  function update() {
    let sum = items.reduce((s, x) => s + x.price, 0);
    document.querySelectorAll('#addons input:checked').forEach((c) => { sum += addons[c.dataset.i].price; });
    document.getElementById('total').textContent = money(sum);
  }
  document.getElementById('addons').addEventListener('change', update);
  update();
</script>
</body>
</html>
A sample copywriting proposal. Total, delivery and validity first, then deliverables, add-ons, revisions and exclusions.

What the page needs

Five things make a proposal easy to say yes to. If one is missing, the client will write back to ask for it.

  • Deliverables with counts and lengths. "Four product pages, about 150 words each" is clear. "Website copy" is not.
  • A price for each, and the total. Show the total at the top.
  • Revisions. Say how many rounds are included and what an extra one costs.
  • What is not included. Photography, keyword research and translation are common ones. Naming them keeps the price honest.
  • Timeline, validity and next step. Say how long the offer stands and what the client does to accept.

The first screen matters, because the client may open the link on a phone.

Two phone screens. One starts with background, the other starts with the total, the delivery time and the scope.
Two phone screens. One starts with background, the other starts with the total, the delivery time and the scope.

For the general case of any kind of proposal, see proposal link. For a one-line price, quote link is the shorter form.

Make it with AI

An AI can lay out the page from your notes. It will also fill gaps with believable prices and terms, so the prompt has to forbid that. Paste this into ChatGPT, Claude or Gemini, then fill in the facts at the bottom.

Write ONE self-contained HTML file: a copywriting proposal for a client.

Rules:
- Use only the facts below. Do not invent prices, dates, deliverables or terms.
- If something is missing, write [ADD: what is missing] instead of guessing.
- Top of the page: what I will write, the total price, the delivery time and the valid-until date.
- Then: deliverables (name, what is included, price), optional add-ons with prices, revision rounds included, what is NOT included, next step.
- Put the deliverables and prices in lists at the top of the script, and add up the total with JavaScript, so I can change them for the next client.
- Plain HTML and CSS, a little JavaScript is fine. It must read well on a phone.
- No external images, scripts or forms. Under 350 words.

Facts:
Client name:
Project:
Deliverables and prices:
Optional add-ons and prices:
Revision rounds included:
Not included:
Delivery time:
Valid until:
How the client accepts:

Then check the result before you send it:

  • Total. Add it up yourself, with and without each add-on. Do not trust the AI's arithmetic.
  • [ADD] marks. Fill each one with a fact you can stand behind, or delete the line.
  • Terms. Revision rounds, deposit and dates must match what you really offer.
  • Names. No other client's name, and no details about other work.
  • Phone. Open it narrow, or ask for a viewport meta tag. See viewport meta tag.

A proposal attached to an email is a copy. If a price changes, you send a second file and hope the client opens the right one. A link points at one page that you can still fix.

From your notes to an AI draft, your check, and a share link in your email.
From your notes to an AI draft, your check, and a share link in your email.
  1. Copy the whole HTML from the chat. Saving HTML from ChatGPT shows how.
  2. Paste it into a NOS document. The page renders as written, and the script that adds up the total runs.
  3. Choose Create share link. HTML to link walks through it.
  4. Open the link on your phone once, as the client will.
  5. Paste it into your email, with the total and the next step in the text.

Whoever opens the link sees the page in a browser, on a phone or a laptop, with no account to make. They cannot edit it. If you fix a line later, you edit the code and the same link shows the new version.

A draft email to paste around the link:

Subject: Copy proposal for [client name]

Hi [name],

Here is the proposal: scope, price and timeline on one page. The total is [total], valid until [date].
[link]

Reply "Yes" with any add-ons and I will send the deposit details.

As of October 2026, Gmail Help says to add a link on a computer by highlighting the text, pressing Ctrl + K, pasting the URL and clicking Apply. Other mail programs differ, so use whatever your own one offers.

Adjust it for each client

Most of the page is the same for every client: your terms, your exclusions, your wording. What changes is the name, the line items and the prices.

One master page, three clients. Only the lines marked orange change.
One master page, three clients. Only the lines marked orange change.

Keep one master version of the HTML in a file you control.

For each new client, paste the master into a new NOS document, change the name and the lists at the top of the script, and create a share link for that document. Each client gets their own address.

Anyone with a link can open the page, so never paste another client's details into it. Search for the old client's name before you share.

If you also publish a general price list, give it its own page.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rate sheet</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f1efea; color: #26221c; line-height: 1.45; }
  .card { max-width: 600px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 18px 18px 16px; box-shadow: 0 2px 10px rgba(0, 0, 0, .08); }
  .sample { display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: .6px; color: #8a4b00; background: #ffe9c7; border-radius: 4px; padding: 1px 7px; }
  h1 { margin: 8px 0 2px; font-size: 20px; }
  .sub { margin: 0 0 10px; font-size: 14px; color: #6a6358; }
  .line { display: grid; grid-template-columns: 1fr auto 74px; gap: 10px; align-items: center; padding: 9px 0; border-bottom: 1px solid #ece8e0; }
  .name { font-size: 15px; }
  .name small { display: block; color: #6a6358; font-size: 13px; }
  .step { display: flex; align-items: center; gap: 6px; }
  .step button { width: 34px; height: 34px; border: 1px solid #cfc8bb; border-radius: 8px; background: #faf8f4; font-size: 18px; line-height: 1; cursor: pointer; }
  .step output { min-width: 22px; text-align: center; font-weight: 700; }
  .sub-t { text-align: right; font-weight: 700; font-size: 15px; }
  .rush { display: flex; gap: 10px; align-items: center; padding: 11px 0; font-size: 15px; cursor: pointer; }
  .rush input { width: 18px; height: 18px; accent-color: #9a4f12; }
  .sum { display: flex; justify-content: space-between; align-items: baseline; margin-top: 4px; padding-top: 10px; border-top: 2px solid #26221c; font-weight: 800; font-size: 22px; }
  .note { margin: 10px 0 0; font-size: 13px; color: #6a6358; }
</style>
</head>
<body>
<div class="card">
  <span class="sample">SAMPLE - use your own rates</span>
  <h1>Copywriting rates</h1>
  <p class="sub">Choose how much you need for a quick estimate.</p>
  <div id="lines"></div>
  <label class="rush"><input type="checkbox" id="rush"> Rush delivery (+25%)</label>
  <div class="sum"><span>Estimate</span><span id="total">$0</span></div>
  <p class="note">An estimate, not a quote. The proposal I send you has the final price.</p>
</div>

<script>
  // Edit the rates here. Each line gets its own stepper.
  const rates = [
    { name: 'Landing page', unit: 'per page, up to 500 words', price: 400, qty: 1 },
    { name: 'Product description', unit: 'per item, up to 100 words', price: 35, qty: 0 },
    { name: 'Email', unit: 'per email, up to 200 words', price: 120, qty: 0 },
    { name: 'Blog post', unit: 'per post, about 800 words', price: 300, qty: 0 }
  ];
  const money = (n) => '$' + n.toLocaleString('en-US');
  const box = document.getElementById('lines');

  box.innerHTML = rates.map((r, i) =>
    '<div class="line"><div class="name">' + r.name + '<small>' + money(r.price) + ' ' + r.unit + '</small></div>' +
    '<div class="step"><button type="button" data-i="' + i + '" data-d="-1" aria-label="Fewer ' + r.name + '">-</button>' +
    '<output id="q' + i + '">' + r.qty + '</output>' +
    '<button type="button" data-i="' + i + '" data-d="1" aria-label="More ' + r.name + '">+</button></div>' +
    '<div class="sub-t" id="s' + i + '"></div></div>'
  ).join('');

  function update() {
    let sum = 0;
    rates.forEach((r, i) => {
      document.getElementById('q' + i).textContent = r.qty;
      document.getElementById('s' + i).textContent = money(r.qty * r.price);
      sum += r.qty * r.price;
    });
    if (document.getElementById('rush').checked) sum = Math.round(sum * 1.25);
    document.getElementById('total').textContent = money(sum);
  }

  box.addEventListener('click', (e) => {
    const b = e.target.closest('button');
    if (!b) return;
    const r = rates[b.dataset.i];
    r.qty = Math.max(0, Math.min(20, r.qty + Number(b.dataset.d)));
    update();
  });
  document.getElementById('rush').addEventListener('change', update);
  update();
</script>
</body>
</html>
A sample rate sheet. Change the quantities and the estimate updates. Edit the rates in the script for your own work.

A rate sheet gives a rough number before a conversation. The proposal gives the real one after it.

Ways to send a proposal compared

Price in the email body PDF or file attached Share link
Fixing a price after sending A new email A new file Edit the code, same link
Copies in circulation One per message or forward One per message or forward One page
Can the client edit it No, but they can quote and change a copy They hold their own copy No, view only
Add-ons that update the total No No Yes, if the page has a script
Account needed to open it No No No

When a price does change after the client has seen it, add a dated line at the top, such as "Revised 8 October: rush add-on now $200". Proposal link explains why a visible revision note keeps trust.

When it does not work

What you see Cause Fix
The total is wrong The AI added up badly, or you changed one price Add it up by hand and fix the lists
A logo or photo is missing Outside images are blocked Use text or an inline SVG
An Accept button sends nothing Sending a form to a server is blocked Ask the client to reply to your email
The text is tiny on a phone No viewport meta tag Ask the AI to add one
A font or script is missing It loads from a site that is not allowed Use Google Fonts, cdnjs, jsDelivr, unpkg, or nothing extra
The price in the email differs from the page You edited only one of them Keep the total in both, and change both together
The client says the price moved after saying yes The page was edited after they accepted Add a dated revision line, and confirm changes in writing
Another client's name shows up The master was copied after an edit Search for the old name before you share

Questions people ask

What should a copywriting proposal include?

The deliverables with counts and lengths, a price for each, the total, how many revision rounds are included, what is not included, the timeline, how long the offer is valid, and the next step. If the client cannot find the total and the next step on the first screen, move them up.

Should the price go in the email or on the page?

Put the total in both, so the client sees it without opening anything. Keep the detail on the page. If you change a price later, change it in both places, or say "see the page" in the email so there is only one number to keep right.

How do I adjust the proposal for each client?

Keep one master version of the HTML. For each client, paste it into a new NOS document, change the client name, the line items and the prices at the top of the script, and create a share link for that document.

Can the client change the prices on my page?

No. A NOS share link opens without an account and the visitor cannot edit the page. A client can tick add-ons on a page like the sample to see a new total, but that stays on their screen and is not saved.

What if the client wants a PDF or a signed document?

Send what they ask for. The link still helps for the conversation before it, because you can fix a line without sending a new file.

Keep reading