# External API calls

Comind.work action logic runs on the server in a restricted runtime. The standard browser APIs like `fetch()` and libraries like `axios` are not available. Instead, use the `RestApi` global to call external REST APIs from [action logic](/developer-guide/building-blocks/actions.md).

## The RestApi builder[​](#the-restapi-builder "Direct link to The RestApi builder")

`RestApi` exposes a fluent builder pattern. Every call follows the same four steps:

1. **Create a client** with a base URL.
2. **Create a request** with a path and HTTP method.
3. **Configure** authentication, headers, query parameters, and body.
4. **Execute** the request and process the response.

```
import { RestApi } from "@comind/api/server";

// 1. Base URL
RestApi.createClient("https://api.example.com");

// 2. Path + method
RestApi.createRequest("/v1/contacts", "GET");

// 3. Headers, auth, body (as needed)
RestApi.addHeader("Accept", "application/json");

// 4. Fire
const res = RestApi.executeRequest();
const data = JSON.parse(res.content);
```

The response object contains `statusCode`, `content` (the body as a string), and other metadata depending on the request type.

note

Each `createClient` / `createRequest` pair configures a single request. Call the pair again for each subsequent request.

## Authentication patterns[​](#authentication-patterns "Direct link to Authentication patterns")

### Basic auth[​](#basic-auth "Direct link to Basic auth")

```
RestApi.createClient("https://api.example.com");
RestApi.createRequest("/data", "GET");
RestApi.setHttpBasicAuth("username", "password");
const res = RestApi.executeRequest();
```

### Bearer token (OAuth 2)[​](#bearer-token-oauth-2 "Direct link to Bearer token (OAuth 2)")

```
RestApi.createClient("https://api.example.com");
RestApi.createRequest("/data", "GET");
RestApi.setOAuth2AuthorizationHeaderAuth(token);
const res = RestApi.executeRequest();
```

### API key in a header[​](#api-key-in-a-header "Direct link to API key in a header")

Some services expect a custom header instead of standard auth:

```
RestApi.createClient("https://api.example.com");
RestApi.createRequest("/data", "GET");
RestApi.addHeader("X-API-Key", apiKey);
const res = RestApi.executeRequest();
```

### Storing credentials securely[​](#storing-credentials-securely "Direct link to Storing credentials securely")

Never hardcode secrets in action logic. Store them as `COMIND_SECRET_*` environment variables and retrieve them at runtime with `AppSchema.getSecrets()`.

Define the secrets in your app's `settings/secrets.ts`:

```
// settings/secrets.ts
export const secrets = {
  crmApiKey: process.env.COMIND_SECRET_CRM_API_KEY,
  crmBaseUrl: process.env.COMIND_SECRET_CRM_BASE_URL,
};
```

Then read them inside action logic:

```
import { AppSchema, RestApi } from "@comind/api/server";

const { COMIND_SECRET_CRM_API_KEY, COMIND_SECRET_CRM_BASE_URL } =
  AppSchema.getSecrets();

if (!COMIND_SECRET_CRM_API_KEY) {
  throw "Missing secret: COMIND_SECRET_CRM_API_KEY";
}

RestApi.createClient(COMIND_SECRET_CRM_BASE_URL);
RestApi.createRequest("/contacts", "GET");
RestApi.addHeader("X-API-Key", COMIND_SECRET_CRM_API_KEY);
const res = RestApi.executeRequest();
```

See [Settings model](/developer-guide/building-blocks/settings-model.md) for the full details on variables, secrets, and the `settings/` directory structure.

## Making requests[​](#making-requests "Direct link to Making requests")

### GET with query parameters[​](#get-with-query-parameters "Direct link to GET with query parameters")

```
import { RestApi } from "@comind/api/server";

RestApi.createClient("https://api.example.com");
RestApi.createRequest("/contacts", "GET");
RestApi.addQueryParameter("status", "active");
RestApi.addQueryParameter("limit", "50");
RestApi.addQueryParameter("offset", "0");
const res = RestApi.executeRequest();

if (res.statusCode !== 200) {
  throw `GET /contacts failed with status ${res.statusCode}: ${res.content}`;
}

const contacts = JSON.parse(res.content);
console.log(`Fetched ${contacts.length} contacts`);
```

### POST with JSON body[​](#post-with-json-body "Direct link to POST with JSON body")

```
import { entity } from "#typings";
import { RestApi } from "@comind/api/server";

RestApi.createClient("https://api.example.com");
RestApi.createRequest("/contacts", "POST");
RestApi.addHeader("Content-Type", "application/json");
RestApi.addJsonBody({
  name: entity.title,
  email: entity.c_email,
  company: entity.c_company_name,
  source: "comind-sync",
});
const res = RestApi.executeRequest();

if (res.statusCode !== 201) {
  throw `POST /contacts failed: ${res.content}`;
}

const created = JSON.parse(res.content);
entity.c_external_id = created.id;
console.log(`Created external contact ${created.id}`);
```

### PUT to update an existing resource[​](#put-to-update-an-existing-resource "Direct link to PUT to update an existing resource")

```
RestApi.createClient("https://api.example.com");
RestApi.createRequest(`/contacts/${entity.c_external_id}`, "PUT");
RestApi.addHeader("Content-Type", "application/json");
RestApi.addJsonBody({
  name: entity.title,
  email: entity.c_email,
  updated_at: new Date().toISOString(),
});
const res = RestApi.executeRequest();

if (res.statusCode !== 200) {
  throw `PUT /contacts/${entity.c_external_id} failed: ${res.content}`;
}
```

### POST with file upload[​](#post-with-file-upload "Direct link to POST with file upload")

```
import { Files, RestApi } from "@comind/api/server";

const fileRef = Files.getFileRef(entity.attachments__list[0].file_uid);

RestApi.createClient("https://api.example.com");
RestApi.createRequest("/uploads", "POST");
RestApi.addHeader("Authorization", "Bearer " + token);
RestApi.addFile("file", fileRef);
const res = RestApi.executeRequest();
```

## Handling responses[​](#handling-responses "Direct link to Handling responses")

Every call to `executeRequest()` returns a response object. Always check the status code before processing the body.

```
const res = RestApi.executeRequest();

// Check for success
if (res.statusCode >= 200 && res.statusCode < 300) {
  const data = JSON.parse(res.content);
  console.log("Success:", JSON.stringify(data));
} else if (res.statusCode === 401) {
  throw "Authentication failed - check your API credentials";
} else if (res.statusCode === 404) {
  console.warn("Resource not found, skipping");
} else if (res.statusCode === 429) {
  throw "Rate limit exceeded - try again later";
} else {
  throw `Request failed with status ${res.statusCode}: ${res.content}`;
}
```

tip

Use `console.log()` to write request and response details to the server logs during development. This is the primary debugging tool for server-side action logic.

## Downloading files[​](#downloading-files "Direct link to Downloading files")

Use `RestApi.downloadFile()` instead of `executeRequest()` when the response is a binary file. The returned object includes a `downloadedFileRef` that you can persist and attach to a record.

```
import { entity } from "#typings";
import { Files, RestApi } from "@comind/api/server";

RestApi.createClient("https://reports.example.com");
RestApi.createRequest("/export/monthly-report.pdf", "GET");
RestApi.addHeader("Authorization", "Bearer " + token);
const res = RestApi.downloadFile();

if (res.statusCode !== 200 || !res.downloadedFileRef) {
  throw `File download failed: ${res.statusCode}`;
}

// Persist the temporary file reference
const stored = Files.persistFileRef(res.downloadedFileRef.ref);

// Attach to the record
entity.attachments__list ??= [];
entity.attachments__list.push({
  file_uid: stored.uploadedFileId,
  title: "monthly-report.pdf",
});

console.log(`Downloaded and attached report (${res.contentLength} bytes)`);
```

For a step-by-step walkthrough of the download-and-attach pattern, see [How to download a file by URL and save as attachment](/developer-guide/how-to/advanced/how-to-download-file-by-url-and-save-as-attachment.md).

## Complete example: sync contacts with an external CRM[​](#complete-example-sync-contacts-with-an-external-crm "Direct link to Complete example: sync contacts with an external CRM")

This example fetches contacts from an external CRM API, then creates or updates matching records in Comind.work using `ComindServer.importData`.

```
import { entity } from "#typings";
import { AppSchema, ComindServer, RestApi } from "@comind/api/server";

function sync_contacts() {
  // 1. Retrieve credentials from secrets
  const { COMIND_SECRET_CRM_API_KEY, COMIND_SECRET_CRM_BASE_URL } =
    AppSchema.getSecrets();

  if (!COMIND_SECRET_CRM_API_KEY || !COMIND_SECRET_CRM_BASE_URL) {
    throw "Missing CRM credentials. Configure COMIND_SECRET_CRM_API_KEY and COMIND_SECRET_CRM_BASE_URL in app secrets.";
  }

  // 2. Fetch contacts from external CRM
  RestApi.createClient(COMIND_SECRET_CRM_BASE_URL);
  RestApi.createRequest("/api/v2/contacts", "GET");
  RestApi.addHeader("X-API-Key", COMIND_SECRET_CRM_API_KEY);
  RestApi.addQueryParameter("updated_since", entity.c_last_sync_date || "");
  RestApi.addQueryParameter("limit", "200");
  RestApi.setRequestTimeout(30);

  const res = RestApi.executeRequest();

  if (res.statusCode !== 200) {
    throw `CRM API returned ${res.statusCode}: ${res.content}`;
  }

  const crmContacts = JSON.parse(res.content).data;
  console.log(`Fetched ${crmContacts.length} contacts from CRM`);

  if (crmContacts.length === 0) {
    entity.c_last_sync_status = "No new contacts";
    entity.c_last_sync_date = new Date().toISOString();
    return;
  }

  // 3. Map external data to Comind.work fields
  const mapped = crmContacts.map((c: any) => ({
    title: c.full_name,
    c_email: c.email,
    c_phone: c.phone,
    c_company_name: c.company,
    c_external_id: String(c.id),
    c_source: "crm-sync",
  }));

  // 4. Import into Comind.work - duplicates matched by c_external_id
  const result = ComindServer.importData({
    data: mapped,
    keyFields: ["c_external_id"],
  });

  console.log(
    `Import complete: ${result.created} created, ${result.updated} updated`
  );

  // 5. Update sync status on the trigger record
  entity.c_last_sync_status = `Synced ${crmContacts.length} contacts`;
  entity.c_last_sync_date = new Date().toISOString();
}

export default {
  sync_contacts,
} satisfies ActionLogic<typeof entity>;
```

### How the example works[​](#how-the-example-works "Direct link to How the example works")

1. **Credentials** are loaded from app secrets - never hardcoded.
2. **GET request** fetches updated contacts with query parameters for incremental sync.
3. **Timeout** is set to 30 seconds with `setRequestTimeout(30)` to avoid blocking indefinitely.
4. **Field mapping** transforms external field names to the app's field names.
5. **`ComindServer.importData`** handles upsert logic - it creates new records and updates existing ones, matching on `c_external_id`.
6. **Sync metadata** is stored on the trigger record for audit purposes.

## Tips[​](#tips "Direct link to Tips")

* **Synchronous execution**: `RestApi` runs synchronously on the server. Long-running requests block the action until they complete. Use `setRequestTimeout(seconds)` to set an upper bound for unreliable APIs.
* **Secrets only**: store all credentials in `settings/secrets.ts` using `COMIND_SECRET_*` environment variables. Never hardcode API keys, tokens, or passwords.
* **Debugging**: use `console.log()` to inspect requests and responses in the server logs. Temporarily store raw responses in a text field on the record (like the [AI action example](/developer-guide/how-to/advanced/how-to-create-ai-based-action.md) does with `c_json_response`).
* **No browser APIs**: `fetch()`, `XMLHttpRequest`, and `axios` are not available in the server runtime. `RestApi` is the only HTTP client.
* **Error handling**: always check `res.statusCode` before parsing `res.content`. Uncaught exceptions in action logic surface as action errors in the UI.
* **Pagination**: if the external API returns paginated results, loop with `createClient` / `createRequest` pairs, adjusting `offset` or `page` query parameters on each iteration.
* **Rate limits**: some APIs enforce rate limits. If you need to make many requests in a loop, consider adding `ComindServer.sleep(1000)` between calls to stay within limits.

## Related[​](#related "Direct link to Related")

* [How to create an AI-based action](/developer-guide/how-to/advanced/how-to-create-ai-based-action.md) - complete example using `RestApi` to call the OpenAI API from action logic
* [How to download a file by URL and save as attachment](/developer-guide/how-to/advanced/how-to-download-file-by-url-and-save-as-attachment.md) - download binary files and attach them to records
