Trimline is in private beta. Public launch: June 2027.Join the waitlist →
Docs / Resources

Drafts & approvals

Every email Trimline sends starts as a draft. Review, edit, approve, hold or discard them.

Beta. The Trimline API is available to private beta customers. Endpoints may change before general availability in June 2027, and every breaking change is announced in the changelog. Request access.

A draft is an email we’ve written and that is waiting for a decision. Every message Trimline sends to a vendor starts as a draft. Unless you’ve turned on autopilot for that vendor, a draft is only sent once someone approves it, in the dashboard, from Slack, or through this API.

Endpoints

The draft object

{
  "id": "drf_3Nc81Ab",
  "object": "draft",
  "negotiation": "neg_8Hk2Pq",
  "renewal": "ren_1Pq8sT2v",
  "kind": "counter_offer",
  "status": "pending",
  "from": "sam@northwind.example",
  "to": ["priya@acmecrm.example"],
  "cc": [],
  "subject": "Re: Acme CRM renewal — Northwind Labs",
  "body": "Thanks Priya, that's helpful, and flat pricing for both years works for us. To sign this month we'd need the discount at 22%…",
  "rationale": "Vendor opened at 12%. Benchmarks for 25–50 seat accounts show 18–25% on 2-year terms. Countering at 22% leaves room to settle near 18%.",
  "expires_at": "2026-12-05T16:42:10Z",
  "approved_by": null,
  "sent_at": null,
  "created_at": "2026-12-03T16:44:02Z",
  "livemode": false
}
kindenumopening, follow_up, counter_offer, acceptance, notice or question.
statusenumpending, approved (queued to send), sent, held, discarded or expired. Once a draft is sent or discarded, its subject and body are erased from Trimline. The sent copy lives only in your mailbox.
rationalestringWhy we wrote what we wrote, in plain language. It’s never sent to the vendor.
expires_attimestamp, nullableAfter this time, the draft is stale, usually because the vendor’s offer has a deadline. Expired drafts are replaced with a fresh one.

List drafts

GET/v1/drafts

The most common call is status=pending, which returns everything waiting on your team.

curl -G https://api.trimline.tech/v1/drafts \
  -H "Authorization: Bearer $TRIMLINE_API_KEY" \
  -d "status=pending"

Edit a draft

PATCH/v1/drafts/:id

Change the subject, body, to or cc before approving. Edited drafts are marked edited: true, and we learn from your edits for future messages to this vendor.

Approve a draft

POST/v1/drafts/:id/approve

Queues the draft to send from the connected mailbox in from. Requires the drafts:write scope and an idempotency key.

approved_byrequiredstringThe email address of the person approving. It’s recorded in the audit log.
send_attimestampSchedule the send for later, for example during the recipient’s business hours.
curl -X POST https://api.trimline.tech/v1/drafts/drf_3Nc81Ab/approve \
  -H "Authorization: Bearer $TRIMLINE_API_KEY" \
  -H "Idempotency-Key: approve-drf_3Nc81Ab" \
  -H "Content-Type: application/json" \
  -d '{ "approved_by": "sam@northwind.example" }'
// e.g. from a Slack button handler
export async function approveDraft(draftId, approver) {
  const res = await fetch(`https://api.trimline.tech/v1/drafts/${draftId}/approve`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.TRIMLINE_API_KEY}`,
      "Idempotency-Key": `approve-${draftId}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ approved_by: approver }),
  });
  if (res.status === 409) return "already handled";
  if (!res.ok) throw new Error((await res.json()).error.message);
  return "sent";
}
def approve_draft(draft_id: str, approver: str) -> str:
    res = requests.post(
        f"https://api.trimline.tech/v1/drafts/{draft_id}/approve",
        headers={**HEADERS, "Idempotency-Key": f"approve-{draft_id}"},
        json={"approved_by": approver},
    )
    if res.status_code == 409:
        return "already handled"
    res.raise_for_status()
    return "sent"

Returns 409 draft_not_pending if the draft has already been dealt with, which is safe to ignore when you have several approval surfaces.

Hold or discard

POST /v1/drafts/:id/hold parks the draft without sending. The negotiation pauses until you approve it or ask for a new version. POST /v1/drafts/:id/discard throws it away. Pass "regenerate": true with optional instructions, such as "be warmer, mention we've been customers for 3 years", to get a replacement.

Something unclear or missing? Email developers@trimline.tech.