Yamo · Integrations

Your website's enquiries, in Yamo

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.

Open Integrations in Agency OS →
  • Your leads stay yours. No other agency sees them.
  • No app for your clients. Everything opens in their browser.
  • No developer to start. Copy, paste, done.

Three ways to connect

Start with the one that fits today; you can add the others later. You'll find all three in Agency OS under Settings → Integrations.

No tech · 2 min

Share your enquiry link

Good for WhatsApp, your Instagram bio, your email signature or a button on your site.

  1. Open Settings → Integrations in Agency OS.
  2. Copy your enquiry link.
  3. Paste it wherever people ask you about trips.
Copy & paste · 10 min

Put the form on your website

Good for your Contact or “Plan a trip” page. It carries your agency's name, logo and colour.

  1. Copy the embed code in Settings → Integrations.
  2. On your website, add an HTML or Embed block. WordPress, Wix and Squarespace all have one.
  3. Paste the code and publish.
Needs a developer

Connect your own system

Good if you already have your own booking form, a CRM or reports you want kept up to date.

  1. Your developer makes an API key in Integrations.
  2. Your form or CRM sends each enquiry to Yamo, either automatically or with a “Send to Yamo” button.
  3. Your system hears back when someone accepts, books or pays.
The guide for your developer ↓

Enquiry came in by phone or WhatsApp? Add it in Tailored → Requests → New request. It's handled exactly the same way.

What happens after someone enquires

The same five steps every time, whether they came through your form, your link or your own system.

  1. 1

    They tell you about their trip

    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.

  2. 2

    It lands in Agency OS as yours

    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.

  3. 3

    You build the trip and send it

    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.

  4. 4

    They open it on their phone and accept

    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.

  5. 5

    The deposit secures their seat

    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.

Questions agencies ask

Will Yamo pass my enquiry to another agency?

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.

Do my clients need the Yamo app?

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.

Do I need a developer?

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.

How will I know when someone books?

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.

Does the form look like my agency?

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.

Which plan do I need?

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.

For your developer

The technical guide

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.

Embedding the form

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.

HTML
<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>

The enquiry API

  • Send the key on every call as Authorization: Bearer yamo_sk_… (or X-Yamo-Key). It is shown once when it's made; Yamo stores only a fingerprint of it.
  • Call from your server only. A key in a web page or app can be read by anyone.
  • Each key can make 120 calls a minute. A revoked key stops working at once.
CallWhat it does
GET/agency-api/v1/pingChecks the key. Answers { "ok": true, "agency": { "slug", "name" } }.
POST/agency-api/v1/enquiriesSends 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.

Sending an enquiry

curl
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"
  }'
FieldWhat it is
fullNameNeededTheir name, 2–120 characters.
email / phoneOne ofWhere the proposal goes. A phone number with its country code works for WhatsApp.
destinationNeededWhere they want to go, in their words.
partySizeNeededHow many are travelling, 1–20.
datesLabelOptionalWhen, in their words: “December 2026”. Defaults to “Flexible”.
daysOptionalHow long, 1–60.
budgetCentsOptionalBudget per person, in cents.
countryCodeOptionalThe two-letter country code if you know it (TZ). Otherwise Yamo works it out.
preferenceChipsOptionalUp to 12 short tags: ["Beach", "Food"].
briefOptionalAnything else they said, up to 2 000 characters.
detailsOptionalThe practical answers, each optional:
  • adults (1–20) and children (under 12). With adults, partySize becomes adults + children.
  • from: the town they leave from.
  • travel: fly, drive or unsure.
  • flights: book or own, with fly only.
  • car: own (driving only), hire, driver, or none (flying only).
  • stay: any of hotel, resort, lodge, guesthouse, self_catering, camping.
  • stayTypes: up to 10 kinds of place in their words, like ["Luxury tented camp"]. The broad stay kinds they come under are added for you.
  • mustHaves: up to 12 things it must have, like ["Sea view", "Kids' club"].
  • meals: breakfast, half_board, all_inclusive or self_catering.
  • passports: all, some or unsure.
An unknown value is a 400; answers that don't go together are dropped.
externalRefOptionalYour 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.

201 Created
{
  "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"
}

Errors

An error answers with JSON { "message": "…" }, in plain words you can show or log.

StatusMeansWhat to do
400A field is missing or wrongThe message names the field.
401No key, an unknown key or a revoked oneCheck the header, or make a new key.
403The plan has no tailored trips, or the agency is suspendedThe agency talks to Yamo about its plan.
404No such enquiry for this agencyCheck the id.
429More than 120 calls a minute on this keyWait, then send again with the same externalRef.

Webhooks

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.

EventWhenIn data
enquiry.createdAn enquiry came in: the form, the API, or typed into Agency OSenquiry
enquiry.closedThe agency closed itenquiry, reason
proposal.sentA proposal or a revised one was sentenquiry (with proposal)
proposal.viewedThe link was opened for the first timeenquiry
proposal.changes_requestedThey asked for changesenquiry, note
proposal.acceptedThey accepted; the trip, booking and payment plan exist. picks lists the accommodation, transport and activities they chose where you offered a choiceenquiry, booking, option, picks
proposal.declinedThey said noenquiry, reason
booking.reservedSeats reserved; the deposit is owedbooking
seat.securedMoney took the seatbooking
payment.receivedAn instalment was paid: card, accepted EFT, or marked paidpayment, booking
booking.cancelledReleased, declined or taken off the tripbooking, by

Every delivery is a POST with a body like this one, for a deposit paid by card:

payment.received
{
  "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"
    }
  }
}

Checking the signature

Every delivery carries three headers, in the Standard Webhooks scheme:

webhook-id

The event's id, the same on every retry, so repeats can be dropped.

webhook-timestamp

Unix seconds. Refuse anything older than five minutes.

webhook-signature

v1, 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.

Node.js
// 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);

Delivery and retries

  • Answer with any 2xx within 4 seconds. Any other status, no answer, or a redirect counts as a failure.
  • Events for one address go out in the order they happened: a payment before the seat it took.
  • A failed delivery is tried again, at the earliest after:
  1. 1 min
  2. 5 min
  3. 30 min
  4. 2 h
  5. 6 h
  6. 24 h
  7. then it stops
  • A retry can go out later than that. Integrations lists recent deliveries, and Retry sends any of them again straight away.
  • The same webhook-id can arrive twice: treat repeats as one event.
  • The address must be public https; Yamo won't post to private or local network addresses.

Field reference

Yamo only ever adds fields, so ignore ones you don't know. Times are ISO 8601 in UTC.

enquiry

FieldHolds
id, externalRefYamo's id, and yours if you sent one
sourceagency for the agency's own enquiries, yamo for ones Yamo routed to it
channelsite (the API), embed (the form on your site), link (the form opened as a link), manual (typed into Agency OS)
statusnew, building, sent, changes, accepted or declined
closedtrue when the agency closed it, rather than the traveller declining
travellername, email (null if only a number was given), phone
destination, datesLabel, days, partySize, budgetCents, preferenceChips, brief, detailsWhat they asked for (details is {} when nobody asked)
proposalThe newest one sent: version, totalCents per person, currency, url, sentAt, openedAt
tripId, bookingIdThe trip and booking it became once accepted

booking

FieldHolds
id, tripId, tripName, tripKindThe booking and its trip (group or tailored)
statusrequested (owes the deposit), approved or active (seat taken), complete, cancelled
partySize, startDateSeats, and the departure date (null when not set)
source, sourceRefagency when it came through the agency, with the channel
enquiryIdThe enquiry a tailored trip came from
travellername, email, phone
totalCents, paidCents, currencyThe whole booking, and what's paid so far

payment

FieldHolds
id, labelThe instalment (“Deposit · secures your seat”)
amountCents, currencyHow much
paidViayoco (card in Yamo), eft (accepted proof), manual (marked paid)
paidAtWhen
Open Agency OS →