The custom action
Every HTTP/REST connector exposes a built-in action named custom that lets you call any endpoint of the connected service — including endpoints not catalogued in the connector's schema (beta, internal, custom routes) — while still reusing the connection's authentication (Bearer token, Basic auth, refresh token, etc.).
Supported on every HTTP/REST connector (Stripe, Slack, HubSpot, Totvs, GitHub, Notion, and others). Not supported on database connectors (Postgres, MySQL, SQL Server, Snowflake) or SOAP connectors — for those, a URL has no meaning.
Syntax
from abstra.connectors import run_connection_action
result = run_connection_action(
"my_stripe_connection",
"custom",
{
"_url": "https://api.stripe.com/v1/charges",
"amount": 1000,
"currency": "usd",
},
)
The full URL is passed in the reserved _url param. All other reserved keys (_method, _query, _body, _headers) work the same way.
How method, query, and body are decided
By default, your "rest" params (everything except the reserved _url / _method / _query / _body / _headers keys) flow naturally:
| Rest-params shape | Method | Where they go |
|---|---|---|
{} (empty) | GET | nothing |
{"foo": 1, "bar": 2} (non-empty) | POST | JSON body |
When the method is POST, PUT, or PATCH, a Content-Type: application/json header is set automatically and the rest params are serialized as the request body.
When the method is GET, DELETE, or HEAD, the rest params are serialized into the query string instead.
Reserved keys
| Key | Type | Effect |
|---|---|---|
_url | str (required) | Full URL (must start with http:// or https://). |
_method | str | Forces the HTTP method (GET, POST, PUT, PATCH, DELETE, ...). |
_query | dict | Appended to the URL as query string, even when the method has a body. |
_body | any | Sent as the request body. Overrides the "rest params" behavior. |
_headers | dict[str, str] | Merged on top of default headers (including the auth header). |
Any other keys in params are treated as "rest params" and flow according to the table above. Keys starting with _ are reserved — don't use them as real request params.
Examples
Plain GET (no params):
run_connection_action(
"my_slack_connection",
"custom",
{"_url": "https://slack.com/api/auth.test"},
)
GET with query string — two equivalent forms:
# (a) Query baked into the URL
run_connection_action(
"my_stripe_connection",
"custom",
{"_url": "https://api.stripe.com/v1/charges?limit=10&status=succeeded"},
)
# (b) Query expressed as params (requires _method='GET' since rest params is non-empty)
run_connection_action(
"my_stripe_connection",
"custom",
{
"_url": "https://api.stripe.com/v1/charges",
"_method": "GET",
"limit": 10,
"status": "succeeded",
},
)
# Both → GET https://api.stripe.com/v1/charges?limit=10&status=succeeded
Form (a) is the simplest when the query values are static. Form (b) is handier when values come from variables (no manual URL escaping). You can also mix them — an existing query string in _url is preserved and any extra params are appended.
POST with JSON body (the default for non-empty rest params):
run_connection_action(
"my_stripe_connection",
"custom",
{
"_url": "https://api.stripe.com/v1/customers",
"email": "ana@example.com",
"name": "Ana",
},
)
# → POST https://api.stripe.com/v1/customers
# Content-Type: application/json
# body: {"email": "ana@example.com", "name": "Ana"}
POST with both query and body:
run_connection_action(
"my_api_connection",
"custom",
{
"_url": "https://api.example.com/v1/items",
"_method": "POST",
"_query": {"dry_run": True},
"_body": {"name": "Widget", "price": 99},
},
)
# → POST https://api.example.com/v1/items?dry_run=true
# body: {"name": "Widget", "price": 99}
DELETE with a query param (use _body to force a body):
run_connection_action(
"my_api_connection",
"custom",
{
"_url": "https://api.example.com/v1/items/123",
"_method": "DELETE",
"reason": "duplicate",
},
)
# → DELETE https://api.example.com/v1/items/123?reason=duplicate
Custom header (overrides the default Content-Type):
run_connection_action(
"my_api_connection",
"custom",
{
"_url": "https://api.example.com/v1/events",
"_headers": {"Content-Type": "application/vnd.events+json", "X-Trace-Id": "abc"},
"_body": {"type": "ping"},
},
)
Authentication
The connection's credentials are applied exactly as they are for catalogued actions — there is no separate auth path. You don't pass tokens manually; the connector injects them.
The connector attaches its credentials to whatever URL you provide. Only call URLs you trust.
SSRF protections
_urls whose host is an IP literal in a non-routable range are rejected before the request is dispatched.
Errors
{ "status": "error", "message": "..." }— Returned for non-2xx responses, with the upstream body included. Same shape as catalogued actions.Bad Request: The "custom" action requires "_url" ...— Returned when_urlis missing or not a full http(s) URL.Bad Request: The "custom" action cannot target the internal IP ...— Returned when_urlresolves to a blocked literal.Connector "..." does not support the "custom" action.— Returned by connectors that don't speak HTTP (database, SOAP).
Notes
customappears inlist_actionsfor every HTTP-based connector, with a payload schema that documents the reserved keys. Other catalogued actions still have full schema validation;customdoes not (rest params are passed through).- The existing query string in
_urlis preserved when extra query params are added (e.g.,_url="https://api.x.com/items?source=cli"+_query={"page": 2}→https://api.x.com/items?source=cli&page=2).