# Sending requests

Source: https://docs.revoplyai.com/flow-triggers/sending-requests/

> The request a flow trigger accepts, the path syntax for mapping fields, and how phone numbers and languages are read.

## The request [#the-request]

| Part              | Rule                                                                                                                      |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Method and URL    | `POST` to the flow's URL, `https://api.revoplyai.com/hooks/rvh_…`                                                         |
| `Content-Type`    | `application/json` (or any `+json` type); anything else is `415 unsupported_media_type`                                   |
| Body              | Any JSON, up to 64 KB (65,536 bytes); larger is `413 payload_too_large`                                                   |
| `Idempotency-Key` | Optional, strongly recommended: your id for the event, up to 200 characters. See [Idempotency](/get-started/idempotency/) |
| Authentication    | None besides the URL: its token is the credential                                                                         |

Send the JSON your system already produces. The flow's trigger settings say where in it
to find the customer's phone number, name and language, and which values to keep.

<CodeBlockTabs defaultValue="cURL">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Node.js">
      Node.js
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```bash
    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'
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Node.js">
    ```js
    /** 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,
      };
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    # The outcomes worth trying again later with the same Idempotency-Key.
    RETRYABLE = {
        "channel_unavailable",
        "rate_limited",
        "template_limit_reached",
        "customer_limit_reached",
        "service_unavailable",
    }


    def start_flow(hook_url: str, payload: dict, idempotency_key: str) -> dict:
        request = urllib.request.Request(
            hook_url,
            data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
            headers={"Content-Type": "application/json", "Idempotency-Key": idempotency_key},
            method="POST",
        )
        try:
            with urllib.request.urlopen(request, timeout=30) as response:
                http_status, body = response.status, response.read()
        except urllib.error.HTTPError as error:  # 4xx and 5xx still carry { id, status, detail }
            http_status, body = error.code, error.read()

        try:
            outcome = json.loads(body)
        except ValueError:  # the per-address limit in front of the hooks answers 429 as plain text
            outcome = {"id": None, "status": "rate_limited" if http_status == 429 else "unexpected", "detail": None}

        status = outcome.get("status")
        return {
            "ok": status in ("accepted", "duplicate"),
            "retry": status in RETRYABLE or http_status >= 500,
            "http_status": http_status,
            "run_id": outcome.get("id"),
            "status": status,
            "detail": outcome.get("detail"),
        }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Paths [#paths]

A path names one value in your JSON: property names joined by dots, and array positions in
square brackets.

| Path                        | Reads                                                   |
| --------------------------- | ------------------------------------------------------- |
| `customer.phone`            | `{"customer": {"phone": "+966501234567"}}`              |
| `items[0].name`             | The `name` of the first item in `items`                 |
| `$.order.id`                | `order.id`; a leading `$` or `$.` is ignored            |
| `[0].id`                    | The `id` of the first element when the body is an array |
| `order-id`, `العميل.الجوال` | Keys with dashes or Arabic letters, as written          |

* Property names are taken literally: any character except `.`, `[`, `]` and spaces. There
  is no quoting, so a key that contains a dot cannot be reached.
* No wildcards, filters or slices. A path is at most 200 characters and 12 steps.
* A string is read as it is; a number or `true`/`false` as JSON writes it; an object or an
  array as compact JSON. A missing value or `null` counts as empty. Spaces around a value
  are removed.

## What the trigger reads [#what-the-trigger-reads]

| Setting                 | Required | Read as                                                                  |
| ----------------------- | -------- | ------------------------------------------------------------------------ |
| Customer's phone number | Yes      | A phone number; see below. Missing or unreadable: `422 invalid_phone`    |
| Customer's name         | No       | Up to 200 characters; used for a contact we create                       |
| Customer's language     | No       | `ar` or `en`; see below                                                  |
| Values to keep          | No       | Up to 10, each up to 1,000 characters, as variables for the flow's steps |

### Phone numbers [#phone-numbers]

Send numbers in E.164, such as `+966501234567`. We also read:

* spaces, dashes, brackets and Arabic-Indic digits (`٠٥٠١٢٣٤٥٦٧`), which are ignored or
  converted;
* `00` in place of `+`: `00966501234567`;
* the country code without `+`: `966501234567`;
* a stray `0` after the country code of the countries listed below: `+9660501234567`.

A number without its country code, such as `0501234567`, is read as a mobile number in the
country of the flow's WhatsApp number, and only when that country is Saudi Arabia, the
United Arab Emirates, Kuwait, Qatar, Bahrain, Oman or Egypt. Anywhere else it is refused
with `422 invalid_phone` rather than guessed: a number placed in the wrong country would
send your message to a stranger.

### Language [#language]

The run uses this language. Values starting with `ar`
(`ar`, `ar-SA`), and `arabic`, `عربي` or `العربية`, mean Arabic; values starting with
`en`, and `english`, mean English; letter case is ignored. Anything else, or no value,
falls back to the language on the customer's contact.

## The last request [#the-last-request]

The builder keeps the last request each URL received, so you can map fields by tapping
them. It keeps the body's shape, not all of it: long lists are cut to their first 3 items
and long strings to their first 200 characters. It is kept for 30 days after it arrived.
Every new request with a JSON body replaces it, even one refused for another reason, such
as a flow that is switched off or a phone number that cannot be read.
