Install the Secure Exchanges relay in your Azure subscription
From an empty Azure subscription to a working HTTP endpoint, callable from any webhook, automation platform or application. About ten minutes, nothing to install on your machine.
This guide takes you from an empty Azure subscription to a working HTTP endpoint you can call from anything that can send a JSON POST: a webhook, an automation platform, your own code.
Allow about ten minutes. You install nothing on your machine — everything happens in the browser.
What you need
- Your three Secure Exchanges licence credentials: serial number, API user and API password. Three GUIDs, shaped like
a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d. If you are an administrator, or if you have access to user management, you can retrieve them yourself: open User management, find the user and click the key icon in the Actions column. - An Azure subscription, with the Contributor role so you can create resources.
- Which environment your licence belongs to: production or preview. If unsure, try production first; if the test in step 7 returns an authorisation error, redo step 3 and pick preview.
Your credentials live in the configuration of your own Function App. They are never handed to the tool calling the relay, and never reach Secure Exchanges outside the usual encrypted protocol.
Step 1 — Open the template
Click the button:
The Azure portal opens a creation form. If you are not signed in yet, Azure asks for your credentials first.
Step 2 — Choose where to install
At the top of the form:
- Subscription: the one that will carry the cost.
- Resource group: click Create new and name it, for example
rg-secure-exchanges. A dedicated group makes it easy to remove everything later. - Region: pick the one closest to you.
Step 3 — Provide your credentials
The form first asks how you want to supply them.
Option A — type them in
Three fields, one GUID each: serial number, API user, API password. All three are masked as you type. The form still checks the format live, so a truncated paste or a value that is not a GUID is flagged right away — you never have to proofread a hidden field.
The three fields are declared as secure parameters: Azure does not record them in your resource group's deployment history, and no later look at that deployment brings them back. They are written to your Function App configuration, where they stay readable by anyone allowed to view that resource's application settings. If your policy rules that out too, use option B — the secret then never leaves your vault.
Option B — read them from your Azure Key Vault
Pick Read them from Azure Key Vault, then:
- Select your vault from the dropdown.
- Type the three secret names. Azure offers no picker for secret names, so copy them exactly as they appear in your vault. The suggested values —
se-serial,se-api-user,se-api-password— are only suggestions.
Your secrets then appear neither in the form nor in the deployment history: only a reference does. The app resolves the values at start-up using its managed identity.
One extra step after the deployment: let the app read the vault. In Cloud Shell, replacing the three names:
az role assignment create --role "Key Vault Secrets User" --assignee-object-id $(az functionapp identity show --resource-group <your-group> --name <your-app> --query principalId -o tsv) --assignee-principal-type ServicePrincipal --scope $(az keyvault show --name <your-vault> --query id -o tsv)
Then restart the app. Until that permission is granted, the app cannot resolve the references and the test in step 7 answers 500 Relay is not configured — a missing permission, not a typo.
This option requires Owner or User Access Administrator on the vault. A plain Contributor role cannot create a role assignment.
The remaining fields
| Field | What to put |
|---|---|
| Function App name | Leave empty and a unique name is generated. If you want a specific one, letters, digits and hyphens only |
| Secure Exchanges environment | Production, or Preview if your licence lives on the preview environment |
| Default language | The language your recipients see when a call does not specify one. Every call can override it with the culture field |
The Sizing tab holds memory and instance ceiling. The defaults suit a few hundred calls a month.
Resources are created in the region of the resource group chosen in step 2.
Step 4 — Create
Click Review + create, then Create. Azure takes about two minutes to provision:
- a storage account,
- a Flex Consumption plan, billed per use,
- the Function App itself, with your credentials already in place.
Step 5 — Install the code
The infrastructure is ready but the app is still empty. One command installs it.
- When the deployment finishes you are already on its page. In the left menu, under Settings, click Outputs. (If you closed that page: resource group → Settings → Deployments → the listed deployment → Outputs.)
- Copy the value of
step1_installationCommand. - At the top of the portal, click the Cloud Shell icon (
>_). Choose Bash if asked. On first use Azure offers to create a small storage account: accept. If your account has several subscriptions, select the right one first:
az account set --subscription "<your subscription name or id>"To list them all:
az account list --output table- Paste the command and press Enter.
The install takes a minute. Wait for Deployment was successful.
Step 6 — Get your URL
Still in the deployment Outputs, copy step2_relayUrlWithKey. It looks like:
https://se-relay-xxxxx.canadaeast-01.azurewebsites.net/api/enveloppe?code=<key>
This URL already carries the access key. Treat it like a password: whoever holds it can create envelopes and send messages on your licence.
The step4_accessKey output gives the key on its own. Use it to build the other routes — the same key works for /api/enveloppe, /api/message and /api/version.
The step3_testCommand output is a ready-made curl against /api/version, key included — the shortest way to confirm the relay answers.
Step 7 — Check that it works
In the same Cloud Shell, replacing the URL and the destination address:
curl -s -X POST "<your URL from step 6>" -H "Content-Type: application/json" -d '{"subject":"Test","destination":"you@yourcompany.com"}'
Expected answer:
{"status":200,"url":"https://www.secure-exchanges.com/...","repId":"...","error":null}
The url field is the envelope link. Open it in your browser to see what your recipient will see.
If you get something else:
| Answer | Cause | Fix |
|---|---|---|
401 with no JSON body |
The URL was truncated when copied | Copy step2_relayUrlWithKey in full |
{"status":500,...,"error":"Relay is not configured"} |
A credential was not saved | Function App → Settings → Environment variables, check SE_SERIAL, SE_API_USER, SE_API_PASSWORD |
{"status":502,...} |
Licence refused, usually the wrong environment | Switch SE_ENDPOINT to the other address (production ↔ preview) |
{"status":403,...} |
The licence has no API access | Contact Secure Exchanges |
404 |
The code is not installed | Redo step 5 |
Operations reference
All three operations share the same URL and the same key: only the end of the path changes.
| Route | What it does |
|---|---|
POST /api/enveloppe |
Creates an envelope link: your recipient sends documents back to you |
POST /api/message |
Sends a secure message to one or more recipients |
GET /api/version |
Reports your version and whether an update is available |
All expect Content-Type: application/json. The HTTP status of the answer always equals the status field in the body: rely on whichever you prefer.
POST /api/enveloppe
Creates a link you send to someone so they can send documents back to you, securely, without subscribing to anything or installing anything.
Required: subject and destination.
| Field | Type | Purpose |
|---|---|---|
subject |
text | Envelope subject, visible to the recipient |
destination |
text | A single email. An envelope is a personal reply link: for several people, call the endpoint once per recipient |
culture |
text | fr-CA or en-CA, for this call only. Otherwise the deployment default |
callBackParameters |
text | Free content returned to you in the API callback, handy to identify the file or client |
replyToAPI |
boolean | true to receive the reply through your API callback instead of by email. Default: false |
notifyWhenRead |
boolean | You are notified when it is opened. Default: true |
replyExpirationHours |
integer | How long the link lives, in hours |
maximumReplyOpenTime |
integer | Maximum number of opens |
authorizedExtensions |
list | Allowed extensions, for example [".pdf", ".jpg"]. Absent = no restriction |
curl -s -X POST "<your URL>/api/enveloppe?code=<key>" -H "Content-Type: application/json" -d '{"subject":"Requested documents","destination":"client@example.com","replyExpirationHours":72,"authorizedExtensions":[".pdf"]}'
{
"status": 200,
"url": "https://www.secure-exchanges.com/...&enveloppe=true",
"repId": "1b2c3d4e-....",
"error": null
}
url is the link to pass on. repId identifies the expected reply: keep it if you use the API callback, it lets you match the reply to the right file.
POST /api/message
Sends a secure message. Each recipient gets their own link, all share the password. Secure Exchanges sends the email itself: you provide no mail server.
Required: subject, message, and at least one entry in recipients.
| Field | Type | Purpose |
|---|---|---|
subject |
text | Email subject |
message |
text | Message body, HTML or plain text |
recipients |
list | The recipients: [{"email":"...","phone":"+15145551234"}]. At least one is required. Under msgSMSOnly, email becomes optional: a recipient reached by SMS alone is accepted |
password |
text | Password protecting the message, shared by all recipients |
culture |
text | fr-CA or en-CA, for this call only |
sendMode |
text | onlyEmail by default. msgSMSOnly, msgSMSCodeEmail, msgEmailCodeSms require a phone number for every recipient |
A recipient the relay cannot reach fails the call and names its index, for example recipients[1]: email is required. Nothing is dropped quietly: either the whole batch goes out, or you get an error pointing at the line to fix.
| notifyWhenRead | boolean | You are notified when it is opened. Default: true |
| clearTextNotification | boolean | Open notification in clear text rather than encrypted. Default: false |
| maximumOpenTime | integer | Opens before destruction, 1 to 99. Default: 5 |
| expirationMinutes | integer | How long the message lives. Default: 50400 |
| senderName | text | Name shown as the sender |
| callBackParameters | text | Free content returned when the message is read. Nothing confidential here |
curl -s -X POST "<your URL>/api/message?code=<key>" -H "Content-Type: application/json" -d '{"subject":"Your documents","message":"<p>Hello</p>","recipients":[{"email":"client@example.com"}],"password":"a-shared-secret"}'
{
"status": 200,
"recipients": [
{
"email": "client@example.com",
"status": 200,
"url": "https://www.secure-exchanges.com/...",
"messageId": "xH25g0f2...",
"trackingId": "4ea4a752-1972-4c76-9d82-63dda214a32f",
"openingCode": null
}
],
"error": null
}
The top-level status is 200 when the call went through; also check each recipient's status, one of them can be refused while the others succeed. trackingId is what you use to track or delete the message.
Attachments are not exposed by this route: shipping binary files does not fit in a plain JSON POST. Contact us if you need them.
GET /api/version
No body, no parameters. Reports what runs on your side and whether an update exists.
curl -s "<your URL>/api/version?code=<key>"
{
"status": 200,
"version": "1.1.0",
"publishedVersion": "1.2.0",
"updateAvailable": true,
"operations": ["POST /api/enveloppe", "POST /api/message", "GET /api/version"],
"error": null
}
It is also the safest test after an install: it creates nothing and sends nothing.
Status codes, common to all three routes
| Code | Meaning | What to do |
|---|---|---|
200 |
Success | — |
400 |
Invalid body or missing required field | The error field names the problem |
401 |
Key missing or invalid | Copy the full URL from step 6 |
403 |
Secure Exchanges refused the licence | Check the API access on your licence |
500 |
Relay misconfigured, or internal error | Check the credentials in the environment variables |
502 |
No answer from Secure Exchanges | Usually the wrong environment: production against preview |
Step 8 — You are ready
Your relay is live. You now have ordinary HTTP URLs you can call from anywhere that sends a JSON POST: a webhook, an automation platform, a CRM, a script, your own application.
| What you want to do | Where to point |
|---|---|
| Create a reply link | <your URL>/api/enveloppe?code=<key> |
| Send a secure message | <your URL>/api/message?code=<key> |
| Check the installed version | <your URL>/api/version?code=<key> |
The recipe is the same everywhere: POST (except /api/version, which is a GET), the Content-Type: application/json header, and the JSON body described in the previous step. The answer comes back as JSON: the link sits in url for an envelope, in recipients[0].url for a message.
If your tool asks you to pick a request type, choose the one that lets you write the body yourself — often called Custom Request or HTTP. Avoid modes that copy trigger fields in automatically: the body has to be exactly the expected JSON.
In the following Zap steps, the link sits in the url field of the answer, ready to drop into an email, an SMS or a CRM record. For a message send, it is recipients[0].url.
Make and Power Automate are configured the same way: an HTTP module, method POST, the full URL, the Content-Type header and the JSON body.
What it costs
Three items, all billed per use. With no calls, the relay costs practically nothing.
| Item | What is billed | Order of magnitude |
|---|---|---|
| Function App (Flex Consumption) | Execution time in GB-seconds, plus the number of executions | The main item, and it stays tiny |
| Storage account | A few megabytes: the relay package and its metadata | A few cents a month |
| Network egress | The calls to Secure Exchanges, a few kilobytes each | Negligible |
How to estimate it for your volume. A call takes one to three seconds: the relay sets up an encrypted exchange with Secure Exchanges, then waits for its answer. At 512 MB per instance, the default, that is roughly 0.5 to 1.5 GB-seconds per call.
For 1,000 envelopes a month: in the order of 500 to 1,500 GB-seconds and 1,000 executions. For 10,000 calls, multiply by ten. Either way you stay far below the amounts that justify budget monitoring.
For an exact figure, use the Azure pricing calculator: rates vary by region, and Microsoft applies a monthly free grant on consumption plans whose amount changes over time. Look up Azure Functions, pick the Flex Consumption plan, and enter the GB-seconds and execution count above.
What would push the bill up, if anything: raising the memory per instance without needing it — the relay never uses more than 512 MB — or turning on always-ready instances, which our template leaves off. The instance ceiling costs nothing by itself: it caps scale-out, it reserves no capacity.
Tighten access (optional)
The key in your URL already protects the endpoint and is revoked in one click: Function App → Functions → CreateEnveloppe → Function keys. If you want a second key, independent of the Azure one:
- Function App → Settings → Environment variables → add
RELAY_EXTRA_API_KEYwith a long random value. - In your tool, add the
x-relay-keyheader carrying that same value.
Any call without that header is then refused.
Step 9 — Know whether you are up to date, and update
Secure Exchanges publishes new versions of the relay: fixes, new operations. Your installation does not update itself, but it can tell you where it stands.
Check your version
Take your URL from step 6, change the end of the path to /api/version, and open it in your browser or in Cloud Shell:
curl -s "https://<your-app>.azurewebsites.net/api/version?code=<your-key>"
{
"status": 200,
"version": "1.1.0",
"publishedVersion": "1.2.0",
"updateAvailable": true,
"operations": ["POST /api/enveloppe", "POST /api/message", "GET /api/version"],
"error": null
}
| Field | What it tells you |
|---|---|
version |
What is running on your side right now |
publishedVersion |
The latest version published by Secure Exchanges |
updateAvailable |
true: an update is waiting. false: you are current |
operations |
The operations available in your version |
If publishedVersion and updateAvailable are null, your app could not reach the published version file. Nothing is broken: everything else works, only the comparison is unavailable.
Run the update
One command, the same as step 5. Find it in the portal: your resource group → Settings → Deployments → your deployment → Outputs → step1_installationCommand.
Paste it into Cloud Shell and wait for Deployment was successful.
Nothing else to do: your credentials, settings, keys and URL are all preserved. No button to click again, no form to fill in — only the code is replaced.
Call /api/version afterwards: version should now match publishedVersion.
If the update brings a new operation, it is reachable straight away with the same key. Keep your URL and change only the end of the path.
Remove everything
Delete the resource group created in step 2: that wipes the Function App and the storage in one go. Nothing is left behind on the Azure side, and your Secure Exchanges licence is unaffected.