TotalBase
Steps

Integration

HTTP request, SOAP request and Sign: call another system.

In the admin: Workflows › a Workflow › a step

HTTP request calls another system's API. With a Database event trigger, it's how TotalBase sends an outgoing webhook: Acme's Accounting sync sends every new invoice to the accounting system.

The HTTP request step of Accounting sync: POST to the accounting API, with its key as a secret header.
The HTTP request step of Accounting sync: POST to the accounting API, with its key as a secret header.

Fields

Field
MethodGET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS
URLRequired. An absolute http or https URL, with {{…}}
HeadersA JSON object: header names and their values, with {{…}}
Secret headersHeaders whose value is a secret from Settings › Secrets. Add a secret header, then its name and the secret
BodyAny JSON value, with {{…}}. Sent as JSON, with content-type: application/json unless a header sets another. Ignored for GET, DELETE, HEAD and OPTIONS

Accounting sync:

FieldValue
MethodPOST
URLhttps://accounting.example/api/invoices
Secret headersAuthorization ← the secret accounting_api_key
Body{"number": "{{start.data.number}}", "customer": "{{start.data.customer}}", "issuedOn": "{{start.data.issued_on}}", "dueOn": "{{start.data.due_on}}"}
Never put a credential in Headers or the URL: they're stored in the workflow's design, its versions and its run history, where every administrator can read them. A secret header stores only the secret's name; its value is decrypted when the step runs and never stored in the workflow or its history.

The result

{ "status": 201, "headers": { "content-type": "application/json" }, "body": { "id": "AC-55120" }, "durationMs": 182 }

body is parsed as JSON when the response says it's JSON, text otherwise. A later step reads {{sync.data.body.id}}.

A response that isn't 2xx fails the step. So do a network error and a timeout. The error's detail says what happened: method, URL, duration and, for a response, its status, headers and body. It never includes your request's headers or body. Connect the Error output to react, for example by emailing someone.

Limits

Timeout30 seconds for the whole exchange, redirects included
Redirectsat most 5, each one checked like the first address
Request bodyworkflows.http_max_request_bytes, 1 MB by default. A larger body fails the step before anything is sent
Responseworkflows.http_max_response_bytes, 5 MB by default. A larger response is cut off and fails the step

Both size limits fail with WORKFLOW_LIMIT_EXCEEDED. They're set in Settings › Features › Workflows.

There are no retries. A request that timed out may still have reached the other system: if it can take an idempotency key, send one, such as the invoice's number. Cancelling a run aborts a request in progress.

Nothing of whoever started the run is forwarded: no cookie, no Authorization. The request carries only what the step configures.

Where it may connect

A step can only reach public addresses. It refuses, with WORKFLOW_HTTP_DESTINATION_BLOCKED:

  • loopback (127.0.0.0/8, ::1) and the unspecified address;
  • private networks (10/8, 172.16/12, 192.168/16, fc00::/7) and carrier-grade NAT (100.64/10);
  • link-local addresses, cloud metadata (169.254.169.254) included;
  • multicast, reserved, broadcast and documentation ranges, and IPv6 forms that embed one of these.

The check is made on every address the name resolves to, at the moment of connecting, and again on every redirect, so a DNS answer that changes in between can't slip through. On a redirect to another origin, authorization, cookie and proxy-authorization headers are dropped.

To reach a system on your own network, such as an internal ERP, add it to HTTP request allowed hosts (workflows.http_allowed_hosts) in Settings › Features › Workflows: a hostname (exact match), an IP address or a CIDR range. It applies to every workflow of the instance.

SOAP request

SOAP request calls one operation of a SOAP web service - a government or bank web service, an older ERP. You describe the call; TotalBase writes the XML envelope, sends it and gives the answer back as JSON.

Fields

Field
Endpoint URLRequired. The service's endpoint, an absolute http or https URL, with {{…}}. Not the WSDL's address
SOAP version1.1 (most services, AFIP included) or 1.2
OperationRequired. The operation's element name, as its WSDL names it: getPersona
NamespaceRequired. The operation's XML namespace, the WSDL's targetNamespace
SOAPActionThe value the WSDL declares for the operation. Leave it empty when the WSDL declares an empty one
BodyA JSON object: the operation's parameters, with {{…}}. A key is an element name, an array repeats the element, null leaves it out
List elementsElement names of the response always read as a list, comma-separated - so {{…}} reads the same shape whether the service answered one or many
SOAP headersA JSON object sent in the envelope's Header, the same way as the body
HTTP headers, Secret headersAs HTTP request's

Where the values come from: the service's WSDL (…?WSDL) lists its endpoint, operations, each one's soapAction and namespace, and the parameters each takes. Read it once and fill the step; the step never reads it.

Padrón lookup - the AFIP taxpayer registry, with the access ticket read from a table by an earlier step ticket:

FieldValue
Endpoint URLhttps://aws.afip.gov.ar/sr-padron/webservices/personaServiceA13
OperationgetPersona
Namespacehttp://a13.soap.ws.server.puc.sr/
SOAPAction(empty)
Body{"token": "{{ticket.data.0.token}}", "sign": "{{ticket.data.0.sign}}", "cuitRepresentada": "20123456789", "idPersona": "{{start.data.cuit}}"}
List elementsdomicilio

What is sent, built by TotalBase:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns="http://a13.soap.ws.server.puc.sr/">
  <soap:Body>
    <ns:getPersona>
      <token>…</token><sign>…</sign>
      <cuitRepresentada>20123456789</cuitRepresentada>
      <idPersona>20301234567</idPersona>
    </ns:getPersona>
  </soap:Body>
</soap:Envelope>

The result

{ "status": 200, "headers": { "content-type": "text/xml;charset=utf-8" }, "body": { "personaReturn": { "persona": { "apellido": "PEREZ", "nombre": "JUAN", "domicilio": [ { "tipoDomicilio": "FISCAL", "direccion": "AV SIEMPRE VIVA 742" } ] } } }, "durationMs": 940 }

body is the content of the envelope's Body, as JSON, namespace prefixes dropped: a later step reads {{padron.data.body.personaReturn.persona.apellido}}. Numbers and booleans are typed; a value with a leading zero (a postal code) stays text.

A SOAP Fault - how a service reports an error, "No existe persona con ese Id" - fails the step with WORKFLOW_SOAP_FAULT; the error's message carries the fault's text and its detail the fault's code and detail. A response that isn't a SOAP envelope, a non-2xx status, a network error and a timeout fail the step as HTTP request's do. Connect the Error output to react.

Limits and where it may connect

The same as HTTP request's: 30 seconds, 5 redirects, workflows.http_max_request_bytes for the envelope, workflows.http_max_response_bytes for the answer, public addresses only unless listed in HTTP request allowed hosts. No retries. Nothing of whoever started the run is forwarded.

Not covered: reading the WSDL for you, WS-Security signatures, attachments (MTOM), a raw XML body. A call that must be signed first - AFIP's WSAA login - signs in a Sign step before this one.

Sign

Sign signs a text with a certificate and its private key, for a service that wants proof of who is calling. AFIP's web services work this way: a login service (WSAA) takes a signed ticket request and answers an access ticket the other services take for 12 hours.

Before: the certificate in Secrets

The certificate is made outside TotalBase, once. With OpenSSL, you create a private key and a certificate request, the issuer (AFIP, in its web portal) signs it, and you get the certificate back:

openssl genrsa -out private.key 2048
openssl req -new -key private.key -subj "/C=AR/O=Empresa Ejemplo/CN=totalbase/serialNumber=CUIT 20123456789" -out request.csr

In Settings › Secrets, add two secrets and paste each file's whole text, -----BEGIN …----- lines included: the certificate (for example afip_cert) and the private key (afip_key). If the key is encrypted, a third secret holds its passphrase. Don't keep them in Files: a file can be downloaded by anyone allowed on its folder, a secret is encrypted and never shown again.

When the certificate is renewed, replace the secret's value. The next run signs with the new one; there's nothing to change or republish.

Fields

Field
FormatCMS / PKCS#7: the text attached, the certificate included, SHA-256, in base64 - what openssl cms -sign -nodetach -outform DER produces
DataRequired. The text to sign, with {{…}} - usually built by an earlier step
CertificateRequired. The secret holding the certificate
Private keyRequired. The secret holding its private key, an RSA key
PassphraseOnly for an encrypted key: the secret holding its passphrase

The result

{ "format": "CMS", "signature": "MIIG…", "certificate": { "subject": "CN=totalbase\nserialNumber=CUIT 20123456789", "issuer": "CN=Computadores\nO=AFIP\nC=AR", "serialNumber": "1A2B…", "validFrom": "2026-10-15T00:00:00.000Z", "validTo": "2028-10-14T00:00:00.000Z" } }

A later step sends {{sign.data.signature}}. certificate.validTo tells when to renew: a Condition on it can email someone a month before.

The step fails with WORKFLOW_SIGN_FAILED before signing anything when the certificate or the key can't be used: not PEM, a wrong passphrase, not an RSA key, a key that isn't the certificate's, or a certificate that expired or isn't valid yet. The message names the secret, never its content. A secret that doesn't exist fails with SECRET_NOT_FOUND.

Example: an AFIP access ticket

Renew WSAA ticket, a Schedule every 6 hours, keeps one ticket per service in a table afip_tickets (service, token, sign, expires_at):

StepKindWhat it does
currentRead recordsthe afip_tickets row of ws_sr_padron_a13 whose expires_at is still ahead
neededConditionno row: go on; otherwise finish - WSAA refuses a new ticket while one is valid
build_ticketExecute JavaScriptthe ticket request, below
signSignData {{build_ticket.data}}, Certificate afip_cert, Private key afip_key
loginSOAP requesthttps://wsaa.afip.gov.ar/ws/services/LoginCms, operation loginCms, namespace http://wsaa.view.sua.dvadac.desein.afip.gov, SOAPAction empty, Body {"in0": "{{sign.data.signature}}"}
ticketExecute JavaScriptreads token, sign and expiration from the answer
saveCreate recordsthe new afip_tickets row

build_ticket:

const now = new Date();
const iso = (d) => d.toISOString().replace(/\.\d+Z$/, "Z");
return `<?xml version="1.0" encoding="UTF-8"?><loginTicketRequest version="1.0"><header><uniqueId>${Math.floor(now / 1000)}</uniqueId><generationTime>${iso(new Date(now - 60000))}</generationTime><expirationTime>${iso(new Date(+now + 600000))}</expirationTime></header><service>ws_sr_padron_a13</service></loginTicketRequest>`;

ticket - WSAA answers the ticket as XML text, inside loginCmsResponse.loginCmsReturn:

const xml = results.login.data.body.loginCmsResponse.loginCmsReturn;
const tag = (name) => xml.match(new RegExp(`<${name}>([^<]*)</${name}>`))[1];
return { token: tag("token"), sign: tag("sign"), expires_at: tag("expirationTime") };

Any other workflow then reads the ticket from afip_tickets and calls the service with a SOAP request, as in Padrón lookup above. Homologation (testing) uses wsaahomo.afip.gov.ar and its own certificate: two more secrets and the other URL.

Calling another workflow

Don't call a TotalBase workflow's URL with an HTTP request. Use Execute another workflow: it keeps the chain of workflows, so loops are refused, and shows in the called workflow's Called by.

Next steps

  • Database event: the trigger behind an outgoing webhook.
  • Secrets: storing an API key, a certificate and its key.