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:
| Property | What it is |
|---|---|
status | The HTTP status the service replied with |
ok | true for 200–299, so the common case is one property rather than arithmetic |
body | What it replied with, as text |
truncated | true 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
- 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. - 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.
- Set the value in this environment — the endpoint, an absolute
http://orhttps://address. - Write the step. In your plugin step or Custom API, call
callServicewith 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:
| Call | Stage | Why |
|---|---|---|
| Taking a payment, reserving stock in another system | PreOperation | It 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 system | PostOperation | The 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
402means the card was declined — an ordinary outcome you decide about. Your script gets it inanswer.statusand 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 message | What it is |
|---|---|
to | Who it goes to. One address, several separated by commas or semicolons, or a list |
cc | Who gets a copy, in the same way. Optional |
replyTo | Where a reply goes, so a customer answers the person who wrote rather than a no-reply address. Optional |
subject | The subject line |
body | What it says |
html | true 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
- Write a file to storage from a script — the other thing a script can reach outside the database.
- Publish a Custom API — the contract an outside system calls you through.