> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Per-language recipes: voice, OTP verify, and analytics beyond first send

> Consolidated per-language recipes for the rest of the core loop — originate an outbound voice call, run the OTP send/check round trip, and read paginated delivery analytics — with a tab for Python, Go, Ruby, PHP, Java, and C# (escape hatch where the SDK has no typed helper), plus the cURL calls the guides already ship.

# Per-language recipes

The [Quickstart](/quickstart) and each SDK page show you a first send. This guide collects the three recipes that complete the core integration loop — **originate an outbound voice call**, **run the OTP send/check round trip**, and **read paginated delivery analytics** — with a per-language tab for Python, Go, Ruby, PHP, Java, and C#. Each snippet ends by accessing the response fields idiomatically (dict access in Python, map access in Go, hash dig in Ruby, array access in PHP, `result.get(...)` in Java, `TryGetProperty` in C#); the cURL blocks mirror the ones the guide pages already ship.

<Note>
  Typed helpers exist on every SDK for voice create and verify send/check; escape hatches (`client.request` / `client.Request` / `$client->request` / `client.request` / `client.RequestAsync`) are used here where a route has no typed wrapper — the analytics read below is an example. Scope per language is on the [SDK index](/sdks).
</Note>

## 1. Voice — originate an outbound call

`POST /api/v1/voice/calls` places the call. The typed `client.voice` helper covers this on every SDK; where a voice surface is outside the SDK's typed scope (recordings, conferences, IVR, dialer), use the language's escape hatch, shown in the second snippet of each tab. Response fields: `data.id` (the `call_...` handle) and `data.status`.

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST https://api.orbit.devotel.io/api/v1/voice/calls \
      -H "X-API-Key: $ORBIT_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "to": "+14155552671",
        "from": "+14155551234",
        "record": true
      }'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    call = client.voice.create(to="+14155552671", from_="+14155551234", record=True)
    print("call id:", call["data"]["id"])
    print("status:", call["data"]["status"])

    # Escape hatch — routes outside the typed voice wrapper:
    conf = client.request("POST", "/voice/conferences", json_body={"name": "support-room-42"})
    print("conference id:", conf["data"]["id"])
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    call, err := client.Voice().Create(context.Background(), orbit.CreateVoiceCallInput{
        To:     "+14155552671",
        From:   "+14155551234",
        Record: true,
    })
    // call.Data.ID, call.Data.Status — then, for routes outside the typed wrapper:
    var conf map[string]any
    err = client.Request(context.Background(), "POST", "/voice/conferences", nil,
        map[string]any{"name": "support-room-42"}, &conf, "")
    // conf["data"].(map[string]any)["id"] — escape hatch
    ```
  </Tab>

  <Tab title="Ruby">
    ```ruby theme={null}
    call = client.voice.create(to: "+14155552671", from: "+14155551234", record: true)
    puts call.dig("data", "id")
    puts call.dig("data", "status")

    # Escape hatch — routes outside the typed voice wrapper:
    conf = client.request("POST", "/voice/conferences", json_body: { "name" => "support-room-42" })
    puts conf.dig("data", "id")
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    $call = $client->voice->createCall(['to' => '+14155552671', 'from' => '+14155551234', 'record' => true]);
    echo $call['data']['id'], PHP_EOL;
    echo $call['data']['status'], PHP_EOL;

    // Escape hatch — routes outside the typed voice wrapper:
    $conf = $client->request('POST', '/voice/conferences', null, ['name' => 'support-room-42']);
    echo $conf['data']['id'], PHP_EOL;
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    Map<String, Object> call = client.voice.createCall("+14155552671", "+14155551234");
    System.out.println(((Map<?, ?>) call.get("data")).get("id"));

    // Escape hatch — routes outside the typed voice wrapper:
    Map<String, Object> conf = client.request(
        "POST", "/voice/conferences", null,
        Map.of("name", "support-room-42"), null);
    System.out.println(((Map<?, ?>) conf.get("data")).get("id"));
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var call = await client.Voice.CreateAsync(to: "+14155552671", from: "+14155551234", record: true);
    var callId = call.GetProperty("data").GetProperty("id").GetString();

    // Escape hatch — routes outside the typed voice wrapper:
    var conf = await client.RequestAsync("POST", "/voice/conferences",
        jsonBody: new Dictionary<string, object?> { ["name"] = "support-room-42" });
    if (conf.TryGetProperty("data", out var confData) && confData.TryGetProperty("id", out var id))
        Console.WriteLine(id.GetString());
    ```
  </Tab>
</Tabs>

Every outbound call dispatches through the Orbit API itself — the SDK never selects a carrier. The full endpoint contract (webhooks, `answer_url` callbacks, hangup, transfer) is in the [Voice API reference](/api-reference/endpoints/voice).

## 2. Verify — OTP round trip

Send the code with `POST /api/v1/verify/send`, keep the returned verification id, and check the code the user typed in with `POST /api/v1/verify/check`. Every SDK wraps both calls typed. Response fields: `data.valid` and `data.status` (`approved` when the code matches).

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    # 1. Send
    curl -X POST https://api.orbit.devotel.io/api/v1/verify/send \
      -H "X-API-Key: $ORBIT_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "to": "+14155552671", "channel": "sms" }'

    # 2. Check — use the verification id send returned:
    curl -X POST https://api.orbit.devotel.io/api/v1/verify/check \
      -H "X-API-Key: $ORBIT_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "verification_id": "ver_01hxyz", "code": "482901" }'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    sent = client.verify.send(to="+14155552671", channel="sms")
    result = client.verify.check(verification_id=sent["data"]["id"], code="482901")
    print("valid:", result["data"]["valid"])     # True
    print("status:", result["data"]["status"])   # 'approved'
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    sent, err := client.Verify().Send(context.Background(), "+14155552671", "sms")
    check, err := client.Verify().Check(context.Background(), sent.Data.ID, "482901")
    fmt.Println("valid:", check.Data.Valid)      // true
    fmt.Println("status:", check.Data.Status)    // "approved"
    ```
  </Tab>

  <Tab title="Ruby">
    ```ruby theme={null}
    sent = client.verify.send(to: "+14155552671", channel: "sms")
    result = client.verify.check(verification_id: sent.dig("data", "id"), code: "482901")
    puts "valid: #{result.dig("data", "valid")}"     # true
    puts "status: #{result.dig("data", "status")}"   # 'approved'
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    $sent = $client->verify->send('+14155552671', 'sms');
    $result = $client->verify->check($sent['data']['id'], '482901');
    echo 'valid: '.($result['data']['valid'] ? 'true' : 'false').PHP_EOL;  // true
    echo 'status: '.$result['data']['status'].PHP_EOL;                     // 'approved'
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    Map<String, Object> sent = client.verify.send("+14155552671", "sms");
    String verificationId = (String) ((Map<?, ?>) sent.get("data")).get("id");
    Map<String, Object> result = client.verify.check(verificationId, "482901");
    Map<?, ?> data = (Map<?, ?>) result.get("data");
    System.out.println("valid: " + data.get("valid"));      // true
    System.out.println("status: " + data.get("status"));    // "approved"
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var sent = await client.Verify.SendAsync(to: "+14155552671", channel: "sms");
    var verificationId = sent.GetProperty("data").GetProperty("id").GetString();
    var check = await client.Verify.CheckAsync(verificationId!, code: "482901");
    var data = check.GetProperty("data");
    Console.WriteLine($"valid: {data.GetProperty("valid").GetBoolean()}");   // True
    Console.WriteLine($"status: {data.GetProperty("status").GetString()}");  // "approved"
    ```
  </Tab>
</Tabs>

A wrongly-typed or expired code reports `valid: false` — it never raises for a bad guess (only for transport/auth failures), so branch on the flag. The full contract — resend, detail, expiry — is in the [Verify API reference](/api-reference/endpoints/verify).

## 3. Analytics — paginated delivery read

`GET /api/v1/analytics/messages` returns aggregate delivery metrics (totals + time-series). It has no typed helper in any SDK, so each language reads it through its escape hatch — the same low-level call the languages already use for other uncovered routes. Response fields: `data.totals.delivery_rate`, `data.totals.total_sent`, `data.time_series`. The [Analytics page](/channels/analytics) ships the same call for cURL, Node, Python, and Go — the tabs below complete the set for the other four languages.

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -G https://api.orbit.devotel.io/api/v1/analytics/messages \
      -H "X-API-Key: $ORBIT_API_KEY" \
      --data-urlencode "channel=sms" \
      --data-urlencode "status=delivered" \
      --data-urlencode "start_date=2026-05-01T00:00:00Z" \
      --data-urlencode "end_date=2026-05-08T00:00:00Z" \
      --data-urlencode "group_by=day"
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    report = client.request(
        "GET", "/analytics/messages",
        params={
            "channel": "sms", "status": "delivered",
            "start_date": "2026-05-01T00:00:00Z", "end_date": "2026-05-08T00:00:00Z",
            "group_by": "day",
        },
    )
    print("delivery rate:", report["data"]["totals"]["delivery_rate"])
    print("total sent:", report["data"]["totals"]["total_sent"])
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    query := url.Values{
        "channel":    {"sms"},
        "status":     {"delivered"},
        "start_date": {"2026-05-01T00:00:00Z"},
        "end_date":   {"2026-05-08T00:00:00Z"},
        "group_by":   {"day"},
    }
    var report map[string]any
    err := client.Request(context.Background(), "GET", "/analytics/messages", query, nil, &report, "")
    totals := report["data"].(map[string]any)["totals"].(map[string]any)
    fmt.Println("delivery rate:", totals["delivery_rate"])
    ```
  </Tab>

  <Tab title="Ruby">
    ```ruby theme={null}
    report = client.request("GET", "/analytics/messages", query: {
      channel: "sms", status: "delivered",
      start_date: "2026-05-01T00:00:00Z", end_date: "2026-05-08T00:00:00Z",
      group_by: "day",
    })
    puts "delivery rate: #{report.dig('data', 'totals', 'delivery_rate')}"
    puts "total sent: #{report.dig('data', 'totals', 'total_sent')}"
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    $report = $client->request('GET', '/analytics/messages', [
        'channel' => 'sms', 'status' => 'delivered',
        'start_date' => '2026-05-01T00:00:00Z', 'end_date' => '2026-05-08T00:00:00Z',
        'group_by' => 'day',
    ]);
    echo 'delivery rate: '.$report['data']['totals']['delivery_rate'].PHP_EOL;
    echo 'total sent: '.$report['data']['totals']['total_sent'].PHP_EOL;
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    Map<String, Object> query = new LinkedHashMap<>();
    query.put("channel", "sms");
    query.put("status", "delivered");
    query.put("start_date", "2026-05-01T00:00:00Z");
    query.put("end_date", "2026-05-08T00:00:00Z");
    query.put("group_by", "day");
    Map<String, Object> report = client.request("GET", "/analytics/messages", query, null, null);
    Map<?, ?> totals = (Map<?, ?>) ((Map<?, ?>) report.get("data")).get("totals");
    System.out.println("delivery rate: " + totals.get("delivery_rate"));
    System.out.println("total sent: " + totals.get("total_sent"));
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var query = new Dictionary<string, object?>
    {
        ["channel"] = "sms",
        ["status"] = "delivered",
        ["start_date"] = "2026-05-01T00:00:00Z",
        ["end_date"] = "2026-05-08T00:00:00Z",
        ["group_by"] = "day",
    };
    var report = await client.RequestAsync("GET", "/analytics/messages", query: query);
    var totals = report.GetProperty("data").GetProperty("totals");
    Console.WriteLine($"delivery rate: {totals.GetProperty("delivery_rate").GetDouble()}");
    Console.WriteLine($"total sent: {totals.GetProperty("total_sent").GetInt64()}");
    ```
  </Tab>
</Tabs>

All analytics calls share a 60-request/minute budget per tenant and require the `analytics:read` scope; queries run against a read replica, so reporting traffic never contends with the live send path. Bucket the series with `group_by` (`hour`, `day`, `week`, `month`) and page large ranges by splitting the date window — cursor pagination (`meta.pagination`) applies to list endpoints, not to the analytics rollups. The full parameter and endpoint matrix is on the [Analytics page](/channels/analytics).
