# Connected services

> Call third-party APIs with ctx.http and auth aliases. Helix injects credentials server-side, so tokens never appear in your code or logs.

Your functions call third-party APIs through `ctx.http`, a [ky](https://github.com/sindresorhus/ky) HTTP client. For authenticated calls, `ctx.http.authed('alias')` routes the request through a Helix proxy that looks up the credentials and injects them server-side. Your code references an alias, never a token, so secrets stay out of your source, your logs, and your repository.

Check method signatures and options against your SDK version before you depend on them.

## How credential injection works

When you call `ctx.http.authed('salesforce_prod').get(url)`:

1. The SDK rewrites the request to the Helix proxy service, passing the target URL and the authentication UUID mapped to the alias.
2. The proxy fetches the credential record and refreshes the OAuth token if it has expired.
3. The proxy injects the credential according to the authentication's type, then forwards the request to the target API and streams the response back to your function.
4. The full request and response are logged with credentials redacted, tagged with your project ID, execution ID, and auth alias.

Credential data and injection rules live entirely on Helix's servers. Your project stores only a mapping from aliases to authentication UUIDs. For the trust boundaries behind this design, see [the security model](/documentation/concepts/security-model/).

## Where authentications come from

An authentication is the credential record itself: the account you connected, the tokens behind it, and the scopes it carries. Create authentications in your **Tray iPaaS account**, not in Helix. Helix then references them by UUID. The [supported services](/documentation/reference/supported-services/) reference lists the services you can connect and their authentication types.

| Type | What the proxy sends | Scopes |
|---|---|---|
| OAuth 2.0 | A token obtained from the service's OAuth flow, refreshed by the proxy when it expires | Chosen when you authorize the connection |
| API Token | A token or key you supply | Whatever the token was issued with |

Both types carry scopes, and the scopes decide what your app can do with the connected account. Grant the narrowest set the app needs: an authentication is shared by every function in the project, and if you widen who can open the published app, you widen who can act through that authentication. See [identity and roles](/documentation/guides/identity-and-roles/).

## Map aliases in helix.config.ts

```typescript
// helix.config.ts
import { defineConfig } from '@trayai/helix-sdk';

export default defineConfig({
  projectId: 'a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d',
  workspaceId: 'f8e7d6c5-4b3a-2190-8765-abcdef012345',

  authentications: {
    salesforce_prod: '1a0fbf5c-2c9e-4aa1-ada3-ccbba65019ab',
    slack: '7b2e4f91-3d1a-4c8b-9e5f-1234567890ab',
  },
});
```

Each key is an alias you choose. Each value is the UUID of an authentication created in your workspace. Calling `authed()` with an alias missing from this map throws immediately, and the error lists the aliases that are available.

## Connect a service

Once the authentication exists in Tray iPaaS, create the alias from the CLI:

```bash
# Interactive: pick a service and an authentication, and name the alias
helix auth connect [service]

# Non-interactive: map an alias straight to an authentication UUID
helix auth add salesforce_prod 1a0fbf5c-2c9e-4aa1-ada3-ccbba65019ab

# List the aliases mapped in this project
helix auth list
```

`helix auth connect` prompts you through the choice and writes the alias into `helix.config.ts`. `helix auth add` is the same result in one line when you already know the UUID.

You can also ask Claude. From inside your project, the agent can wire up an authenticated service and drive these commands for you. The full command surface is on the [auth CLI reference](/documentation/reference/cli/auth/).

## Make requests

Plain `ctx.http` is a standard ky instance: `searchParams` for query strings, `json:` for JSON bodies, `.json()` to parse responses. Authenticated calls through `ctx.http.authed('alias')` use a smaller shape. They take `query`, `body`, `headers`, and `timeout`, and they resolve to `{ status, body, headers }`. `body` is already parsed for a JSON response, and an object you pass as `body` is sent as JSON.

```typescript
// functions/api/v1/report.get.ts
import { defineFunction } from '@trayai/helix-sdk';

export default defineFunction(async (ctx) => {
  // Authenticated: proxied through Helix, credentials injected server-side
  const contacts = await ctx.http
    .authed('salesforce_prod')
    .get('https://acme.my.salesforce.com/services/data/v59.0/query', {
      query: { q: 'SELECT Id, Name FROM Contact' },
    });

  // Unauthenticated: direct request, still logged
  const weather = await ctx.http
    .get('https://api.open-meteo.com/v1/forecast', {
      searchParams: { latitude: 51.5, longitude: -0.1 },
    })
    .json();

  return { contacts: contacts.body, weather };
});
```

On plain `ctx.http`, every ky feature is available, including retries with backoff and per-request timeouts:

```typescript
const data = await ctx.http
  .get('https://flaky-api.example.com/data', {
    retry: { limit: 3, backoffLimit: 3000 },
    timeout: 10_000,
  })
  .json();
```

An authed client has no `.extend()`. To share a base URL and default headers across calls to one API, wrap the call in a small helper:

```typescript
const salesforce = (path: string, query?: Record<string, string>) =>
  ctx.http
    .authed('salesforce_prod')
    .get(`https://acme.my.salesforce.com/services/data/v59.0/${path}`, {
      query,
      headers: { 'Sforce-Auto-Assign': 'FALSE' },
    });

const contacts = await salesforce('query', { q: 'SELECT Id FROM Contact' });
const account = await salesforce('sobjects/Account/001xx');
```

Write full URLs. Where a service uses a per-account hostname, such as a Salesforce instance URL or a Jira subdomain, put the literal value in your code or read it from your own configuration.

## Automatic token refresh

If a target API returns a 401 and the credential is OAuth-based, the proxy refreshes the token and retries the request once. Expired tokens are also refreshed when the proxy resolves the credential record, before the first attempt. Your function sees only the final response and never handles refresh logic.

## Request logging and audit

Every authenticated request and response is logged with credentials redacted and tagged with project ID, execution ID, and auth alias. Unauthenticated `ctx.http` requests are logged too. Full request and response visibility is available in the [Helix dashboard](https://app.helix.tray.ai), so an admin can audit exactly which functions called which APIs under which authentication. Open an execution to see the third-party calls it made. See [observability](/documentation/guides/observability/).

## Worked example: Salesforce

```typescript
// functions/api/v1/salesforce/contacts.get.ts
import { z } from 'zod';
import { defineFunction } from '@trayai/helix-sdk';

export const input = z.object({
  limit: z.coerce.number().min(1).max(200).default(50),
});

interface SalesforceQueryResult {
  totalSize: number;
  done: boolean;
  records: Array<{ Id: string; Name: string; Email: string | null }>;
}

export default defineFunction<typeof input>(async (ctx) => {
  const res = await ctx.http
    .authed('salesforce_prod')
    .get('https://acme.my.salesforce.com/services/data/v59.0/query', {
      query: {
        q: `SELECT Id, Name, Email FROM Contact LIMIT ${ctx.input.limit}`,
      },
    });
  const result = res.body as SalesforceQueryResult;

  return {
    total: result.totalSize,
    contacts: result.records.map((r) => ({
      id: r.Id,
      name: r.Name,
      email: r.Email,
    })),
  };
});
```

## Worked example: Slack

Slack returns HTTP 200 with `ok: false` on failure, so check the body rather than the status code:

```typescript
// functions/api/v1/notify.post.ts
import { z } from 'zod';
import { defineFunction } from '@trayai/helix-sdk';

export const input = z.object({
  channel: z.string().min(1),
  text: z.string().min(1),
});

interface SlackPostMessageResponse {
  ok: boolean;
  ts?: string;
  error?: string;
}

export default defineFunction<typeof input>(async (ctx) => {
  const res = await ctx.http
    .authed('slack')
    .post('https://slack.com/api/chat.postMessage', {
      body: { channel: ctx.input.channel, text: ctx.input.text },
    });
  const result = res.body as SlackPostMessageResponse;

  if (!result.ok) {
    ctx.log.error('Slack rejected the message', { error: result.error });
    throw ctx.error(502, 'Slack API error');
  }

  return { messageTs: result.ts };
});
```

## Handle upstream errors

An authed call does not throw on a 4xx or 5xx response. The status comes back as a value, so check `res.status`, log the upstream body, and return a controlled error to your caller:

```typescript
export default defineFunction(async (ctx) => {
  const res = await ctx.http
    .authed('salesforce_prod')
    .get('https://acme.my.salesforce.com/services/data/v59.0/query', {
      query: { q: 'SELECT Id FROM Contact' },
    });

  if (res.status >= 400) {
    ctx.log.error('Salesforce error', { status: res.status, body: res.body });
    throw ctx.error(502, 'Upstream API error');
  }

  return res.body;
});
```

A redirect (3xx) is also returned, with its `location` header, instead of being followed. Two cases do throw an `SdkError`: an alias that isn't in `helix.config.ts` (the error lists the configured aliases), and a call where no response comes back, such as a network failure. Plain `ctx.http` behaves differently: like ky, it throws on any non-2xx status unless you pass `throwHttpErrors: false`.

## HTTP and HTTPS only

The proxy covers HTTP and HTTPS, which includes REST, GraphQL, SOAP, and webhooks. It is the only way a function reaches an outside system today: there is no database connection and no queue. For state between requests, use the project-scoped [key-value store](/documentation/guides/key-value-store/).

---

Canonical: https://helix.tray.ai/documentation/guides/connected-services/
Any link on this page is available as markdown by appending .md to its URL.
Full corpus: https://helix.tray.ai/documentation/llms-full.txt