Trustio API
Send review invitations to your guests directly from the system you already use — your PMS, booking platform, CRM or loyalty tool. One endpoint, one API key, no new software for your staff to learn.
Overview
The Trustio API has a single purpose: your system tells us to send a review invitation, and we deliver it by SMS or Viber. Everything else — message templates, delivery, quota, unsubscribes, the review landing page — is handled on our side.
A typical integration fires one request when a guest checks out or an order is closed. That is the entire flow.
You do not need a developer for every setup. If your system can call a webhook or is available on Zapier, the whole integration is a URL and a key. See No-code.
Getting started
- Sign in to app.trustio.bg as the account owner.
- Go to Настройки (Settings) and scroll to the API достъп section at the bottom.
- Click Създай ключ (Create key), give it a name that identifies the system — for example
Booking system. - Copy the key immediately. It is shown only once and cannot be recovered afterwards.
- Send one test request using the examples below.
The key is shown once. Trustio stores only a cryptographic hash of it, so nobody — including our support team — can retrieve it later. If it is lost, revoke it and create a new one.
Authentication
Pass your key as a bearer token in the Authorization header. Never put it in a URL, a query string or client-side code.
Authorization: Bearer trst_a7f3c81e94082591d3937a6669a76f0af0a31cd52571e37
Each business can hold up to five active keys. Use a separate key per connected system, so revoking one never affects the others. Keys can be revoked at any time from the same Settings screen; a revoked key stops working immediately.
A key can only send invitations. It cannot read your customer list, view reviews, or change any setting.
Send an invitation
Send a JSON body with Content-Type: application/json.
| Field | Type | Description |
|---|---|---|
external_id required |
string | Your own reference for this send — a booking number, order ID or reservation code. Used to prevent duplicate invitations. Max 128 characters. |
phone required |
string | The guest's mobile number. See Phone numbers for accepted formats. |
message required |
string | The message text, including any placeholders. See Message content. |
name optional |
string | The guest's name, used to personalise the message. If omitted, the phone number is used instead. |
channel optional |
string | sms or viber. Defaults to sms. Viber requires the Растеж plan. |
// request body { "external_id": "booking-88123", "phone": "+4915112345678", "name": "Klaus", "message": "Zdraveite {име}, ostavete otziv: {линк}", "channel": "sms" }
Response
A successful request returns HTTP 200. The invitation is queued and delivered within about a minute.
{
"status": "queued",
"message_id": "266351e9-5671-45c2-92cb-570a7b885138",
"external_id": "booking-88123",
"phone": "+4915112345678",
"channel": "sms",
"duplicate": false
}
| Field | Meaning |
|---|---|
status | queued — accepted and scheduled for delivery. duplicate — this external_id was already used; nothing new was sent. |
message_id | Trustio's internal identifier for the message. |
phone | The number after normalisation to E.164 format. |
duplicate | true when the request was recognised as a repeat. |
Every response — success or failure — is a JSON object. The API never returns an empty body.
Duplicate protection
The external_id field is what stops a guest receiving the same invitation twice. Before sending, Trustio checks whether that identifier has already been used by your business. If it has, the original result is returned with "duplicate": true and nothing is sent or charged.
This means retries are safe. If your system loses the connection mid-request and retries, the guest still receives exactly one message.
Use a stable identifier from your own system. A booking number, reservation code or order ID works well. Do not generate a random value per attempt — that defeats the protection entirely.
The identifier is unique within your business, so two different hotels using Trustio can both use booking-1 without any conflict.
Phone numbers
Trustio normalises numbers to E.164 before sending. Both Bulgarian and international numbers are supported.
| You send | Delivered to | Notes |
|---|---|---|
0888123456 | +359888123456 | Bulgarian national format |
359888123456 | +359888123456 | Country code without plus |
+4915112345678 | +4915112345678 | International, as sent |
0048512345678 | +48512345678 | 00 prefix is converted |
48512345678 | rejected | Foreign number without + or 00 |
Foreign numbers must carry a plus sign or a leading 00. Without one, a number cannot be distinguished from a malformed Bulgarian number, so it is rejected rather than guessed. If your system stores country codes separately, concatenate them with a + before sending.
Message content
You supply the full message text. Three placeholders are replaced by Trustio at send time. They are written in Cyrillic, matching what business owners see in the app.
| Placeholder | Replaced with |
|---|---|
{име} | The value of name, or the phone number if none was given |
{бизнес} | Your business name as configured in Settings |
{линк} | A unique review link for this guest — always include this |
Messages containing blocked words are rejected. This protects your sender reputation with the mobile operators.
SMS and Viber
SMS
SMS is delivered in Latin characters only — a requirement of the Bulgarian mobile operators. Cyrillic text is transliterated automatically, so Здравейте becomes Zdraveite.
Keep the final message under 160 characters. Longer messages are split into multiple SMS segments, and each segment is deducted from your quota separately. Remember that {линк} expands to a full URL, so leave room for it — around 30 characters is a safe allowance.
Viber
Viber supports Cyrillic and allows up to 1000 characters, so no transliteration is applied. Viber sending requires the Растеж plan; a request with "channel": "viber" on a lower plan is rejected.
Note that Viber has strong coverage in Bulgaria and the Balkans, but is far less common in Western Europe. For German, British or Scandinavian guests, SMS is the reliable choice.
Errors
Failed requests return a JSON object with an error code and a human-readable message.
{
"error": "missing_external_id",
"message": "external_id is required. Use your own booking/order reference."
}
| HTTP | Code | What to do |
|---|---|---|
| 401 | missing_api_key | No bearer token was supplied in the header. |
| 401 | invalid_format | The key is malformed. Check for truncation or stray whitespace. |
| 401 | not_found | The key does not exist. It may belong to a different environment. |
| 401 | revoked | The key was revoked in Settings. Create a new one. |
| 400 | invalid_json | The body is not valid JSON. |
| 400 | missing_external_id | Supply a reference from your own system. |
| 400 | missing_phone | The phone field is empty. |
| 400 | missing_message | The message field is empty. |
| 400 | invalid_channel | Use sms or viber. |
| 400 | forbidden | The key is not permitted to send. |
| 400 | account_suspended | The Trustio account is frozen. Check the subscription. |
| 400 | blocked_word | The message contains a disallowed word. |
| 400 | send_failed | The send was rejected — commonly an invalid phone number, an exhausted quota, or Viber on a plan without it. Read message for the specific reason. |
| 405 | method_not_allowed | Use POST. |
| 500 | internal_error | Retry with the same external_id. Duplicate protection makes this safe. |
Retries are always safe as long as you reuse the same external_id. A retry after a timeout will never produce a second message.
Code examples
curl -X POST \ 'https://ilmbmjeqbbrgswiqvlgh.supabase.co/functions/v1/public-api-send' \ -H 'Authorization: Bearer trst_YOUR_KEY' \ -H 'Content-Type: application/json' \ -d '{ "external_id": "booking-88123", "phone": "+4915112345678", "name": "Klaus", "message": "Zdraveite {име}, ostavete otziv: {линк}", "channel": "sms" }'
const res = await fetch( 'https://ilmbmjeqbbrgswiqvlgh.supabase.co/functions/v1/public-api-send', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.TRUSTIO_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ external_id: booking.id, phone: booking.guestPhone, name: booking.guestName, message: 'Zdraveite {име}, ostavete otziv: {линк}', channel: 'sms' }) } ); const data = await res.json(); if (!res.ok) { console.error('Trustio rejected the send:', data.error, data.message); } else if (data.duplicate) { console.log('Already sent for this booking'); } else { console.log('Queued:', data.message_id); }
$ch = curl_init('https://ilmbmjeqbbrgswiqvlgh.supabase.co/functions/v1/public-api-send'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('TRUSTIO_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'external_id' => $booking['id'], 'phone' => $booking['guest_phone'], 'name' => $booking['guest_name'], 'message' => 'Zdraveite {име}, ostavete otziv: {линк}', 'channel' => 'sms', ], JSON_UNESCAPED_UNICODE), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $data = json_decode($body, true); if ($status !== 200) { error_log('Trustio error: ' . $data['message']); }
import os, requests resp = requests.post( 'https://ilmbmjeqbbrgswiqvlgh.supabase.co/functions/v1/public-api-send', headers={ 'Authorization': f'Bearer {os.environ["TRUSTIO_API_KEY"]}', 'Content-Type': 'application/json', }, json={ 'external_id': booking.id, 'phone': booking.guest_phone, 'name': booking.guest_name, 'message': 'Zdraveite {име}, ostavete otziv: {линк}', 'channel': 'sms', }, timeout=15, ) data = resp.json() if resp.status_code != 200: print('Trustio rejected:', data['error'], data['message']) elif data['duplicate']: print('Already sent for this booking') else: print('Queued:', data['message_id'])
Store the key as an environment variable or secret — never commit it to source control and never expose it in browser code. Anyone holding the key can send invitations from your account and consume your quota.
No-code integration
If your booking or POS system can call a webhook, or is available on Zapier or Make, no programming is required.
- Create a Zap or scenario triggered by the event you want — a completed stay, a closed order, a finished appointment.
- Add a Webhooks → POST action.
- Set the URL to the endpoint above and the method to POST.
- Add a header
Authorizationwith the valueBearer trst_YOUR_KEY. - Map the fields: booking reference to
external_id, guest phone tophone, guest name toname, and type your message text intomessage.
Set the payload type to JSON. Mapping the booking reference into external_id matters here — automation platforms retry failed steps, and that field is what stops a guest receiving several invitations.
Limits
- Up to five active API keys per business.
- Every invitation is deducted from your monthly plan quota, exactly as if sent from the app. When the quota is exhausted, requests are rejected until it renews or you add a package.
- SMS messages longer than 160 characters consume one quota unit per segment.
- Guests who have unsubscribed will not receive messages.
You can see every message sent through the API in Покани inside the app, alongside those sent manually.
Support
Integration questions, or a system you are not sure can connect — write to support@trustio.bg. Tell us which system you use and we will confirm whether it can be connected.
When reporting a problem, include the error code, the external_id and the approximate time of the request. Never send us your API key.