Docs / Automation Plugins / Provider API Discovery

Provider API Discovery

Capture provider web requests and replay them through the agent's authenticated session.

Developer Preview: This workflow is for building reliable provider plugins. It uses a real managed browser session to discover the requests that later run through browser.sessionFetch.

Goal

Use visible browser work once to learn the provider’s request shape. Then run repeatable skills through authenticated provider APIs.

The default loop is:

open real session -> perform UI action -> capture request -> replay with sessionFetch -> validate shape

This keeps login, OTP, captcha, and consent screens in the browser while moving carts, checkout previews, account reads, order status, and profile updates into API helpers.

Capture a real session

Start from the same managed browser profile the plugin will use in production. Log in through setup or recover, then perform the provider action once through the UI.

Capture the network request that represents the action. For each candidate request, record:

  • method and URL
  • GraphQL operationName or REST endpoint
  • minimum request body
  • required variables
  • response shape
  • status codes and provider error payloads
  • headers that are stable enough to send again
  • whether the request needs the browser session cookies

Do not copy raw cookies, OTPs, tokens, or account secrets into plugin state. The runtime owns the session.

Replay through sessionFetch

Put the captured request behind a provider helper.

export async function getOrderStatus({
  browser,
  orderRef,
}: {
  browser: BrowserProfile;
  orderRef: string;
}) {
  const response = await browser.sessionFetch('https://api.sample.example/graphql', {
    method: 'POST',
    json: {
      operationName: 'OrderStatusQuery',
      variables: { orderRef },
      query: ORDER_STATUS_QUERY,
    },
  });

  if (response.status === 429) {
    return { ok: false as const, errorCode: 'rate_limited' };
  }

  if (!response.ok) {
    return { ok: false as const, errorCode: 'provider_unavailable' };
  }

  return {
    ok: true as const,
    data: OrderStatusResponse.parse(await response.json()),
  };
}

Pin the operationName and the smallest variable set that works. Keep the provider’s full response out of user-facing logs and plugin state.

Validate drift

Providers change request and response shapes. Treat that as normal.

Use lightweight shape validators in helpers:

import { z } from 'zod';

const OrderStatusResponse = z.object({
  data: z.object({
    order: z.object({
      status: z.string(),
      etaMinutes: z.number().optional(),
    }),
  }),
});

When validation fails, return provider_unavailable with retryable: true and log structured fields that help debug the shape change.

log.warn('Order status response changed', {
  operationName: 'OrderStatusQuery',
  status: response.status,
});

Add a canary skill

For high-value providers, add a read-only canary skill that exercises the smallest authenticated request. Run it before mutating flows or on a schedule controlled by the platform.

A good canary:

  • requires no user-visible state change
  • uses browser.sessionFetch
  • validates the response shape
  • returns rate_limited separately from provider_unavailable
  • logs the operation name or endpoint in fields

When to fall back to browser.act

Use browser.act when the provider step has no stable request to replay:

  • login
  • OTP entry
  • captcha or risk challenge
  • consent or terms screen
  • payment iframe or secure field
  • a flow where the provider intentionally requires visible UI confirmation

Do not use browser.act for repeated provider work when a request exists. A chain of UI clicks is slower, harder to test, and more likely to fail after small page changes.

Skill pattern

Keep the public skill stable and hide provider request details in helpers.

export const checkOrder: SkillHandler<{ orderRef: string }> = async ({
  input,
  browser,
  log,
}) => {
  const result = await providerApi.getOrderStatus({
    browser,
    orderRef: input.orderRef,
  });

  if (!result.ok) {
    return {
      ok: false,
      errorCode: result.errorCode,
      message: 'Could not load order status.',
      retryable: result.errorCode !== 'provider_rejected',
    };
  }

  log.info('Checked order status', {
    orderRef: input.orderRef,
    operationName: 'OrderStatusQuery',
  });

  return { ok: true, data: result.data };
};

The skill name and output can stay stable even when the provider changes its underlying endpoint.