InvoiceStudio
On this page

Invoices as an API

One POST creates an invoice or quote and gives you back a total, a PDF, a UBL 2.1 e-invoice and a shareable link — no template engine, no PDF toolchain, no rounding bugs of your own. 250 documents a month are free, keys are issued instantly, and every event can be pushed to a signed webhook.

curl -X POST https://invoicestudio.co/api/v1/invoices \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"version":1,"type":"invoice","number":"INV-1001","issueDate":"2026-09-01","dueDate":"2026-09-15","currency":"EUR","from":"Acme GmbH\nHauptstrasse 1\n10115 Berlin","to":"Globex Ltd\n5 King Street\nLondon","items":[{"name":"Consulting","description":"September retainer","quantity":10,"unitCostCents":12000}],"tax":{"title":"VAT","kind":"percent","value":19},"notes":"Thank you for your business.","paymentLink":"https://buy.stripe.com/test_123"}'
OpenAPI spec

Pricing

Free

$0 / forever

250 documents / month

Everything in the API, no card. Enough to build and ship.

Starter

$19 / month

1,000 documents / month

For a live product with steady invoicing volume.

Growth

$49 / month

5,000 documents / month

For platforms invoicing on behalf of many customers.

A document is one invoice or quote created via POST /v1/invoices. PDF, UBL, reads and webhooks are unmetered. Hard cap — no overage bills. Need more than 5,000? Contact us.

Quick start

  1. 1. Get a key

    Create one in the developer dashboard. It is shown once — store it as INVOICESTUDIO_API_KEY.

  2. 2. Create an invoice

    The response carries the computed total and the PDF and UBL URLs.

    curl -X POST https://invoicestudio.co/api/v1/invoices \
      -H "Authorization: Bearer $INVOICESTUDIO_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{"version":1,"type":"invoice","number":"INV-1001","issueDate":"2026-09-01","dueDate":"2026-09-15","currency":"EUR","from":"Acme GmbH\nHauptstrasse 1\n10115 Berlin","to":"Globex Ltd\n5 King Street\nLondon","items":[{"name":"Consulting","description":"September retainer","quantity":10,"unitCostCents":12000}],"tax":{"title":"VAT","kind":"percent","value":19},"notes":"Thank you for your business.","paymentLink":"https://buy.stripe.com/test_123"}'
  3. 3. Download the PDF

    Rendered on demand, no extra quota.

    curl https://invoicestudio.co/api/v1/invoices/6f1c9b6e-0f2a-4a3a-9a12-2f4d8c9b1a55/pdf \
      -H "Authorization: Bearer $INVOICESTUDIO_API_KEY" \
      -o invoice.pdf
  4. 4. Share it

    Publishes a public link and returns the invoice with share_url set.

    curl -X POST https://invoicestudio.co/api/v1/invoices/6f1c9b6e-0f2a-4a3a-9a12-2f4d8c9b1a55/share \
      -H "Authorization: Bearer $INVOICESTUDIO_API_KEY"

Authentication

  • Send your key as Authorization: Bearer is_live_… on every request. There are no cookies and no OAuth.
  • Keys are shown once at creation and stored only as a hash — we cannot recover one for you.
  • Revoke a key at any time from the dashboard; it stops working immediately.
  • Every response — errors included — carries an x-request-id. Quote it when you contact support.

Errors

Every non-2xx response uses the same envelope. error.code maps 1:1 to the HTTP status, so branch on the code, not the message.

{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_request",
    "message": "items[0].quantity: must be a number between 0 and 1000000.",
    "param": "items[0].quantity",
    "request_id": "req_9tQ0Zg2mCq3xVb7A"
  }
}
codeHTTPtypeMeaning
unauthorized401authentication_errorMissing, malformed or unknown API key.
forbidden403permission_errorThe key is valid but not allowed to do this.
not_found404invalid_request_errorNo such resource for this account.
invalid_request400invalid_request_errorThe body or a query parameter failed validation; `error.param` names the field.
rate_limited429rate_limit_errorToo many requests for this key, or too many failed authentications from this IP.
quota_exceeded402quota_errorThe monthly document quota for your tier is used up.
idempotency_conflict409idempotency_errorThe `Idempotency-Key` was reused with a different body, or a matching request is in flight.
internal500api_errorSomething failed on our side. Retry, and quote the `request_id`.

Idempotency

  • Send an Idempotency-Key header (any unique string, ≤ 255 characters) on POST /v1/invoices so a retried request cannot create a second invoice.
  • Keys are remembered for 24 hours. A retry with the same key and the same body replays the original 201 response and adds idempotent-replayed: true.
  • The same key with a different body — or an identical request still in flight — returns 409 idempotency_conflict.

Pagination

  • GET /v1/invoices returns newest first, limit items per page (1100, default 20).
  • When has_more is true, pass the response’s next_cursor as starting_after to fetch the next page. On the last page next_cursor is null.
  • Cursors are opaque — do not construct or parse them.

Rate limits

  • 600 requests per minute per API key.
  • Every 2xx carries x-ratelimit-limit and x-ratelimit-remaining.
  • Over the limit you get 429 rate_limited with a Retry-After header in seconds. Back off; do not hot-retry.
  • Failed authentications are separately limited to 30 per minute per IP.

Endpoint reference

get/v1/me

Retrieve the account

Returns the API tier of the key and this month’s document usage against the tier limit.

curl https://invoicestudio.co/api/v1/me \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY"
get/v1/invoices

List invoices

Returns your invoices and quotes, newest first. Page forward by passing the previous response’s `next_cursor` as `starting_after`.

curl "https://invoicestudio.co/api/v1/invoices?limit=20" \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY"
post/v1/invoices

Create an invoice

Creates an invoice or quote and counts one document against the monthly quota. Send an `Idempotency-Key` to make retries safe: the stored response is replayed with `idempotent-replayed: true`, while the same key with a different body — or a request still in flight — returns 409.

curl -X POST https://invoicestudio.co/api/v1/invoices \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"version":1,"type":"invoice","number":"INV-1001","issueDate":"2026-09-01","dueDate":"2026-09-15","currency":"EUR","from":"Acme GmbH\nHauptstrasse 1\n10115 Berlin","to":"Globex Ltd\n5 King Street\nLondon","items":[{"name":"Consulting","description":"September retainer","quantity":10,"unitCostCents":12000}],"tax":{"title":"VAT","kind":"percent","value":19},"notes":"Thank you for your business.","paymentLink":"https://buy.stripe.com/test_123"}'
get/v1/invoices/{id}

Retrieve an invoice

Fetches one invoice or quote by id, including its status and share link.

curl https://invoicestudio.co/api/v1/invoices/6f1c9b6e-0f2a-4a3a-9a12-2f4d8c9b1a55 \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY"
delete/v1/invoices/{id}

Delete an invoice

Permanently deletes an invoice and any share link. Emits `invoice.deleted`. Deleting does not refund the document against the monthly quota.

curl -X DELETE https://invoicestudio.co/api/v1/invoices/6f1c9b6e-0f2a-4a3a-9a12-2f4d8c9b1a55 \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY"
get/v1/invoices/{id}/pdf

Download the PDF

Renders the invoice as a PDF. The response is a file attachment named after the document.

curl https://invoicestudio.co/api/v1/invoices/6f1c9b6e-0f2a-4a3a-9a12-2f4d8c9b1a55/pdf \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY" \
  -o invoice.pdf
get/v1/invoices/{id}/ubl

Download UBL 2.1 XML

Returns the invoice as UBL 2.1 XML for e-invoicing pipelines.

curl https://invoicestudio.co/api/v1/invoices/6f1c9b6e-0f2a-4a3a-9a12-2f4d8c9b1a55/ubl \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY" \
  -o invoice.xml
post/v1/invoices/{id}/share

Create a share link

Publishes a public link for the invoice and returns the invoice with `share_url` set. Idempotent: repeated calls return the same link. Emits `invoice.shared`.

curl -X POST https://invoicestudio.co/api/v1/invoices/6f1c9b6e-0f2a-4a3a-9a12-2f4d8c9b1a55/share \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY"
get/v1/webhooks

List webhook endpoints

Returns every endpoint on the account, oldest first. Secrets are never included.

curl https://invoicestudio.co/api/v1/webhooks \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY"
post/v1/webhooks

Create a webhook endpoint

Registers a public https URL for the given events and returns the signing secret — the only time it is shown. At most 10 endpoints per account.

curl -X POST https://invoicestudio.co/api/v1/webhooks \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/invoicestudio","events":["invoice.created","invoice.shared"]}'
get/v1/webhooks/{id}

Retrieve a webhook endpoint

Fetches one endpoint by id. The signing secret is not returned.

curl https://invoicestudio.co/api/v1/webhooks/3a7c1f60-9d21-4a4e-9f70-1b2c3d4e5f60 \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY"
delete/v1/webhooks/{id}

Delete a webhook endpoint

Removes the endpoint. Pending deliveries for it stop.

curl -X DELETE https://invoicestudio.co/api/v1/webhooks/3a7c1f60-9d21-4a4e-9f70-1b2c3d4e5f60 \
  -H "Authorization: Bearer $INVOICESTUDIO_API_KEY"

Webhooks

Register an https endpoint (dashboard or POST /v1/webhooks) and we POST an event to it. Deliveries are not authenticated with your API key — verify the signature instead.

Events

  • invoice.createdSent when an invoice or quote is created through the API. `data.object` is the Invoice.
  • invoice.sharedSent when a share link is created for an invoice. `data.object` is the Invoice, with `share_url` set.
  • invoice.viewedSent the first time a recipient opens the shared invoice page. `data.object` is the Invoice.
  • invoice.deletedSent when an invoice is deleted. `data.object` is the Deleted stub.

Payload

{
  "id": "evt_9tQ0Zg2mCq3xVb7A",
  "object": "event",
  "type": "invoice.created",
  "created": 1772620200,
  "data": {
    "object": {
      "id": "6f1c9b6e-0f2a-4a3a-9a12-2f4d8c9b1a55",
      "object": "invoice",
      "type": "invoice",
      "number": "INV-1001",
      "currency": "EUR",
      "total_cents": 142800,
      "balance_due_cents": 142800,
      "status": "draft",
      "created_at": "2026-09-01T09:30:00.000Z",
      "pdf_url": "https://invoicestudio.co/api/v1/invoices/6f1c9b6e-0f2a-4a3a-9a12-2f4d8c9b1a55/pdf",
      "ubl_url": "https://invoicestudio.co/api/v1/invoices/6f1c9b6e-0f2a-4a3a-9a12-2f4d8c9b1a55/ubl",
      "share_url": null,
      "data": {
        "version": 1,
        "type": "invoice",
        "number": "INV-1001",
        "issueDate": "2026-09-01",
        "dueDate": "2026-09-15",
        "currency": "EUR",
        "from": "Acme GmbH\nHauptstrasse 1\n10115 Berlin",
        "to": "Globex Ltd\n5 King Street\nLondon",
        "items": [
          {
            "name": "Consulting",
            "description": "September retainer",
            "quantity": 10,
            "unitCostCents": 12000
          }
        ],
        "tax": {
          "title": "VAT",
          "kind": "percent",
          "value": 19
        },
        "notes": "Thank you for your business.",
        "paymentLink": "https://buy.stripe.com/test_123"
      }
    }
  }
}

Verifying the signature

Each delivery carries InvoiceStudio-Signature: t=<unix-seconds>,v1=<hex>, where v1 is HMAC-SHA256 of the string "<t>.<raw request body>", keyed with the endpoint secret (whsec_…), hex-encoded. Reject anything older than 300 seconds and compare in constant time.

import crypto from 'node:crypto'

// Verify against the RAW body, before JSON.parse.
export function verify(secret, header, rawBody, nowS = Date.now() / 1000) {
  try {
    const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
    const t = Number(parts.t)
    if (!Number.isFinite(t) || Math.abs(nowS - t) > 300) return false
    const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex')
    const a = Buffer.from(expected, 'hex')
    const b = Buffer.from(parts.v1 ?? '', 'hex')
    return a.length === b.length && crypto.timingSafeEqual(a, b)
  } catch {
    return false
  }
}

// req.headers['invoicestudio-signature'] carries t=…,v1=…
import hashlib, hmac, time

# header = request.headers["InvoiceStudio-Signature"]
def verify(secret: str, header: str, raw_body: bytes) -> bool:
    try:
        parts = dict(p.split("=", 1) for p in header.split(","))
        t = int(parts["t"])
        v1 = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > 300:
        return False
    expected = hmac.new(
        secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, v1)

Retries and limits

  • Any 2xx marks the delivery successful. A failure is retried after 1m, 5m, 30m, 2h, 12h6 attempts in total, after which the delivery is parked.
  • Deliveries are unique per (endpoint, event id), and redirects are not followed.
  • A payload over 256 KB is truncated to data: { object: { id, object }, truncated: true } — re-fetch the resource over the API when you see truncated.

UBL e-invoicing

GET /v1/invoices/{id}/ubl returns the document as a base UBL 2.1 Invoice XML: supplier and customer parties, the issue date, currency, one InvoiceLine per item with quantity and unit price, a TaxTotal/TaxSubtotal when there is tax, the due date when set, the legal monetary totals, and payment means (your bank details or payment link) when you provide them.

Honest limitation: this is valid UBL 2.1, but it is notEN 16931 / PEPPOL validated. Addresses are free text, so the structured postal code and country fields those profiles require are not populated, and there are no VAT identifiers or scheme ids. Use it to feed your own e-invoicing pipeline — do not expect it to pass a PEPPOL access point unmodified.

SDKs

Thin, dependency-free clients over the same REST API — typed responses, the error envelope raised as an exception, and idempotency keys handled for you.

TypeScript

npm i @claudiumarius/invoicestudio-sdk
import { InvoiceStudio } from '@claudiumarius/invoicestudio-sdk'

const client = new InvoiceStudio(process.env.INVOICESTUDIO_API_KEY!)

const inv = await client.invoices.create({
  version: 1,
  type: 'invoice',
  number: 'INV-1001',
  issueDate: '2026-09-01',
  currency: 'EUR',
  from: 'Acme GmbH',
  to: 'Globex Ltd',
  items: [{ name: 'Consulting', quantity: 10, unitCostCents: 12000 }],
})

console.log(inv.id, inv.total_cents, inv.pdf_url)

Python

pip install invoicestudio
import os
from invoicestudio import InvoiceStudio

client = InvoiceStudio(os.environ["INVOICESTUDIO_API_KEY"])

inv = client.invoices.create({
    "version": 1,
    "type": "invoice",
    "number": "INV-1001",
    "issueDate": "2026-09-01",
    "currency": "EUR",
    "from": "Acme GmbH",
    "to": "Globex Ltd",
    "items": [{"name": "Consulting", "quantity": 10, "unitCostCents": 12000}],
})

print(inv["id"], inv["total_cents"], inv["pdf_url"])