RevoplyAIDocs
Guides

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

  1. In Automations, create a flow and choose the trigger Your system sends an event.
  2. Choose the WhatsApp number to send from.
  3. Add a template step first, such as your approved order_shipped template. 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.
  4. 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_unavailable and service_unavailable later, with the same key. The samples above back off and try up to four times.
  • Fix, do not retry, everything else. invalid_phone means 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.

On this page