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

# 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 and, optionally, an expiry date. The key is shown once; copy it now.

    Creating keys requires the **API access** permission. The Admin profile has it by default; an administrator can grant it to other profiles from **Roles & Permissions**. A key acts as the user who created it, with that user's permissions.
  </Step>

  <Step title="Check the key works">
    List the workspaces the key can act in. The response also tells you which workspace the call used.

    ```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 }
        ]
      }
    }
    ```

    To act in a different workspace, send `X-Workspace-Id: <id>` on any request. See [Workspaces](/api-reference/workspaces).
  </Step>

  <Step title="Build a template in the app">
    The public API does not upload PDF files, so the documents and fields for a package come from a template. 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).

    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` in order. You map recipients onto roles by position (`roleIndex`) in the next step.
  </Step>

  <Step title="Create a package from the template">
    `POST /api/v1/templates/{templateId}` creates a `DRAFT` package. Supply one recipient per role, using the role's zero-based position in the template's `roles` array. Merge field values are optional and are interpolated into the package title and email copy.

    ```bash theme={null}
    curl -X POST https://my.flowsign.app/api/v1/templates/tmpl_abc123 \
      -H "Authorization: Bearer fsk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
        "recipients": [
          { "name": "Ana Reid", "email": "ana@example.com", "roleIndex": 0 },
          { "name": "Ben Toa", "email": "ben@example.com", "roleIndex": 1 }
        ],
        "mergeFieldValues": { "start_date": "1 October 2026" }
      }'
    ```

    ```json theme={null}
    { "data": { "packageId": "pkg_9f3c2e" } }
    ```

    Pass `scheduledAt` (an ISO 8601 timestamp) instead 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 user 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" } }
    ```

    Every Signer and Viewer now receives their invitation email. Sending charges one package against your allowance; a `402` means the organisation has run out of credit.
  </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 each 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 { packageId } = await call("POST", `/api/v1/templates/${template.id}`, {
    recipients: [
      { name: "Ana Reid", email: "ana@example.com", roleIndex: 0 },
      { name: "Ben Toa", email: "ben@example.com", roleIndex: 1 },
    ],
    mergeFieldValues: { start_date: "1 October 2026" },
  });

  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",
      f"/api/v1/templates/{template['id']}",
      {
          "recipients": [
              {"name": "Ana Reid", "email": "ana@example.com", "roleIndex": 0},
              {"name": "Ben Toa", "email": "ben@example.com", "roleIndex": 1},
          ],
          "mergeFieldValues": {"start_date": "1 October 2026"},
      },
  )

  sent = call("PATCH", f"/api/v1/packages/{created['packageId']}", {"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/templates/{template.GetProperty("id").GetString()}",
      new
      {
          recipients = new[]
          {
              new { name = "Ana Reid", email = "ana@example.com", roleIndex = 0 },
              new { name = "Ben Toa", email = "ben@example.com", roleIndex = 1 },
          },
          mergeFieldValues = new Dictionary<string, string> { ["start_date"] = "1 October 2026" },
      });

  var sent = await CallAsync(
      HttpMethod.Patch,
      $"/api/v1/packages/{created.GetProperty("packageId").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" }`                              | Ends the package as `VOID` and notifies recipients        | Void packages                       |
| `{ "action": "on_hold" }`                           | Sets the package to `ON_HOLD`                             | Void packages                       |
| `{ "action": "transfer", "newOwnerId": "usr_..." }` | Transfers ownership to another member of the organisation | Package owner, 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. Admins define them under **Settings → Custom fields**; each one has a `key` that never changes.

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.

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`. It returns a storage `filePath` per document, but the public API has no endpoint for uploading the PDF bytes, and fields are placed in the app. Use it when the package will be finished in the app; use templates when the whole flow must run from code.

## 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 gates 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 a playground.
  </Card>
</CardGroup>
