Start a flow from your system
Send a WhatsApp template when an order ships, by posting your order JSON to a flow trigger URL, with safe retries.
This guide sends a customer a WhatsApp message when your store marks their order as shipped, and lets them reply in the same conversation. You need the Business plan, a connected WhatsApp Business number, an approved template, and an Owner or Admin login.
1. Build the flow
- In Automations, create a flow and choose the trigger Your system sends an event.
- Choose the WhatsApp number to send from.
- Add a template step first, such as your approved
order_shippedtemplate. A flow started by your system must send a template before anything else. Add whatever follows: a question, a handover to your team, an end. - Create the URL and copy it into your store's server configuration as
REVOPLY_HOOK_URL. It is shown in full only once. Treat it as a password.
Leave the flow unpublished for now.
2. Send a sample request
Send one request with your own phone number in it, in the shape your store already uses:
curl --silent --show-error "$REVOPLY_HOOK_URL" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order-10482-shipped' \
--data '{
"order": { "id": "10482", "status": "shipped" },
"customer": { "name": "نورة العتيبي", "phone": "+966501234567" }
}' \
--write-out '\nHTTP %{http_code}\n'It is answered 409 with "status": "flow_off", because the flow is not published, and
the builder keeps the body. Back in the trigger settings, choose Refresh, then tap
customer.phone for the phone number, customer.name for the name, and order.id as a
value to keep, say as order_id, to use in the template.
3. Publish and send for real
Publish the flow and switch it on. Now call the URL from your code when an order ships.
Use an Idempotency-Key built from the event itself, such as order-10482-shipped, and
send the same key on every retry of that event:
// Starts a RevoplyAI flow from your system (Node.js 18+, no packages).
//
// REVOPLY_HOOK_URL=https://api.revoplyai.com/hooks/rvh_… node start-flow.node.mjs
//
// The hook URL is the credential: keep it in your server's configuration, never in a browser.
import { fileURLToPath } from 'node:url';
// #region start
/** The outcomes worth trying again later with the same Idempotency-Key. */
const RETRYABLE = new Set([
'channel_unavailable',
'rate_limited',
'template_limit_reached',
'customer_limit_reached',
'service_unavailable',
]);
export async function startFlow(hookUrl, payload, idempotencyKey, { fetchImpl = fetch } = {}) {
const response = await fetchImpl(hookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
body: JSON.stringify(payload),
});
// Every answer is { id, status, detail }, except a 429 from the per-address limit in front
// of the hooks, which is plain text.
const outcome = await response.json().catch(() => ({ id: null, status: null, detail: null }));
const status = outcome.status ?? (response.status === 429 ? 'rate_limited' : 'unexpected');
return {
ok: status === 'accepted' || status === 'duplicate',
retry: RETRYABLE.has(status) || response.status >= 500,
httpStatus: response.status,
runId: outcome.id,
status,
detail: outcome.detail,
};
}
// #endregion
// #region example
const order = {
order: { id: '10482', status: 'shipped' },
customer: { name: 'نورة العتيبي', phone: '+966501234567' },
};
async function main() {
// One key per business event, reused on every retry of it: a retry that reaches us twice
// starts the flow once.
const key = `order-${order.order.id}-${order.order.status}`;
for (let attempt = 1; attempt <= 4; attempt++) {
const result = await startFlow(process.env.REVOPLY_HOOK_URL, order, key);
console.log(
`${result.httpStatus} ${result.status}${result.detail ? `: ${result.detail}` : ''}`,
);
if (result.ok) {
console.log(`run ${result.runId}`);
return 0;
}
if (!result.retry) return 1;
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 15_000));
}
return 1;
}
// #endregion
if (process.argv[1] === fileURLToPath(import.meta.url)) {
process.exitCode = await main();
}
A 202 with "status": "accepted" means the run is queued: the template goes out in a
moment. A 200 with "status": "duplicate" means this order's event already started a
run; nothing new happens.
4. Handle what comes back
- Retry
rate_limited,template_limit_reached,customer_limit_reached,channel_unavailableandservice_unavailablelater, with the same key. The samples above back off and try up to four times. - Fix, do not retry, everything else.
invalid_phonemeans the phone path found nothing readable; send numbers with their country code. - Never retry
opted_out.
The full list is in Responses and errors.
5. Learn how each run ended
Keep the id from the 202. Subscribe a webhook endpoint to
Automation completed and Automation failed: their data.run.id is that id, and
data.run.variables holds what the flow collected, such as the customer's answer.
Before going live
- Test with your own number; there is no test mode.
- Each accepted request sends a template Meta bills for, and counts against the daily allowance for automated templates and the limit of 3 per customer in 24 hours.
- If the URL may have leaked, Replace the URL: the old one stops working at once.
Verify webhook signatures
Verify X-Revoply-Signature in Node.js, Python, PHP or C#, get the raw body in Express, Flask, Laravel and ASP.NET, and debug failures.
Hand over to a human
Open a ticket in your helpdesk when the assistant hands a conversation to your team, using the conversation.handover event.