# Stripe and draft mechanics

Use these request shapes as a starting point, not as a substitute for checking the current Stripe and mail-provider schemas. Every credential and record ID below is a placeholder. Keep customer data and receipts in private storage.

## Paginate canceled subscriptions

Stripe's `GET https://api.stripe.com/v1/subscriptions/search` accepts `query=status:"canceled"`, `limit=100`, and a `page` token returned as `next_page`. Filter `canceled_at` after fetching. For changes too fresh for search indexing, reconcile against subscription events or a direct customer read.

```typescript
const auth = 'Bearer <stripe-credential-from-secret-store>';
async function stripeGet(path: string): Promise<any> {
  const r = await fetch(`https://api.stripe.com${path}`, {
    headers: { Authorization: auth }
  });
  if (!r.ok) throw new Error(`Stripe ${r.status}: ${await r.text()}`);
  return r.json();
}

async function canceledBetween(startISO: string, endISO: string): Promise<any[]> {
  const start = Date.parse(startISO) / 1000;
  const end = Date.parse(endISO) / 1000;
  const out: any[] = [];
  let page: string | undefined;
  for (;;) {
    const q = new URLSearchParams({ query: 'status:"canceled"', limit: '100' });
    if (page) q.set('page', page);
    const data = await stripeGet(`/v1/subscriptions/search?${q}`);
    out.push(...(data.data ?? []).filter((s: any) =>
      s.canceled_at && s.canceled_at >= start && s.canceled_at < end));
    if (!data.has_more) break;
    if (!data.next_page || data.next_page === page) throw new Error('Missing or repeated search cursor');
    page = data.next_page;
  }
  return out;
}
```

When Sauna's authentication proxy injects Stripe credentials, omit the explicit Authorization header and pass the approved connection through your execution tool instead. In a direct client, load the credential at runtime from a secret manager, never a published skill.

## Verify the whole customer

`GET /v1/subscriptions?customer=<customer-id>&status=all&limit=100` is a different endpoint from canceled-subscription search. Page until `has_more` is false. A sample that reads only the first few subscriptions can miss a fresh active one.

```typescript
async function hasLiveSubscription(customerId: string): Promise<boolean> {
  let cursor: string | undefined;
  for (;;) {
    const q = new URLSearchParams({ customer: customerId, status: 'all', limit: '100' });
    if (cursor) q.set('starting_after', cursor);
    const data = await stripeGet(`/v1/subscriptions?${q}`);
    if ((data.data ?? []).some((s: any) => ['active', 'trialing', 'past_due'].includes(s.status))) return true;
    if (!data.has_more) return false;
    const last = data.data?.at(-1)?.id;
    if (!last || last === cursor) throw new Error('Missing or repeated subscription cursor');
    cursor = last;
  }
}
```

For paid-history segmentation, page `GET /v1/invoices?customer=<customer-id>&limit=100`, inspect invoices with `status=paid` and positive `amount_paid`, and match the canceled subscription where the API version exposes its association. `items.data[].price.unit_amount` is a catalog price, not proof of payment. If invoice association is uncertain, mark for manual review instead of assuming paid.

## Draft and receipt shapes

If using Superhuman MCP, inspect its live schema, then create a `new` HTML draft in an authorized work mailbox. Its `to` parameter is an array even for one recipient.

```bash
mcp call 'https://mcp.mail.superhuman.com/mcp.create_or_update_draft' \
  type=new \
  'to=["<recipient-email>"]' \
  'cc=[]' 'bcc=[]' \
  send_as_email='<your-work-email>' \
  subject='Checking in' \
  body='<p>Hey &lt;first-name&gt;,</p><p>What were you hoping to do? Where did it fall over?</p>'
```

Capture the returned `draft_id`, `thread_id`, and `open_url`, then append one JSON line per result. Never log real addresses or message bodies into public files.

```bash
jq -nc --arg draft '<returned-draft-id>' \
  --arg thread '<returned-thread-id>' \
  --arg url '<returned-open-url>' \
  '{draft_id:$draft,thread_id:$thread,open_url:$url}' \
  >> session/recovery-draft-receipts.ndjson
```

Check the created draft with the provider's draft-read path. For replies, use the last incoming message as the anchor; for new notes, use `type=new`. If a draft tool requires both draft and thread identifiers for updates, retain both. Nothing here authorizes sending.