*REST API › Guides*

# Testing your integration

> Start with a read-only key, check the account's automations before the first write, and keep irreversible operations out of any account that holds real data.

There is no sandbox and there are no test keys: every key starts with `fk_live_` and acts on the real data of the account that issued it. Every step below is a way to reduce what a test can touch.

## Start with a read-only key

Create a key with only the `:read` scopes the integration needs, plus [`account:read`](https://docs.filmo.io/rest-api/guides/scopes#account:read), and build all your reading, paging and parsing with it. A read-only key cannot change anything, so a bug in your code cannot either. The [Scopes](https://docs.filmo.io/rest-api/guides/scopes) guide has recipes by integration type. You can also try reads from the [OpenAPI reference](https://docs.filmo.io/rest-api/openapi).

## A separate account for writes

Test writes in a separate account that holds only fictitious data (names such as Acme Rentals, emails at `example.com`), not in the account of a real customer. Use a free trial account: it serves to test the API. An administrator creates API keys and connects OAuth and MCP in it like in any other account, and the trial unlocks every add-on. Know its terms.

- The trial lasts 14 days and needs no card. After it expires it may be extendable once, by 7 days.
- A trial unlocks every add-on while it lasts ([`trial`](https://docs.filmo.io/rest-api/objects/account#response-trial)). When it ends, the operations behind an add-on answer 402 [`addon_required`](https://docs.filmo.io/rest-api/errors/addon_required) while the others keep working, so a test that passed in the trial can fail later. Keys and OAuth connections are not revoked when the trial ends, and authentication does not look at the trial: it checks the key, its owner and the owner's access to the account.
- The data of a trial account is purged about 30 days after the trial ends. Do not keep anything there that you need.
- Treat the test account as a real one. Validating an invoice or a credit note is irreversible and uses up numbers of the account's numbering, also in a trial account: validate only what a test needs.

## Before writing: check the automations

A write through the API goes through the same path as a change made in the app, so it fires the account's event workflows. You cannot suppress that per request: there is no header or flag for it. Those workflows can send emails to customers or call third parties, so this check is mandatory before the first write. Events fire after the response and on a best-effort basis; most business writes (orders, invoices, payments, quotes, contacts, tasks, projects) fire one, while some edits, such as tag assignments or item price changes, fire none.

1. **List the active workflows that start on an event or an entity** Call [`GET /api/v1/workflows`](https://docs.filmo.io/rest-api/endpoint-reference/workflows/list-workflows) with [`isActive`](https://docs.filmo.io/rest-api/endpoint-reference/workflows/list-workflows#param-isActive) set to `true` and [`triggerTypes`](https://docs.filmo.io/rest-api/endpoint-reference/workflows/list-workflows#param-triggerTypes) set to `event,entity`. The key needs [`workflows:read`](https://docs.filmo.io/rest-api/guides/scopes#workflows:read), and only administrators can list workflows.
2. **Read what each one does** For each workflow, [`trigger`](https://docs.filmo.io/rest-api/objects/workflow#response-trigger) says which event or entity starts it. Open it in the app to see what it sends.
3. **Pause them, or exclude the API** Pause the ones that could reach a customer or a third party while you test. Alternatively, filter the trigger of each workflow by `origin` to exclude `api` (and `mcp`, if you also test over MCP), so it ignores what your key writes. Resume or restore them afterwards.

If you also test over MCP, an assistant writes with the same effect. Read [Safe writes](https://docs.filmo.io/mcp/guides/safe-writes) before you let one write.

**cURL**

```bash
BASE="${FILMO_BASE_URL:-https://app.filmo.io}"

curl -sS -G "$BASE/api/v1/workflows" \
  -H "Authorization: Bearer $FILMO_API_KEY" \
  --data-urlencode "isActive=true" \
  --data-urlencode "triggerTypes=event,entity" \
  --data-urlencode "limit=500"
```

**Node**

```javascript
const base = process.env.FILMO_BASE_URL ?? "https://app.filmo.io";

const url = new URL(base + "/api/v1/workflows");
url.searchParams.set("isActive", "true");
url.searchParams.set("triggerTypes", "event,entity");
url.searchParams.set("limit", "500");

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.FILMO_API_KEY}` },
});
if (!res.ok) throw new Error("HTTP " + res.status + " " + (await res.text()));
const { data, pagination } = await res.json();

for (const workflow of data) {
  const starts = workflow.trigger.event ?? workflow.trigger.entity;
  console.log(workflow.name, "-", workflow.trigger.type, starts);
}
if (pagination.hasMore) console.warn("More workflows: follow pagination.nextCursor");
```

**Python**

```python
import os

import requests

base = os.environ.get("FILMO_BASE_URL", "https://app.filmo.io")

res = requests.get(
    base + "/api/v1/workflows",
    headers={"Authorization": f"Bearer {os.environ['FILMO_API_KEY']}"},
    params={"isActive": "true", "triggerTypes": "event,entity", "limit": "500"},
    timeout=30,
)
res.raise_for_status()
body = res.json()

for workflow in body["data"]:
    trigger = workflow["trigger"]
    print(workflow["name"], "-", trigger["type"], trigger["event"] or trigger["entity"])
if body["pagination"]["hasMore"]:
    print("More workflows: follow pagination.nextCursor")
```

## Never in production

Do not call these operations from a test against an account that holds real data. They cannot be undone, they produce fiscal documents, or they notify people.

- Validate or void an invoice, a credit note, a contract or a supplier invoice.
- Cancel or sign a contract.
- Receive a purchase order, return order lines or record a payment.
- Run a workflow.
- Cancel an order or remove its reservation.
- Delete a record.
- Create a note that mentions users: it notifies them.

| Operation | Scope |
| --- | --- |
| `DELETE /api/v1/contacts/{contactId}` | `contacts:write` |
| `DELETE /api/v1/contacts/{contactId}/addresses/{addressId}` | `contacts:write` |
| `DELETE /api/v1/items/{itemId}` | `items:write` |
| `DELETE /api/v1/spaces/{spaceId}` | `spaces:write` |
| `DELETE /api/v1/vehicles/{vehicleId}` | `vehicles:write` |
| `DELETE /api/v1/staff/{staffId}` | `staff:write` |
| `DELETE /api/v1/tags/{tagId}` | `tags:write` |
| `DELETE /api/v1/availability/unavailabilities/{unavailabilityId}` | `availability:write` |
| `POST /api/v1/notes` | `notes:write` |
| `DELETE /api/v1/tasks/{taskId}` | `tasks:write` |
| `POST /api/v1/purchase-orders/{purchaseOrderId}/receive` | `purchase-orders:write` |
| `POST /api/v1/treasury/payments` | `treasury:write` |
| `POST /api/v1/orders/{orderId}/cancel` | `orders:write` |
| `POST /api/v1/orders/line-items/returns` | `orders:write` |
| `POST /api/v1/workflows/{workflowId}/runs` | `workflows:run` |
| `POST /api/v1/invoices/{invoiceId}/validate` | `invoices:write` |
| `POST /api/v1/invoices/{invoiceId}/void` | `invoices:write` |
| `POST /api/v1/credit-notes/{creditNoteId}/validate` | `invoices:write` |
| `POST /api/v1/credit-notes/{creditNoteId}/void` | `invoices:write` |
| `POST /api/v1/contracts/{contractId}/validate` | `contracts:write` |
| `POST /api/v1/contracts/{contractId}/sign` | `contracts:write` |
| `POST /api/v1/contracts/{contractId}/void` | `contracts:write` |
| `POST /api/v1/contracts/{contractId}/cancel` | `contracts:write` |
| `POST /api/v1/supplier-invoices/{supplierInvoiceId}/validate` | `supplier-invoices:write` |
| `POST /api/v1/supplier-invoices/{supplierInvoiceId}/void` | `supplier-invoices:write` |

The table is generated from the same lists that drive the badges on the operation pages, so the two cannot disagree.

## If you must test in production

Prefer a separate account. If the only account is the real one, reduce the risk:

- Do the automations check above first.
- Mark every record you create so you can find it, avoid it and clean it up. Create a tag such as `api-test` with [`POST /api/v1/tags`](https://docs.filmo.io/rest-api/endpoint-reference/tags/create-a-tag) and attach it to each test record, and give test records an obvious name prefix. Exclude that tag and that prefix from your reports and from the integration's own queries.
- Use a fictitious contact (an email at `example.com`) for test orders and notes, so that no customer receives anything.
- Never call the operations in the table above.
- Use a key limited to the scopes of the test, and revoke it when you finish.

## Simulate the API locally

Without a sandbox, the only risk-free way to exercise writes, idempotency and error paths is a mock server built from the OpenAPI file:

```bash
npm install --global @stoplight/prism-cli
prism mock -d https://docs.filmo.io/openapi.json
```

Decimal fields come back as placeholder strings, not real amounts. See [OpenAPI file and generated clients](https://docs.filmo.io/rest-api/guides/openapi-and-clients) for the details and the limits of a mock.

## Exercise the failure paths

- 401: call with a wrong key and check that your code alerts rather than retrying.
- 403: call an operation with a key that lacks its scope and read `requiredScope` from the body.
- 429: lower your own limit in a test to force it, and check that you wait for `Retry-After`.
- A timeout during a write: kill the request after sending it, then retry with the same `Idempotency-Key` and confirm that only one record exists.

## Clean up

Not every record can be deleted, and some changes cannot be undone. Archiving an order makes it invisible to the API: its GET answers 404. Check what is possible for each kind of record in [Archiving and deleting](https://docs.filmo.io/further-reading/archiving-and-deleting) before you create test data you will want to remove.

---

Canonical URL: https://docs.filmo.io/rest-api/guides/testing  
Last updated: 2026-10-09