Quickstart: C#
This quickstart uses HttpClient and System.Text.Json, both part of .NET 8. No packages are needed.
Create a console app
dotnet new console -n SynticHello && cd SynticHelloSend a request
Replace Program.cs with:
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
var http = new HttpClient { BaseAddress = new Uri("https://app.syntic.ai/v1/"), Timeout = TimeSpan.FromMinutes(5) };
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("SYNTIC_API_KEY"));
var body = new JsonObject
{
["model"] = "Amara S1.2",
["stream"] = false,
["max_tokens"] = 300,
["messages"] = new JsonArray
{
new JsonObject { ["role"] = "system", ["content"] = "Answer in two sentences." },
new JsonObject { ["role"] = "user", ["content"] = "Why do leaves change colour in autumn?" },
},
};
using var response = await http.PostAsJsonAsync("chat/completions", body);
var data = JsonNode.Parse(await response.Content.ReadAsStringAsync())!;
if (!response.IsSuccessStatusCode)
{
var requestId = response.Headers.TryGetValues("x-syntic-request-id", out var ids) ? ids.First() : "?";
throw new Exception($"{(int)response.StatusCode} {data["error"]} (request {requestId})");
}
Console.WriteLine(data["choices"]![0]!["message"]!["content"]);
Console.WriteLine($"tokens: {data["usage"]!["total_tokens"]}");Note the trailing slash on BaseAddress and no leading slash on "chat/completions"; together they resolve to https://app.syntic.ai/v1/chat/completions.
Run it
dotnet runStream the reply
Ask HttpClient to return as soon as headers arrive, then read the body line by line:
var streamBody = new JsonObject
{
["model"] = "Amara Quick",
["messages"] = new JsonArray { new JsonObject { ["role"] = "user", ["content"] = "List four uses for honey." } },
};
using var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions")
{
Content = JsonContent.Create(streamBody),
};
using var streamed = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
streamed.EnsureSuccessStatusCode();
using var reader = new StreamReader(await streamed.Content.ReadAsStreamAsync());
while (await reader.ReadLineAsync() is { } line)
{
if (!line.StartsWith("data: ")) continue;
var payload = line["data: ".Length..];
if (payload == "[DONE]") break;
var evt = JsonNode.Parse(payload)!;
if (evt["error"] is not null) throw new Exception($"stream failed: {evt["error"]?["code"]}");
var piece = evt["choices"]?[0]?["delta"]?["content"]?.GetValue<string>();
if (piece is not null) Console.Write(piece);
}
Console.WriteLine();Pass a CancellationToken to SendAsync and ReadLineAsync to stop a stream; cancelling closes the connection.
Call a function
var tools = JsonNode.Parse("""
[{"type": "function", "function": {
"name": "parcel_status",
"description": "Delivery status for a parcel tracking number.",
"parameters": {"type": "object", "properties": {"tracking": {"type": "string"}}, "required": ["tracking"]}}}]
""")!;
var messages = new JsonArray { new JsonObject { ["role"] = "user", ["content"] = "Where is parcel ZX-4410?" } };
async Task<JsonNode> Ask()
{
var b = new JsonObject { ["model"] = "Amara S1.2", ["stream"] = false, ["tools"] = tools.DeepClone(), ["messages"] = messages.DeepClone() };
using var r = await http.PostAsJsonAsync("chat/completions", b);
r.EnsureSuccessStatusCode();
return JsonNode.Parse(await r.Content.ReadAsStringAsync())!;
}
var reply = await Ask();
var msg = reply["choices"]![0]!["message"]!;
if (msg["tool_calls"] is JsonArray calls)
{
messages.Add(msg.DeepClone()); // the assistant turn, unchanged
foreach (var call in calls)
{
var args = JsonNode.Parse(call!["function"]!["arguments"]!.GetValue<string>());
var result = """{"status":"out for delivery","eta":"14:00-16:00"}"""; // replace with a real lookup
messages.Add(new JsonObject { ["role"] = "tool", ["tool_call_id"] = call["id"]!.GetValue<string>(), ["content"] = result });
}
reply = await Ask();
}
Console.WriteLine(reply["choices"]![0]!["message"]!["content"]);Using an OpenAI-compatible .NET library
Libraries that let you set a custom endpoint can also be used. Point them at https://app.syntic.ai/v1. Check one thing first: whether the library sends "stream": false on non-streaming calls. If it leaves the field out, the API streams the reply and a non-streaming method fails to parse it; use the library’s streaming methods, or plain HttpClient as above.
Handle errors
| Status | What to do |
|---|---|
400, 413 | Fix the request; error names the problem. |
401, 403 | Check the key and any origin or scope restrictions. |
402 | quota_exhausted: see Handle 402. |
429 | Wait for response.Headers.RetryAfter, then retry. |
500, 502, 503 | Retry with exponential back-off. |
For retries in .NET, a DelegatingHandler or the Microsoft.Extensions.Http.Resilience package can apply the rules from Retries with back-off to every request.
Create one HttpClient (or use IHttpClientFactory) and reuse it. Creating a new client per request exhausts sockets under load.
Next steps
- Guides
- Chat completions reference
- Other languages: the planned official .NET client.