# eSignStack for Platforms

Give each of your customers their own eSignStack workspace, created, branded and run by your server through the API.

This guide is for software platforms that add e-signatures for their customers. It covers the flows in the order you build them. Request and response schemas are in the [API reference](/docs/api).

## How it fits together

| Concept            | What it is                                                                                                                         |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| Platform           | Your organization's account. It owns customer workspaces, platform keys, platform webhooks, brand defaults and the console.        |
| Customer workspace | One customer's isolated space: templates, envelopes, branding, members. ID `ws_…`, plus your own `external_id`.                    |
| Platform key       | `sk_live_…` or `sk_test_…`. Acts on any of your workspaces when you send `Workspace-Id: ws_…`; without it, on platform endpoints.   |
| Workspace key      | `rk_live_…` or `rk_test_…`. Bound to one workspace, with an explicit list of abilities. Use one per customer to limit exposure.     |
| Platform member    | A person on your team who uses the [platform console](/docs/platform-console): an admin, developer, billing or support member.     |

Keys belong on your server. Never put one in a browser or mobile app; the browser only ever opens a short-lived session URL.

## Get set up

1. Subscribe to the **Platform** plan with the account that will pay for your platform.
2. Contact eSignStack. We create your platform, link it to that paying account and make you its first admin.
3. Open **Platform console** from the workspace switcher or your user menu, invite your team, and create your first platform key.

### Billing

The Platform plan is $99 a month, or $990 a year.

- **In-app signing** in your customer workspaces is unlimited under fair use (normal use by your customers' own organizations).
- **API and MCP envelopes** from all of your workspaces count toward the paying account: 500 a month are included (6,000 up front on yearly billing). Each extra envelope is 25¢, billed on the next invoice.
- An envelope counts once: when it's sent through the API or MCP, or when the API first creates a signing session for an envelope sent in the app. Test mode never counts.
- **Spending cap (optional):** on the paying account's **Billing** page, turn on **Limit extras each month** under **Limit on extra API envelopes** and enter an amount. The account gets an email at 80% and when the limit is reached. After that, API sends past the allowance fail with `api_spending_cap_reached` until the month resets.
- Extras pause while the latest invoice is unpaid (`api_overage_unavailable`).

Customer workspaces get the Platform plan's features, including custom branding, kiosk mode and signer verification. The console's **Settings** page shows who is billed and this period's API usage.

## 1. Create a workspace for each customer

Create a workspace when a customer signs up. Use your own customer ID as `external_id`: it's unique within your platform, so sending the same request again returns the same workspace (`201` the first time, `200` after). The same `external_id` with different attributes returns `409`.

```bash
curl -X POST "https://esignstack.app/api/v1/workspaces" \
  -H "Authorization: Bearer $PLATFORM_KEY" \
  -H "Idempotency-Key: customer-1001-create" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Dental",
    "external_id": "customer_1001",
    "metadata": { "region": "west" },
    "settings": {
      "embed_allowed_origins": ["https://acme.example.com"],
      "signing_return_allowed_origins": ["https://acme.example.com"],
      "default_signing_window_days": 14
    },
    "notification_policy": { "signing_request": "host", "reminder": "host" }
  }'
```

- Don't send `Workspace-Id` to the `/workspaces` endpoints. Store the returned `id` (`ws_…`) next to your customer.
- A workspace takes the key's mode: a test key creates and lists test workspaces, a live key live ones. Test keys can create and send envelopes; test documents are watermarked and never billed.
- `settings` accepts `embed_allowed_origins` and `signing_return_allowed_origins` (origins only: no path, query or credentials; up to 50 return origins), `default_signing_window_days` (1–365) and the signing disclosure text (`esign_disclosure_title`, `esign_disclosure_body`, `esign_disclosure_agreement_label`).
- `notification_policy` says who sends each email: `esignstack` (we send it) or `host` (we stay silent; you notify people yourself). Kinds: `signing_request`, `reminder`, `sequential_handoff`, `completed_copy_recipients`, `completed_copy_cc`, `owner_completed`, `delivery_problem`, `declined`, `voided`.
- `metadata` takes up to 20 string key/value pairs. Don't store secrets there.
- `brand_kit` takes an inline kit or a `bk_…` ID (see Branding below).

Other workspace calls:

| Task                          | Request                                                                                  |
| ----------------------------- | ---------------------------------------------------------------------------------------- |
| Find a customer's workspace   | `GET /workspaces?filter[external_id]=customer_1001` (also `filter[status]`, `filter[name]`) |
| Update                        | `PATCH /workspaces/{ws}` (`external_id` can't change)                                    |
| Suspend or resume             | `POST /workspaces/{ws}/suspend`, `POST /workspaces/{ws}/resume`                          |
| Usage for reconciliation      | `GET /usage?workspace=ws_…&period=2026-11`                                               |
| Your Platform allowance       | `GET /usage` without `Workspace-Id`: `platform.api_envelopes` (allowance, extras, cap)   |
| Platform settings (read-only) | `GET /platform` with `platform:read`                                                     |
| Platform members (read-only)  | `GET /platform/members` (`filter[role]`); invitations and roles stay in the console      |
| Export a workspace            | `GET /workspaces/{ws}/export` with `Workspace-Id` and the `workspaces:export` ability     |

A suspended workspace keeps its reads and its signing links already sent, but every write through the API or the app is blocked.

## 2. Keys

The simplest integration holds one platform key and names the workspace on each call:

```bash
curl "https://esignstack.app/api/v1/brand_kits/effective" \
  -H "Authorization: Bearer $PLATFORM_KEY" \
  -H "Workspace-Id: ws_01J8Z3Q4M7X2V5N9K6T1R0WABC"
```

For isolation, create a workspace key per customer with only the abilities its integration needs. Call with a platform key and no `Workspace-Id`. The secret is returned once.

```bash
curl -X POST "https://esignstack.app/api/v1/api_keys" \
  -H "Authorization: Bearer $PLATFORM_KEY" \
  -H "Idempotency-Key: customer-1001-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Dental server",
    "workspace": "ws_01J8Z3Q4M7X2V5N9K6T1R0WABC",
    "abilities": ["envelopes:read", "envelopes:write", "envelopes:send", "templates:read", "files:write", "artifacts:read"],
    "expires_at": "2027-10-01T00:00:00Z"
  }'
```

- Abilities: `envelopes:read|write|send`, `templates:read|write`, `folders:read|write`, `files:write`, `webhooks:read|write`, `brand_kits:read|write`, `artifacts:read`, `usage:read`, `keys:write`, `workspaces:export`. Platform keys only: `workspaces:read|write`, `login_links:write`, `identity_providers:write`.
- A key can't grant abilities it doesn't have, and can't outlive its own expiry. The new key has the same mode as the key that created it, so a test key can only issue test keys.
- List keys with `GET /api_keys`, check the current key with `GET /api_keys/current`, revoke with `DELETE /api_keys/{key}`.
- In the console, only platform admins can create keys.

Create and action `POST`s take an `Idempotency-Key` (up to 255 characters); the deprovision endpoints don't. Reuse it only to retry the same request; a different body with the same key returns `409 idempotency_key_reused`. Versions, errors and rate limits are in the [API quick start](/docs/api-authentication).

## 3. Branding

A brand kit holds logos, colors, radius, font and email wording. Set a platform default once and override it per customer; a workspace inherits every field it leaves empty.

```bash
# Platform default: platform key, no Workspace-Id
curl -X POST "https://esignstack.app/api/v1/brand_kits" \
  -H "Authorization: Bearer $PLATFORM_KEY" \
  -F "name=Acme Platform" -F "primary_color=#1f4fd8" -F "radius_px=8" \
  -F "font=instrument-sans" -F "support_contact=help@example.com" \
  -F "logo_light=@logo-light.png"

# Customer override: same endpoint with Workspace-Id
curl -X POST "https://esignstack.app/api/v1/brand_kits" \
  -H "Authorization: Bearer $PLATFORM_KEY" -H "Workspace-Id: $WS" \
  -F "name=Acme Dental" -F "primary_color=#0a7d5a" -F "logo_light=@acme-dental.png"

# What signers will see
curl "https://esignstack.app/api/v1/brand_kits/effective" \
  -H "Authorization: Bearer $PLATFORM_KEY" -H "Workspace-Id: $WS"
```

- Colors are hex, the font is `instrument-sans`, `system` or `serif`, and the radius is bounded. Logos are PNG, JPEG or WebP, up to 2 MB.
- Creating, updating or deleting a kit needs a live key; a test key gets `403 live_key_required`.
- Each owner has one kit; update it with `PATCH /brand_kits/{kit}`. Send `null` for a field to inherit it again.
- `show_powered_by: false` hides the eSignStack credit in your workspaces.
- Admins can also edit the platform default under **Settings → Brand defaults** in the console.

## 4. Webhooks and catch-up

One platform endpoint receives events from all of your workspaces. Create it with a platform key and no `Workspace-Id`, or in the console under **Webhooks**:

```bash
curl -X POST "https://esignstack.app/api/v1/webhook_endpoints" \
  -H "Authorization: Bearer $PLATFORM_KEY" \
  -H "Idempotency-Key: platform-endpoint" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://hooks.example.com/esignstack", "enabled_events": ["*"] }'
```

Each event carries `platform` (`plat_…`) and `workspace` (`ws_…`), so you can route it to the right customer. Verify the `Webhook-Signature` header, answer `2xx` within 15 seconds, and de-duplicate on `Webhook-Id`: deliveries can repeat and arrive out of order. Signature details are in [Webhooks and events](/docs/webhooks).

**Catch up after downtime.** Events stay available for 30 days. On startup and on a schedule, list what you missed and resend anything you didn't process:

```bash
curl -G "https://esignstack.app/api/v1/events" \
  -H "Authorization: Bearer $PLATFORM_KEY" \
  --data-urlencode "filter[created][gte]=2026-12-01T00:00:00Z" --data-urlencode "filter[type]=envelope.completed"

curl -X POST "https://esignstack.app/api/v1/events/evt_01J…/resend" \
  -H "Authorization: Bearer $PLATFORM_KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{ "webhook_endpoint": "we_01J…" }'
```

Rotate the secret with `POST /webhook_endpoints/{id}/rotate_secret`; `expire_old_in` (up to 86,400 seconds) keeps the old secret valid meanwhile. If every delivery to an endpoint fails for 5 days, it's turned off. Check its status on the console's **Webhooks** page and turn it back on with **Enable**.

## 5. Send envelopes

Use the platform key with `Workspace-Id`, or the customer's workspace key.

| Step                                    | Endpoint                                                          |
| --------------------------------------- | ----------------------------------------------------------------- |
| Upload a PDF and poll until it's ready  | `POST /files`, `GET /files/{file}`                                |
| Create a draft from a ready file        | `POST /envelopes`                                                 |
| Or create from a template               | `POST /templates/{tpl}/envelopes`                                 |
| Add documents, recipients and fields    | `/envelopes/{env}/documents`, `/recipients`, `/fields`            |
| Preview, then send                      | `POST /envelopes/{env}/preview`, `POST /envelopes/{env}/send`     |
| Correct, remind, extend or void         | `/corrections`, `/remind`, `/extend`, `/void`                     |
| Get the signed files                    | `GET /envelopes/{env}/artifacts`                                  |

```bash
curl -X POST "https://esignstack.app/api/v1/envelopes" \
  -H "Authorization: Bearer $PLATFORM_KEY" -H "Workspace-Id: $WS" \
  -H "Idempotency-Key: case-77-create" -H "Content-Type: application/json" \
  -d '{ "file": "file_01J8Z3Q4M7X2V5N9K6T1R0WABC", "name": "Case 77", "external_id": "case-77" }'
```

On `envelope.completed`, call `GET /envelopes/{env}/artifacts` and download the files promptly; the URLs are short-lived. The full walkthrough is in the [Envelopes API](/docs/envelopes).

## 6. Signing and editor sessions

A **signing session** gives one recipient a URL to sign, hosted on eSignStack or embedded in your app. Create it on your server when the signer is ready:

```bash
curl -X POST "https://esignstack.app/api/v1/envelopes/$ENV/recipients/$RCP/signing_sessions" \
  -H "Authorization: Bearer $PLATFORM_KEY" -H "Workspace-Id: $WS" \
  -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
  -d '{
    "mode": "embedded",
    "parent_origin": "https://acme.example.com",
    "actor": { "name": "Jane Doe", "email": "jane@example.com", "external_id": "staff_17" }
  }'
```

- `parent_origin` must be one of the workspace's `embed_allowed_origins`. A hosted `return_url` must be on the workspace's `signing_return_allowed_origins`, which you set when provisioning the workspace or a workspace admin sets under **Settings → Embedding**.
- The URL must be opened within 10 minutes. An open session lasts 60 minutes idle and 8 hours at most.
- `actor` is who your app says is acting; the audit trail records it as stated by your platform.
- The signing page uses the workspace's brand kit; `appearance` can adjust `primary`, `radius`, `font` and `dark_mode`.

An **editor session** lets your user prepare a draft or template in eSignStack's editor without an eSignStack login. See [Embedded signing](/docs/embed-integration) and [Editor sessions](/docs/editor-sessions).

## 7. Sign in your people

Your staff can open eSignStack without a separate password, two ways:

- **Login links:** your server requests a single-use link for a person and redirects them to it.
- **Sign in with your platform:** register your OpenID Connect provider and people sign in with their platform account.

Both identify a person by your own ID, never by email, and give them memberships in your workspaces that follow your decisions. See [Platform sign-in](/docs/platform-sign-in).

## Build order

1. Create a test workspace with an `external_id`; retry it to see the same workspace come back.
2. Create a brand kit and check `GET /brand_kits/effective`.
3. Create a platform webhook endpoint, verify signatures, and add the `GET /events` catch-up job.
4. Wire the envelope flow with idempotency keys, then download artifacts on `envelope.completed`.
5. Add signing sessions, hosted first, then embedded.
6. Add login links, then OIDC if your people also go straight to eSignStack.
7. Create a workspace key per customer and keep the platform key for platform-level work.
