Skip to content

Pre-GA Design Partner and Early Access only. Request access

Helix Docs

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.

For developers Updated Aug 5, 2026
View as Markdown

Your functions call third-party APIs through ctx.http, a 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.

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.

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.

TypeWhat the proxy sendsScopes
OAuth 2.0A token obtained from the service’s OAuth flow, refreshed by the proxy when it expiresChosen when you authorize the connection
API TokenA token or key you supplyWhatever 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.

Map aliases in helix.config.ts

// 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:

# 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.

Make requests

ctx.http is a standard ky instance, so requests look like ky everywhere else: searchParams for query strings, json: for JSON bodies, .json() to parse responses.

// 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', {
      searchParams: { q: 'SELECT Id, Name FROM Contact' },
    })
    .json();

  // 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, weather };
});

Every ky feature is available, including retries with backoff and per-request timeouts:

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

Because authed() returns a normal ky instance, you can .extend() it into a pre-configured client for one API:

const sf = ctx.http.authed('salesforce_prod').extend({
  prefixUrl: 'https://acme.my.salesforce.com/services/data/v59.0',
  headers: { 'Sforce-Auto-Assign': 'FALSE' },
});

const contacts = await sf.get('query', {
  searchParams: { q: 'SELECT Id FROM Contact' },
}).json();

const account = await sf.get('sobjects/Account/001xx').json();

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, 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.

Worked example: Salesforce

// 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 result = await ctx.http
    .authed('salesforce_prod')
    .get('https://acme.my.salesforce.com/services/data/v59.0/query', {
      searchParams: {
        q: `SELECT Id, Name, Email FROM Contact LIMIT ${ctx.input.limit}`,
      },
    })
    .json<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:

// 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 result = await ctx.http
    .authed('slack')
    .post('https://slack.com/api/chat.postMessage', {
      json: { channel: ctx.input.channel, text: ctx.input.text },
    })
    .json<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

ky throws on 4xx and 5xx responses. Catch the error, log the upstream body, and return a controlled error to your caller:

export default defineFunction(async (ctx) => {
  try {
    return await ctx.http
      .authed('salesforce_prod')
      .get('https://acme.my.salesforce.com/services/data/v59.0/query', {
        searchParams: { q: 'SELECT Id FROM Contact' },
      })
      .json();
  } catch (error: any) {
    if (error.response) {
      // HTTP error from the target API (4xx, 5xx)
      const body = await error.response.json();
      ctx.log.error('Salesforce error', { status: error.response.status, body });
      throw ctx.error(502, 'Upstream API error');
    }
    // Network error, proxy error, etc.
    throw error;
  }
});

HTTP and HTTPS only

The proxy covers HTTP and HTTPS, which includes REST, GraphQL, SOAP, webhooks, and AWS APIs. 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.

Loading search…

Jump to a section

tab to move · esc to close