Sign in

SDK Invoices

The InvoiceClient issues, tracks, and pays invoices for a Studio engagement. It is available at client.invoices on any authenticated StudioClient.

Two audiences, one client

Which methods you may call depends on the credential on the parent StudioClient:

Method Audience Description
list(opts?)bothList invoices, filter by org/project/status
get(id)bothFetch a single invoice
summary(orgId)bothOutstanding / paid / overdue totals
create(data)providerCreate a DRAFT invoice
update(id, patch)providerEdit a DRAFT invoice
send(id)providerFinalize & send via Stripe
cancel(id)providerVoid a DRAFT or SENT invoice
payLink(id)clientStripe-hosted payment URL

Provider methods require a ysk_prov_* key. Client methods accept an org member session token. Authorization is enforced server-side by SpiceDB.

Types

type StudioInvoiceStatus =
  | 'DRAFT' | 'SENT' | 'PAID' | 'OVERDUE' | 'CANCELLED' | 'REFUNDED';

interface StudioLineItem {
  description: string;
  quantity: number;
  unitPriceCents: number;
  totalCents: number;        // server-computed
}

interface StudioInvoice {
  id: string;
  number: string;            // "STUDIO-2026-0042"
  organizationId: string;
  providerSlug: string;      // "capswan-studio"
  projectId?: string;
  description: string;
  lineItems: StudioLineItem[];
  amountCents: number;       // server-computed (sum of line totals)
  currency: string;          // "usd"
  status: StudioInvoiceStatus;
  hostedInvoiceUrl?: string; // Stripe pay page
  pdfUrl?: string;
  dueDate?: string;
  issuedAt?: string;
  paidAt?: string;
  notes?: string;
  createdAt: string;
}

interface InvoiceSummary {
  currency: string;
  outstandingCents: number;  // SENT + OVERDUE
  paidCents: number;
  draftCents: number;
  overdueCount: number;
  nextDueDate?: string;
}

Money is never computed on the client. On create/update you send only description, quantity, and unitPriceCents; totals are derived server-side and Stripe is the system of record.

Issue an invoice (provider)

import { StudioClient } from '@yapture/studio-sdk';

const studio = new StudioClient({
  apiKey: process.env.YAPTURE_STUDIO_API_KEY, // ysk_prov_capswan-studio
});

const invoice = await studio.invoices.create({
  organizationId: 'org_acme',
  description: 'March retainer — product design & frontend',
  lineItems: [
    { description: 'Design sprint',        quantity: 1,  unitPriceCents: 500000 },
    { description: 'Frontend dev (hours)', quantity: 40, unitPriceCents: 15000 },
  ],
  dueDate: '2026-04-15',
});

// Finalize and email the client a Stripe-hosted pay page
const sent = await studio.invoices.send(invoice.id);
console.log(sent.number, sent.status); // STUDIO-2026-0042 SENT

Show & pay invoices (client portal)

import { StudioClient } from '@yapture/studio-sdk';

// Constructed per-request with the signed-in org member's token
const studio = new StudioClient({ apiKey: session.accessToken });

const { data: invoices } = await studio.invoices.list({
  organizationId: 'org_acme',
});
const summary = await studio.invoices.summary('org_acme');

console.log(`Outstanding: $${(summary.outstandingCents / 100).toFixed(2)}`);

// "Pay" — open the Stripe-hosted page; the portal never sees card data
const { hostedInvoiceUrl } = await studio.invoices.payLink(invoices[0].id);
window.open(hostedInvoiceUrl, '_blank');

Lifecycle

DRAFT ──update──▶ DRAFT
DRAFT ──send────▶ SENT ──(stripe paid)──▶ PAID
                  SENT ──(past dueDate)──▶ OVERDUE ──(paid)──▶ PAID
DRAFT|SENT ──cancel──▶ CANCELLED
PAID ──(admin refund)──▶ REFUNDED

See the Invoicing guide for the full lifecycle and the Invoices API reference for raw endpoints.