Skip to main content

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 shapeMethodWhere they go
{} (empty)GETnothing
{"foo": 1, "bar": 2} (non-empty)POSTJSON 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

KeyTypeEffect
_urlstr (required)Full URL (must start with http:// or https://).
_methodstrForces the HTTP method (GET, POST, PUT, PATCH, DELETE, ...).
_querydictAppended to the URL as query string, even when the method has a body.
_bodyanySent as the request body. Overrides the "rest params" behavior.
_headersdict[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.

Domain check is your responsibility

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 _url is missing or not a full http(s) URL.
  • Bad Request: The "custom" action cannot target the internal IP ... — Returned when _url resolves to a blocked literal.
  • Connector "..." does not support the "custom" action. — Returned by connectors that don't speak HTTP (database, SOAP).

Notes

  • custom appears in list_actions for every HTTP-based connector, with a payload schema that documents the reserved keys. Other catalogued actions still have full schema validation; custom does not (rest params are passed through).
  • The existing query string in _url is 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).