Share your enquiry link
Good for WhatsApp, your Instagram bio, your email signature or a button on your site.
- Open Settings → Integrations in Agency OS.
- Copy your enquiry link.
- Paste it wherever people ask you about trips.
Put Yamo's enquiry form on your website, or share it as a link on WhatsApp and Instagram. Every enquiry lands in Agency OS as yours. You build the trip, send it back as a link, and see the moment they book.
Start with the one that fits today; you can add the others later. You'll find all three in Agency OS under Settings → Integrations.
Good for WhatsApp, your Instagram bio, your email signature or a button on your site.
Good for your Contact or “Plan a trip” page. It carries your agency's name, logo and colour.
Good if you already have your own booking form, a CRM or reports you want kept up to date.
Enquiry came in by phone or WhatsApp? Add it in Tailored → Requests → New request. It's handled exactly the same way.
The same five steps every time, whether they came through your form, your link or your own system.
Where, when, how long, who's going, the kind of trip and a rough budget, fly or drive (and the car: their own, hired, or with a driver), where they'd like to stay and what it must have, mostly with a tap, then their name and an email or WhatsApp number. The ideas to tap fit their trip (a ryokan for Japan, a tented camp for the Mara, a kids' club with children), and every list takes their own words too. Or they tap Talk to Yamo and answer the same questions in a chat. If they leave an email, Yamo sends them a short note that you have their enquiry.
You'll find it in Tailored → Requests, marked as your own enquiry, and your admins get a notification. It never goes to Yamo's matching.
Add the costs, the days and a photo, then press Send. Yamo emails them the link, and one tap sends it on WhatsApp with the message already written.
No app to download. They see the days and the costs, then accept, ask for changes or say no. To accept they make a free account, which keeps their payment plan, trip pack and documents together.
They pay by card or EFT. You see it in Agency OS straight away, along with every payment after it. If you've connected your own system, it hears about it too.
No. Enquiries from your form, your link or your own system belong to you from the moment they arrive. They never go to Yamo's matching, and no other agency sees them.
No. The form and the trip you send them open in any browser, on any phone. They make a free account only when they accept, so their payments, trip pack and documents are all in one place.
Not for the link or the form: you copy and paste them yourself. You'd only need a developer to connect your own booking form, CRM or reports.
Agency OS shows every reply, reservation and payment under Activity, and your admins are notified. If you've connected your own system, it hears the same news.
Yes. It shows your agency's name, logo and colour, and every enquiry is addressed to you. On your website it sits inside your own page, with “Planned with Yamo” in small print.
The enquiry form and the enquiry API come with plans that include tailored trips. If yours doesn't, Agency OS tells you on the Integrations screen.
Everything needed to connect an agency's own system to Yamo: sending enquiries from your server, receiving signed webhooks, and checking them. The API lives at https://api.yamo.travel, speaks JSON, and puts money in cents (2500000 is R 25 000). An admin of the agency makes the keys and the webhook in Agency OS → Settings → Integrations.
Integrations gives the agency this code with its slug filled in. The script line lets the frame grow to fit the form, and brings the top of the form back into view when the person moves to the next step; without it the frame keeps its minimum height and scrolls.
<iframe id="yamo-enquiry" src="https://app.yamo.travel/a/your-slug/enquire?embed=1" title="Plan a trip with Your Agency" loading="lazy" style="display:block;width:100%;max-width:640px;min-height:760px;border:0;border-radius:20px;margin:0 auto"></iframe>
<script>addEventListener('message',function(e){if(e.origin!=='https://app.yamo.travel'||!e.data)return;var f=document.getElementById('yamo-enquiry');if(e.data.type==='yamo:height')f.style.height=e.data.height+'px';if(e.data.type==='yamo:top'&&f.getBoundingClientRect().top<0)f.scrollIntoView({behavior:'smooth'})});</script>Authorization: Bearer yamo_sk_… (or X-Yamo-Key). It is shown once when it's made; Yamo stores only a fingerprint of it.| Call | What it does |
|---|---|
GET/agency-api/v1/ping | Checks the key. Answers { "ok": true, "agency": { "slug", "name" } }. |
POST/agency-api/v1/enquiries | Sends an enquiry. Answers with the enquiry: 201 when new, 200 when the same externalRef was sent before. |
GET/agency-api/v1/enquiries/{id} | Where it stands now: proposal sent and opened, accepted, the trip and booking it became. |
curl https://api.yamo.travel/agency-api/v1/enquiries \
-H "Authorization: Bearer yamo_sk_…" \
-H "Content-Type: application/json" \
-d '{
"fullName": "Naledi Khumalo",
"email": "naledi@example.com",
"phone": "+27821234567",
"destination": "Zanzibar, Tanzania",
"datesLabel": "December 2026",
"days": 7,
"partySize": 2,
"budgetCents": 2500000,
"brief": "Beach and a spice tour, flying from Johannesburg",
"externalRef": "web-1042"
}'| Field | What it is | |
|---|---|---|
fullName | Needed | Their name, 2–120 characters. |
email / phone | One of | Where the proposal goes. A phone number with its country code works for WhatsApp. |
destination | Needed | Where they want to go, in their words. |
partySize | Needed | How many are travelling, 1–20. |
datesLabel | Optional | When, in their words: “December 2026”. Defaults to “Flexible”. |
days | Optional | How long, 1–60. |
budgetCents | Optional | Budget per person, in cents. |
countryCode | Optional | The two-letter country code if you know it (TZ). Otherwise Yamo works it out. |
preferenceChips | Optional | Up to 12 short tags: ["Beach", "Food"]. |
brief | Optional | Anything else they said, up to 2 000 characters. |
details | Optional | The practical answers, each optional:
400; answers that don't go together are dropped. |
externalRef | Optional | Your own id for it, up to 120 characters. Sending the same one again returns the existing enquiry instead of a second one. |
The answer is the enquiry as Yamo holds it; webhooks carry the same shape.
{
"id": "2808dbfc-d0ab-40d4-b2aa-62ecb1723254",
"externalRef": "web-1042",
"source": "agency",
"channel": "site",
"status": "new",
"closed": false,
"traveller": { "name": "Naledi Khumalo", "email": "naledi@example.com", "phone": "+27821234567" },
"destination": "Zanzibar, Tanzania",
"datesLabel": "December 2026",
"days": 7,
"partySize": 2,
"budgetCents": 2500000,
"preferenceChips": [],
"brief": "Beach and a spice tour",
"details": { "adults": 2, "from": "Johannesburg", "travel": "fly", "flights": "book", "car": "driver" },
"proposal": null,
"tripId": null,
"bookingId": null,
"createdAt": "2026-10-20T12:00:00.000Z",
"updatedAt": "2026-10-20T12:00:00.000Z"
}An error answers with JSON { "message": "…" }, in plain words you can show or log.
| Status | Means | What to do |
|---|---|---|
| 400 | A field is missing or wrong | The message names the field. |
| 401 | No key, an unknown key or a revoked one | Check the header, or make a new key. |
| 403 | The plan has no tailored trips, or the agency is suspended | The agency talks to Yamo about its plan. |
| 404 | No such enquiry for this agency | Check the id. |
| 429 | More than 120 calls a minute on this key | Wait, then send again with the same externalRef. |
Yamo posts to one https address whenever something happens to the agency's enquiries and bookings. The agency adds it in Integrations, copies the signing secret (whsec_…) shown once, and can send a ping with Send test event. It can choose events, pause sending, and make a new secret at any time.
| Event | When | In data |
|---|---|---|
enquiry.created | An enquiry came in: the form, the API, or typed into Agency OS | enquiry |
enquiry.closed | The agency closed it | enquiry, reason |
proposal.sent | A proposal or a revised one was sent | enquiry (with proposal) |
proposal.viewed | The link was opened for the first time | enquiry |
proposal.changes_requested | They asked for changes | enquiry, note |
proposal.accepted | They accepted; the trip, booking and payment plan exist. picks lists the accommodation, transport and activities they chose where you offered a choice | enquiry, booking, option, picks |
proposal.declined | They said no | enquiry, reason |
booking.reserved | Seats reserved; the deposit is owed | booking |
seat.secured | Money took the seat | booking |
payment.received | An instalment was paid: card, accepted EFT, or marked paid | payment, booking |
booking.cancelled | Released, declined or taken off the trip | booking, by |
Every delivery is a POST with a body like this one, for a deposit paid by card:
{
"id": "evt_f201bb415fc44f43b47d2a089c8e08b1",
"type": "payment.received",
"createdAt": "2026-10-22T09:14:03.000Z",
"agency": { "id": "…", "slug": "your-slug" },
"data": {
"payment": {
"id": "…", "label": "Deposit · secures your seat",
"amountCents": 500000, "currency": "ZAR",
"paidVia": "yoco", "paidAt": "2026-10-22T09:14:02.000Z"
},
"booking": {
"id": "…", "tripId": "…", "tripName": "Zanzibar, Tanzania", "tripKind": "tailored",
"startDate": null, "partySize": 2, "status": "approved",
"source": "agency", "sourceRef": "agency.your-slug.site",
"enquiryId": "2808dbfc-d0ab-40d4-b2aa-62ecb1723254",
"traveller": { "name": "Naledi Khumalo", "email": "naledi@example.com", "phone": "+27821234567" },
"totalCents": 4000000, "paidCents": 500000, "currency": "ZAR"
}
}
}Every delivery carries three headers, in the Standard Webhooks scheme:
webhook-idThe event's id, the same on every retry, so repeats can be dropped.
webhook-timestampUnix seconds. Refuse anything older than five minutes.
webhook-signaturev1, followed by a base64 HMAC-SHA256 of id.timestamp.body.
The key is the secret with its whsec_ prefix removed, base64-decoded. Sign the raw body exactly as it arrived, and compare in constant time.
// Express: keep the raw body, e.g. app.post('/hooks/yamo', express.raw({ type: 'application/json' }), handler) import { createHmac, timingSafeEqual } from 'node:crypto'; export function fromYamo(headers, rawBody, secret) { const id = headers['webhook-id']; const ts = headers['webhook-timestamp']; const sig = headers['webhook-signature'] || ''; if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64'); const expected = createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest(); return sig.split(' ').some((part) => { const given = Buffer.from(part.replace(/^v1,/, ''), 'base64'); return given.length === expected.length && timingSafeEqual(given, expected); }); } // In the handler: answer fast, do the work after. // if (!fromYamo(req.headers, req.body.toString('utf8'), process.env.YAMO_WEBHOOK_SECRET)) return res.sendStatus(401); // const event = JSON.parse(req.body); res.sendStatus(200); handle(event);
<?php // WordPress or plain PHP: read the raw body before anything parses it. function from_yamo(array $headers, string $rawBody, string $secret): bool { $h = array_change_key_case($headers, CASE_LOWER); $id = $h['webhook-id'] ?? ''; $ts = $h['webhook-timestamp'] ?? ''; if ($id === '' || abs(time() - (int) $ts) > 300) return false; $key = base64_decode(preg_replace('/^whsec_/', '', $secret)); $expected = base64_encode(hash_hmac('sha256', "$id.$ts.$rawBody", $key, true)); foreach (explode(' ', $h['webhook-signature'] ?? '') as $part) { if (hash_equals($expected, preg_replace('/^v1,/', '', $part))) return true; } return false; } $raw = file_get_contents('php://input'); if (!from_yamo(getallheaders(), $raw, getenv('YAMO_WEBHOOK_SECRET'))) { http_response_code(401); exit; } $event = json_decode($raw, true); http_response_code(200); // switch ($event['type']) { case 'payment.received': … }
webhook-id can arrive twice: treat repeats as one event.Yamo only ever adds fields, so ignore ones you don't know. Times are ISO 8601 in UTC.
| Field | Holds |
|---|---|
id, externalRef | Yamo's id, and yours if you sent one |
source | agency for the agency's own enquiries, yamo for ones Yamo routed to it |
channel | site (the API), embed (the form on your site), link (the form opened as a link), manual (typed into Agency OS) |
status | new, building, sent, changes, accepted or declined |
closed | true when the agency closed it, rather than the traveller declining |
traveller | name, email (null if only a number was given), phone |
destination, datesLabel, days, partySize, budgetCents, preferenceChips, brief, details | What they asked for (details is {} when nobody asked) |
proposal | The newest one sent: version, totalCents per person, currency, url, sentAt, openedAt |
tripId, bookingId | The trip and booking it became once accepted |
| Field | Holds |
|---|---|
id, tripId, tripName, tripKind | The booking and its trip (group or tailored) |
status | requested (owes the deposit), approved or active (seat taken), complete, cancelled |
partySize, startDate | Seats, and the departure date (null when not set) |
source, sourceRef | agency when it came through the agency, with the channel |
enquiryId | The enquiry a tailored trip came from |
traveller | name, email, phone |
totalCents, paidCents, currency | The whole booking, and what's paid so far |
| Field | Holds |
|---|---|
id, label | The instalment (“Deposit · secures your seat”) |
amountCents, currency | How much |
paidVia | yoco (card in Yamo), eft (accepted proof), manual (marked paid) |
paidAt | When |