> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flowsign.app/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Flowsign is one word with a lowercase s.
> The REST API base URL is https://my.flowsign.app and every endpoint lives under /api/v1.
> When answering API questions, cite the HTTP method and endpoint path.
> API access needs the Enterprise plan and an API key with the API access permission.

# Developer quickstart

> From no API key to a sent package in five requests.

The Flowsign REST API lets you create packages from templates, send them, read their status and manage webhook endpoints. This page gets you to a sent package as quickly as the public API allows.

<Info>
  The API and webhooks are available on the Enterprise plan. Calls from other plans return `402`.
</Info>

Base URL: `https://my.flowsign.app`. Every success response is wrapped as `{ "data": ... }`; every error is `{ "error": "...", "details"?: {...} }`. See [Errors](/api-reference/errors).

<Steps>
  <Step title="Create an API key">
    In the app, open **Settings > API keys** ([my.flowsign.app/settings/api-keys](https://my.flowsign.app/settings/api-keys)) and create a key. Give it a name, choose the role it acts as and the workspace it works in, and optionally set an expiry date. The key is shown once; copy it now.

    Only an owner can create keys. The role you choose must have the **API access** permission; the Admin profile has it by default, and an administrator can grant it to other profiles from **Roles & Permissions**. The key acts with that role's permissions in that workspace, not as the person who issued it.
  </Step>

  <Step title="Check the key works">
    List the workspaces the key can act in. A key is bound to one workspace, so the response lists that one and names it as the active workspace.

    ```bash theme={null}
    curl https://my.flowsign.app/api/v1/workspaces \
      -H "Authorization: Bearer fsk_your_key_here"
    ```

    ```json theme={null}
    {
      "data": {
        "active": "ws_default",
        "workspaces": [
          { "id": "ws_default", "name": "Head office", "slug": "head-office", "isDefault": true }
        ]
      }
    }
    ```

    You can send `X-Workspace-Id: <id>` on any request to be explicit, but it may only name the key's own workspace; any other value is refused with `401`. To work in another workspace, create a key for it. See [Workspaces](/api-reference/workspaces).
  </Step>

  <Step title="Build a template in the app">
    The quickest route to a sent package is a template: it already holds the documents, roles and fields, so the API only needs the recipients. Create one in the app with at least one document, one role and the fields each role must complete, then set it to `ACTIVE`. See [Building a workflow](/guides/building-a-workflow). (The API can also upload documents and place fields itself; see [Creating a package without a template](#creating-a-package-without-a-template).)

    Find its id with the API:

    ```bash theme={null}
    curl "https://my.flowsign.app/api/v1/templates?status=ACTIVE" \
      -H "Authorization: Bearer fsk_your_key_here"
    ```

    Each template in the response lists its `roles`. You map recipients onto roles by role name in the next step.
  </Step>

  <Step title="Create a package from the template">
    `POST /api/v1/packages/from-template` creates a `DRAFT` package from an `ACTIVE` template; a template still in `DRAFT` is refused with `409` until it is published in the app. Supply one recipient per role, naming the role with `role`; roles are matched by name, not by position, and every role in the template must be given a recipient. Merge field values go in `fields`, keyed by merge field key; a blank value takes the field's default, and keys the template does not ask the sender for are rejected with `422`. Pass an `externalId` of your own to make the call idempotent: repeating it returns the existing package instead of creating another.

    ```bash theme={null}
    curl -X POST https://my.flowsign.app/api/v1/packages/from-template \
      -H "Authorization: Bearer fsk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
        "templateId": "tmpl_abc123",
        "recipients": [
          { "role": "Employee", "name": "Ana Reid", "email": "ana@example.com" },
          { "role": "Manager", "name": "Ben Toa", "email": "ben@example.com" }
        ],
        "fields": { "start_date": "1 October 2026" },
        "externalId": "hr-2026-0142"
      }'
    ```

    ```json theme={null}
    {
      "data": {
        "id": "pkg_9f3c2e",
        "externalId": "hr-2026-0142",
        "status": "DRAFT",
        "recipients": [
          { "id": "rcp_1a2b", "name": "Ana Reid", "email": "ana@example.com", "role": "Employee" },
          { "id": "rcp_3c4d", "name": "Ben Toa", "email": "ben@example.com", "role": "Manager" }
        ]
      }
    }
    ```

    Pass `"status": "sent"` to send the package in the same call (the response then reports `IN_PROGRESS`), or `scheduledAt` (an ISO 8601 timestamp) to create a `SCHEDULED` package that Flowsign sends at that time. Do not call send on a scheduled package.
  </Step>

  <Step title="Send it">
    Sending is an action on the package. Only `DRAFT` packages can be sent, and the key's role needs the send permission.

    ```bash theme={null}
    curl -X PATCH https://my.flowsign.app/api/v1/packages/pkg_9f3c2e \
      -H "Authorization: Bearer fsk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "action": "send" }'
    ```

    ```json theme={null}
    { "data": { "id": "pkg_9f3c2e", "status": "IN_PROGRESS" } }
    ```

    With parallel signing, every Signer and Viewer now receives their invitation email; with a signing order on, only the first turn is invited and the rest follow as each turn completes. Sending charges one package against your allowance. A `402` means the organisation has run out of packages (or its plan no longer includes the feature), and a `409` means a document's file has not finished uploading.
  </Step>

  <Step title="Follow progress">
    Poll the package, or register a [webhook](/webhooks/overview) and react to `SESSION_COMPLETED` and `PACKAGE_COMPLETED` instead.

    ```bash theme={null}
    curl https://my.flowsign.app/api/v1/packages/pkg_9f3c2e \
      -H "Authorization: Bearer fsk_your_key_here"
    ```

    The response includes `status`, each recipient's session with `sentAt`, `openedAt`, `completedAt` and `declinedAt`, the documents, and the last 100 audit events.
  </Step>
</Steps>

## The same flow in code

The steps above are the cURL walkthrough. Here is the whole run in one file, in three of the languages the endpoint reference also shows.

<CodeGroup>
  ```javascript JavaScript theme={null}
  const BASE = "https://my.flowsign.app";
  const headers = {
    Authorization: `Bearer ${process.env.FLOWSIGN_API_KEY}`,
    "Content-Type": "application/json",
  };

  async function call(method, path, body) {
    const res = await fetch(`${BASE}${path}`, {
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    });
    const json = await res.json();
    if (!res.ok) throw new Error(`${res.status} ${json.error}`);
    return json.data;
  }

  const { templates } = await call("GET", "/api/v1/templates?status=ACTIVE");
  const template = templates.find((t) => t.name === "Employment agreement");

  const { id: packageId } = await call("POST", "/api/v1/packages/from-template", {
    templateId: template.id,
    recipients: [
      { role: "Employee", name: "Ana Reid", email: "ana@example.com" },
      { role: "Manager", name: "Ben Toa", email: "ben@example.com" },
    ],
    fields: { start_date: "1 October 2026" },
    externalId: "hr-2026-0142",
  });

  const sent = await call("PATCH", `/api/v1/packages/${packageId}`, { action: "send" });
  console.log(sent.status);
  ```

  ```python Python theme={null}
  import os

  import requests

  BASE = "https://my.flowsign.app"

  session = requests.Session()
  session.headers.update({"Authorization": f"Bearer {os.environ['FLOWSIGN_API_KEY']}"})


  def call(method, path, body=None):
      res = session.request(method, f"{BASE}{path}", json=body, timeout=30)
      if not res.ok:
          raise RuntimeError(f"{res.status_code} {res.json()['error']}")
      return res.json()["data"]


  templates = call("GET", "/api/v1/templates?status=ACTIVE")["templates"]
  template = next(t for t in templates if t["name"] == "Employment agreement")

  created = call(
      "POST",
      "/api/v1/packages/from-template",
      {
          "templateId": template["id"],
          "recipients": [
              {"role": "Employee", "name": "Ana Reid", "email": "ana@example.com"},
              {"role": "Manager", "name": "Ben Toa", "email": "ben@example.com"},
          ],
          "fields": {"start_date": "1 October 2026"},
          "externalId": "hr-2026-0142",
      },
  )

  sent = call("PATCH", f"/api/v1/packages/{created['id']}", {"action": "send"})
  print(sent["status"])
  ```

  ```csharp C# theme={null}
  using System.Net.Http.Headers;
  using System.Net.Http.Json;
  using System.Text.Json;

  using var http = new HttpClient { BaseAddress = new Uri("https://my.flowsign.app") };
  http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
      "Bearer",
      Environment.GetEnvironmentVariable("FLOWSIGN_API_KEY"));

  async Task<JsonElement> CallAsync(HttpMethod method, string path, object? body = null)
  {
      using var request = new HttpRequestMessage(method, path);
      if (body is not null) request.Content = JsonContent.Create(body);

      using var response = await http.SendAsync(request);
      var json = await response.Content.ReadFromJsonAsync<JsonElement>();

      if (!response.IsSuccessStatusCode)
      {
          throw new HttpRequestException(
              $"{(int)response.StatusCode} {json.GetProperty("error").GetString()}");
      }

      return json.GetProperty("data");
  }

  var list = await CallAsync(HttpMethod.Get, "/api/v1/templates?status=ACTIVE");
  var template = list.GetProperty("templates").EnumerateArray()
      .First(t => t.GetProperty("name").GetString() == "Employment agreement");

  var created = await CallAsync(
      HttpMethod.Post,
      "/api/v1/packages/from-template",
      new
      {
          templateId = template.GetProperty("id").GetString(),
          recipients = new[]
          {
              new { role = "Employee", name = "Ana Reid", email = "ana@example.com" },
              new { role = "Manager", name = "Ben Toa", email = "ben@example.com" },
          },
          fields = new Dictionary<string, string> { ["start_date"] = "1 October 2026" },
          externalId = "hr-2026-0142",
      });

  var sent = await CallAsync(
      HttpMethod.Patch,
      $"/api/v1/packages/{created.GetProperty("id").GetString()}",
      new { action = "send" });

  Console.WriteLine(sent.GetProperty("status").GetString());
  ```
</CodeGroup>

## Other package actions

`PATCH /api/v1/packages/{packageId}` also accepts:

| Body                                                | Effect                                                                                                                        | Permission                                              |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `{ "action": "void", "reason": "..." }`             | Ends the package as `VOID` and notifies recipients. `reason` is optional (up to 500 characters) and is quoted in the email    | Void packages                                           |
| `{ "action": "on_hold" }`                           | Pauses an `IN_PROGRESS` package as `ON_HOLD`. Its signing links stop working and nobody new is invited                        | Void packages                                           |
| `{ "action": "resume" }`                            | Returns an `ON_HOLD` package to `IN_PROGRESS`. The same links work again, and the expiry moves back by the time spent on hold | Void packages                                           |
| `{ "action": "transfer", "newOwnerId": "usr_..." }` | Transfers ownership to another member of the organisation                                                                     | The key's issuer owns the package, or Transfer packages |

## Custom fields

Custom fields are the organisation's own reference values on a package, such as an employee ID or a cost centre. They are defined in the app under **Settings > Custom fields** (the API reads their values but does not create or edit the definitions); each one has a `key` that never changes. `GET /api/v1/custom-fields` lists them.

Packages carry their values as `metadata`, an object keyed by custom field `key`. `GET /api/v1/packages`, `GET /api/v1/packages/{packageId}` and every [webhook event](/webhooks/events) include it, as `{}` when nothing is set. `GET /api/v1/templates/{templateId}` returns the template's defaults the same way; they are set in the app, and `POST` or `PATCH` on `/api/v1/templates` refuses `metadata` with `400`.

Set values with `metadata` on `POST /api/v1/packages/from-template` or `POST /api/v1/packages`. A package from a template starts with the template's defaults, and each key you send replaces one; an empty string clears it.

```json theme={null}
"metadata": { "employee_id": "E-1042", "cost_centre": "CC-4021" }
```

The request fails with `422` and `details.metadata` when a key is not a live custom field that applies to the package, and when a required custom field is empty on a call that sends or schedules the package. Sending a draft with `PATCH` checks its required custom fields the same way.

## Creating a package without a template

`POST /api/v1/packages` creates a `DRAFT` package from a title, recipients and document descriptors (`fileName`, `pageCount`, `nonce`), or a `SCHEDULED` one when you pass `scheduledAt`. Each document comes back with an `uploadUrl`: `PUT` the PDF bytes to it to attach the file. The URL is single-use and expires after two hours, and sending is refused with `409` while any document's file has not arrived.

Documents can also be added later with `POST /api/v1/packages/{packageId}/documents`, or taken from the library with `POST /api/v1/packages/{packageId}/library-documents`, and fields are placed with `PUT /api/v1/packages/{packageId}/fields`. Templates accept documents the same way through `POST /api/v1/templates/{templateId}/documents` and `PUT /api/v1/templates/{templateId}/fields`. Use a template when the same documents go out repeatedly; build the package directly when every send carries its own files.

## Release labels

Parts of the app carry a label such as **Experimental** or **Coming soon**, explained in [Release labels](/release-labels). Those labels describe the app, not this API: an experimental feature is not part of the public API unless an endpoint says so.

## Next

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/api-reference/authentication">
    Key lifecycle, permissions, plan requirements and the IP allowlist.
  </Card>

  <Card title="Errors and rate limits" icon="triangle-alert" href="/api-reference/errors">
    Status codes, validation details and the 120 requests per minute limit.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/webhooks/overview">
    Signed event deliveries with retries.
  </Card>

  <Card title="Endpoint reference" icon="list" href="/api-reference/endpoints/workspaces/list-workspaces">
    Every request and response field, with copyable code samples.
  </Card>
</CardGroup>
