Raw requests
outbound.raw sends a request to the vendor's own API by path, through the same rate budget and retries. It is refused for anything that is not a known read unless you turn writes on.
outbound.raw calls the vendor’s API by path and returns the response body as it came. It exists for the endpoint nobody has modelled yet.
import { createOutbound, OutboundError } from "outbound-sdk";
import { instantly } from "outbound-sdk/instantly";
import { fakeVendor } from "../retries/fake-vendor.ts";
// A stand-in vendor that answers 200 with an empty body, and a hook that
// prints each request the SDK sends.
const outbound = createOutbound({
adapter: instantly({ apiKey: "example-key" }),
fetch: fakeVendor(200).fetch,
hooks: {
onRequest: (event) => {
console.log("sent:", event.method, event.url);
},
},
});
console.log(outbound.raw.provider);
// A path on the vendor's API. The adapter adds the base URL and the key.
await outbound.raw.get("/campaigns", { query: { limit: 5, search: "q4" } });
// Instantly lists leads with POST. The adapter knows that one is a read.
await outbound.raw.request("POST", "/leads/list", { body: { limit: 5 } });
// Anything that is not a known read is refused before it is sent.
try {
await outbound.raw.request("DELETE", "/campaigns/abc");
} catch (error) {
if (OutboundError.is(error)) {
console.log(error.code, "-", error.message);
}
}
instantly
sent: GET https://api.instantly.ai/api/v2/campaigns?limit=5&search=q4
sent: POST https://api.instantly.ai/api/v2/leads/list
Unsupported - DELETE /campaigns/abc is not a known read on instantly. The SDK is read-only; pass allowWrites: true to createOutbound to permit it
The vendor in that sample is a stand-in that answers 200 with an empty body, so the sample can show which requests are sent and which are refused. It shows nothing about what Instantly returns.
Methods
| Member | Returns | Does |
|---|---|---|
raw.get(path, options?) |
Promise<Json> |
A GET. |
raw.request(method, path, options?) |
Promise<Json> |
Any of GET, POST, PUT, PATCH, DELETE. |
raw.provider |
string |
The adapter’s name. |
path is a path on the vendor’s API, such as /campaigns. The adapter puts its base URL in front and adds the credential.
RawRequestOptions:
| Option | Type | Holds |
|---|---|---|
query |
an object of strings, numbers and booleans | Query parameters. An undefined value is left out. |
body |
JSON | The request body. |
lane |
string |
A rate lane the adapter declares, for an endpoint with its own stricter limit. See rate limits. |
timeout, retries, deadline, maxRequests, signal |
The per-call options. |
What you get back
The response body, parsed as JSON. An empty body is null, and a body that is not JSON comes back as a string. There is no meta, no normalization, no schema check and no pagination: one call is one vendor request, and paging is the vendor’s own parameters.
The body is the vendor’s data untouched. A raw request does not pass through the plugins that wrap reads, so a cache does not store it and the redact plugin cannot mask it. For that reason raw is refused as Unsupported on an instance where a plugin has turned the native capability off, as redact does, and the message names the plugin.
What still applies
A raw request goes through the same request engine as every other read:
- The rate budget, shared with the normalized reads on the same key.
- Retries, timeouts,
deadlineandmaxRequests. - Hooks:
onRequestper attempt, andonActionoronErrorwith the actionraw. - Error mapping: a vendor failure is an
OutboundErrorwith acode.
Reads only
The SDK is read-only, and raw keeps that promise. Each adapter knows which of its vendor’s requests are reads. A GET is one. So are the POST endpoints a vendor uses for listing and searching, such as Instantly’s POST /leads/list above. Any other request is refused with Unsupported before it is sent.
allowWrites: true on createOutbound turns that check off for the instance.
The agent tools and the MCP server do not expose raw: see tools.
On a combined instance
A combined instance has several vendors behind it, so there is no one API for a path to mean. raw is refused there, and the message lists the members you can pick. Scope to one of them with outbound.workspace("provider:workspace").
import { createOutbound, OutboundError } from "outbound-sdk";
import { combine } from "outbound-sdk/combine";
import { heyreach } from "outbound-sdk/heyreach";
import { instantly } from "outbound-sdk/instantly";
import { fakeVendor } from "../retries/fake-vendor.ts";
const outbound = createOutbound({
adapter: combine([
instantly({ apiKey: "example-key", workspaceId: "acme" }),
heyreach({ apiKey: "example-key", workspaceId: "acme" }),
]),
fetch: fakeVendor(200).fetch,
hooks: {
onRequest: (event) => {
console.log("sent:", event.method, event.url);
},
},
});
// Two vendors are behind this instance, so there is no one API to call.
try {
await outbound.raw.get("/campaigns");
} catch (error) {
if (OutboundError.is(error)) {
console.log(error.code, "-", error.message);
}
}
// The label alone still covers both tools, so name one member as
// `provider:workspace`.
const email = outbound.workspace("instantly:acme");
console.log(email.raw.provider);
await email.raw.get("/campaigns");
Unsupported - raw is not available on a combined instance: it belongs to one vendor. Use outbound.workspace("provider:workspace").raw, for example "instantly:acme" or "heyreach:acme"
instantly
sent: GET https://api.instantly.ai/api/v2/campaigns
A workspace label on its own is enough when only one adapter carries it. When a client is on two tools under one label, as above, the label still means both, and raw stays refused until you name the provider too.
outbound.native follows the same rule.