Skip to main content

Call an outside service from a script

Goal​

Have a server-side script call something outside the platform — take a payment, tell another system a record changed, send somebody an email — and have the address it calls be a setting an administrator controls rather than a line in your script.

The two functions​

Both are available in a Server Plugin Step and in a Custom API.

var answer = callService('par_payment_service', '{"reference":"ORD-1","amount":25}');

var sent = sendEmail('par_mail_service', { to: 'buyer@example.test', subject: 'Your order is paid', body: 'Thank you.' });

The body can be text, as above, or an object or a list, which is sent as JSON: callService('par_payment_service', { reference: 'ORD-1', amount: 25 }) posts the same thing.

callService answers an object:

PropertyWhat it is
statusThe HTTP status the service replied with
oktrue for 200–299, so the common case is one property rather than arithmetic
bodyWhat it replied with, as text
truncatedtrue if the reply was too big to read in full

The first argument is a service name, not an address​

This is the part that surprises people, and it is the whole design.

'par_payment_service' is the logical name of an environment variable. You declare it in your solution; an administrator sets its value — the endpoint — in each environment. So:

  • your script says what it is calling, never where;
  • Dev points at a sandbox and Production points at the real provider, with no change to the solution;
  • an environment that has configured nothing reaches nothing.

There is deliberately no function that takes a URL. One would let any script in any tenant open a connection to anywhere at all, and no amount of reviewing solutions afterwards would take that back.

Set it up​

  1. Declare the service. In the Admin Control Center, open Environment Variables and create one — par_payment_service, type Text. Give it a description saying what it is for; the person setting it in Production may not be you.
  2. Add it to your solution, so it travels. A variable's declaration travels; its value does not, which is what keeps Dev's settings out of Production.
  3. Set the value in this environment — the endpoint, an absolute http:// or https:// address.
  4. Write the step. In your plugin step or Custom API, call callService with that name.

Which side of the save to call from​

A Server Plugin Step runs either before the record is written or after it, and outbound calls belong on different sides depending on what they are:

CallStageWhy
Taking a payment, reserving stock in another systemPreOperationIt happens inside the save's transaction, so if the record then fails to save, nothing is left claiming that money was taken
Sending a confirmation, notifying another systemPostOperationThe record exists by then, so a mail service being down delays a message rather than undoing the record

A call before the save gets ten seconds. It is holding the save open while it waits, and a person is watching a spinner. If the service you are calling needs longer than that, make the step Asynchronous — it then runs after the save, on its own, and can take as long as it needs.

What counts as failure​

The distinction is worth learning once, because it decides how much of your script you have to write:

  • A status the service chose is an answer. A payment provider's 402 means the card was declined — an ordinary outcome you decide about. Your script gets it in answer.status and carries on.
  • No answer at all is a failure, and it stops the step by itself: no such service, no endpoint set in this environment, an endpoint that is not a web address, or the service never replied. There is nothing a script can do about any of those, and carrying on would mean acting as though you had integrated with something.

So the shape of most steps is:

var answer = callService('par_payment_service', body);
if (answer.ok) {
setValue('par_payment_reference', answer.body);
} else {
throwValidationError(`The payment service refused this order (${answer.status}).`);
}

Sending email​

sendEmail takes the service's name and a message, and posts it to that service. The message is the platform's, so a deployment configures one mail adapter rather than one per script that ever sends a message.

In the messageWhat it is
toWho it goes to. One address, several separated by commas or semicolons, or a list
ccWho gets a copy, in the same way. Optional
replyToWhere a reply goes, so a customer answers the person who wrote rather than a no-reply address. Optional
subjectThe subject line
bodyWhat it says
htmltrue if the body is HTML. Optional; plain text otherwise

sendEmail never stops your script. It answers { ok, error }. When the email did not go, because nobody set up a mail service here, the address was refused or the mail service was down, ok is false and error says why, in words that say who can fix it. That lets a script record the failure and try again later, which is how Parsware Sales marks an email Failed. When the save should stop instead, say so:

if (!sent.ok) { throwValidationError(sent.error); }

A script written before this passed four arguments (sendEmail(service, to, subject, body)). It needs the message form above.

The platform does not speak SMTP. It posts to whatever the environment names, which every mail provider offers; a deployment whose only path is an SMTP relay puts a small HTTP endpoint in front of it.

Try it​

Import the business-rules-outbound-calls sample solution. It has an Orders table, two services declared as environment variables and nothing set, and two steps — one that takes payment before the save and one that emails the confirmation after it. Its README walks through pointing the services at an endpoint that answers 200, then at one that answers 402, and watching the save be refused with nothing stored.

See also​