Guides

Calling the Zalo API from C#/.NET: OAuth, webhooks and OA messaging

A technical guide to integrating the Zalo OA API in C#/.NET: OAuth v4 with PKCE, token lifecycle, typed HttpClient with retries, signed ASP.NET Core webhooks, idempotency and safe logging.

Summary: what you need to call the Zalo OA API from .NET

To call the Zalo OA API from .NET, your app must be authorised by the OA through OAuth v4 (with PKCE) to obtain an access token and refresh token, then call the OpenAPI with an access_token header. According to Zalo's documentation, access tokens are valid for 25 hours, refresh tokens for 3 months, and each refresh token can be used only once. On the .NET side, use a typed HttpClient via IHttpClientFactory, a token service with a synchronisation lock, the Microsoft.Extensions.Http.Resilience pipeline, and a webhook endpoint that verifies the signature before processing.

This article is for developers. For the business view (message types, CRM connection, customer service flow), see the Zalo OA integration guide. Endpoints and limits below come from the Zalo Platform Document Hub at the time of writing; Zalo changes its API fairly often, so check Zalo's current documentation before you ship.

OAuth v4 with PKCE: getting the first access token

Zalo's "Authentication and authorisation for apps" page describes a five-step OAuth v4 flow. The authorization code is single-use and valid for 10 minutes from issue.

  • Step 1 – Create a code verifier (a 43-character string with upper case, lower case and digits, different for every request) and store it server-side. The code challenge is the SHA-256 of the verifier, Base64-encoded without padding. The Open APIs overview page says Base64URL; in .NET you can use SHA256.HashData and then Base64Url.EncodeToString (System.Buffers.Text, .NET 9 and later).
  • Step 2 – In Zalo for Developers, set the callback URL and code challenge, and select the permission groups you need (for example sending messages and notifications via the OA, receiving message events).
  • Step 3 – Send the authorisation link to the OA admin. After approval, your callback URL receives an HTTP GET with code and oa_id. Use the state parameter to match the code to its code verifier.
  • Step 4 – POST to https://oauth.zaloapp.com/v4/oa/access_token, Content-Type application/x-www-form-urlencoded, header secret_key, body with code, app_id, grant_type=authorization_code and code_verifier.
  • Step 5 – Store access_token, refresh_token and expires_in (in seconds; the documentation example is 90000, i.e. 25 hours).

Token lifecycle and safe refresh in .NET

Per Zalo's documentation: every access token comes with a refresh token; a refresh token is single-use; after a successful refresh the old refresh token is invalidated and the old access token expires as soon as the new one is issued. Refresh uses the same endpoint with grant_type=refresh_token. If the refresh token expires, the OA admin must authorise again from scratch.

The key design consequence: if two processes refresh at the same time, one ends up holding a dead refresh token. So only one place may refresh.

  • Create a singleton ZaloTokenService that keeps tokens in memory and uses SemaphoreSlim so only one thread refreshes at a time.
  • Persist the new token pair (encrypted database or secret store) immediately on receipt, before using the new access token. If that write fails you may lose the only valid refresh token.
  • Refresh proactively before expiry (for example when a few hours remain) from a BackgroundService, instead of waiting for token errors.
  • With multiple instances, use a distributed lock (for example a database lease) so only one instance refreshes; others read tokens from the shared store.
  • Never put secret_key, access tokens or refresh tokens in source code, logs or error messages; load them from secret configuration via IOptions.

Typed HttpClient, retries and consultation messages

Register a typed client, for example services.AddHttpClient<ZaloOaClient>() with BaseAddress https://openapi.zalo.me/, and add .AddStandardResilienceHandler(). According to Microsoft Learn, the standard handler stacks several resilience strategies with default options to handle transient errors; use AddResilienceHandler if you need a custom pipeline. A separate DelegatingHandler fetches the access token from ZaloTokenService and adds the access_token header to each request.

Text and attachment consultation messages (tin Tư vấn) use POST https://openapi.zalo.me/v3.0/oa/message/cs with a JSON body containing recipient.user_id and message. Per the "conditions for consultation messages" page, the app must have the send-messages permission and the recipient must have interacted with the OA within 7 days. The response returns message_id, user_id, sent_time and quota information; the docs state that from 1 January 2026 consultation messages within the 48-hour window are free without limit and that some quota fields stop being returned after 1 March 2026, so do not hard-depend on them.

Proactive notifications (order confirmations, reminders) go through ZBS Template Message: POST https://openapi.zalo.me/v3.0/oa/message/template with user_id, template_id and template_data, using approved templates. For sending by phone number and ZBS account requirements, check Zalo's current documentation.

Retrying message sends correctly

  • Retrying on network errors and HTTP 5xx is reasonable, but a send may have succeeded even if the client timed out, so retrying sends can create duplicates.
  • Store send state by business key (for example order_id + message type) before calling the API; retry only if that key has no message_id yet.
  • Zalo often returns HTTP 200 with a non-zero error field for business errors; read the error field in the body rather than relying on status codes. For the error code table, check Zalo's current documentation.
  • An invalid-token error should trigger one refresh and one retry, never an endless loop.

Webhooks in ASP.NET Core minimal APIs: verify before you process

Zalo POSTs events (user messages, follows, button taps) to the HTTPS webhook URL configured in your app. On the Zalo for Developers forum, the Zalo team explains that the X-ZEvent-Signature header follows mac = sha256(appId + data + timeStamp + OAsecretKey), where data is the event payload received. For the exact concatenation order, header format and timestamp source, check Zalo's current documentation and test with real events before switching to reject mode.

  • Read the raw body exactly as received (enable Request.EnableBuffering or read the stream into a string) before parsing JSON; hashing re-serialised JSON will not match.
  • Compute SHA256.HashData(Encoding.UTF8.GetBytes(appId + rawBody + timestamp + oaSecretKey)), convert to lower-case hex (the header value usually carries a "mac=" prefix; strip it first) and compare with CryptographicOperations.FixedTimeEquals to avoid timing leaks.
  • Reject immediately (401) if the header is missing, the OA secret key is empty or the signature is wrong; unauthenticated requests must never reach business logic.
  • Return 200 quickly: put the event on a queue (Channel, outbox table or message broker) and process it in a BackgroundService instead of calling AI or CRM inside the webhook request.
  • Limit body size and request rate on the webhook endpoint.

Idempotency and logging without personal data

Webhooks can be delivered more than once; the Zalo forum has reports of two events for a single message. Treat every event as possibly duplicated.

  • Build an idempotency key from msg_id (when present) or event_name + user_id + timestamp; store it under a UNIQUE constraint and skip events already processed.
  • On the sending side, the business key from the retry section prevents duplicate sends when jobs rerun.
  • Use structured ILogger logging with event_name, oa_id, idempotency key, outcome and duration; never log message content, names, phone numbers or tokens.
  • If you must keep conversation content for customer service, store it in an access-controlled store with a clear retention period, separate from technical logs.

.NET libraries: write your own client or use an SDK

For the Zalo OA OpenAPI, most teams write a thin typed client: the number of endpoints is usually small, and owning the client gives control over retries, logging and token storage.

The author has an unofficial personal project, Zalo.Net, for the personal Zalo Web client, which is a different scope from this article; OA integrations should use Zalo's official OpenAPI.

Production checklist

Zalo OA API integration checklist for .NET
AreaCheck
OAuthFresh code verifier per request, state parameter used; secret_key only on the server
TokensOne place refreshes, under a lock; persist new pair before use; alert on refresh failure
HTTP clientTyped client via IHttpClientFactory, resilience handler, sensible timeouts
SendingCheck recipient interaction conditions; read the error field in the body; business key against duplicates
WebhookHTTPS, verify X-ZEvent-Signature on the raw body, constant-time compare, fast 200
IdempotencyUNIQUE constraint on the event key
Logs and dataNo tokens or personal data in logs; access control on conversation storage
Change trackingFollow Zalo developer notices; run integration tests regularly

Frequently asked questions

How long is a Zalo OA access token valid?

According to the Zalo Platform documentation, an OA access token is valid for 25 hours from issue, a refresh token for 3 months, and each refresh token can be used only once. The authorization code is valid for 10 minutes and single-use.

Why does my Zalo refresh token say expired before 3 months?

Usually because it has already been used, for example by two processes refreshing at once or by not storing the new refresh token after the previous refresh. Let only one place refresh, under a lock, and persist the new pair immediately.

How do I verify a Zalo OA webhook signature in C#?

Read the raw body, compute SHA-256 of appId + data + timestamp + OA secret key as described by Zalo, convert to hex and compare with the X-ZEvent-Signature header using CryptographicOperations.FixedTimeEquals. Check Zalo's current documentation for the exact format.

Is there an official C# SDK for the Zalo OA API?

Zalo's developer documentation lists SDKs including a .NET (Dotnet) SDK, but SDKs can lag behind API changes, so check them against the current documentation. Because you usually need only a few endpoints, many teams write a thin typed HttpClient to keep retries, logging and token storage under their control.

Can I message users who have never contacted my OA?

Consultation messages require the recipient to have interacted with the OA within the last 7 days. For proactive notifications such as order confirmations or reminders, use ZBS Template Message with approved templates.

Share article:
Key point

The hard part of the Zalo OA API is not the HTTP calls but the token lifecycle, webhook signatures and de-duplication. Get those three right from the start and the business layer on top (CRM, chatbots, AI) stays stable. Before connecting Zalo to AI, clean up customer data as described in integrating CRM and ERP before AI, and see the architecture in building an AI backend with C# and .NET. EYC supports system integration through its integration and consulting solution; scope and operating responsibilities are defined under contract.

Sources

  1. Xác thực và ủy quyền cho Ứng dụng (OAuth v4) — Zalo Platform Document Hub
  2. Open APIs – tổng quan và ví dụ xác thực — Zalo Platform Document Hub
  3. Điều kiện gửi tin Tư vấn — Zalo Platform Document Hub
  4. Gửi tin Tư vấn đính kèm file — Zalo Platform Document Hub
  5. Bắt đầu với ZBS (ZBS Template Message) — Zalo Platform Document Hub
  6. Nhận 2 webhooks khi nhắn 1 message (trả lời về X-ZEvent-Signature) — Zalo for Developers – Cộng đồng
  7. Build resilient HTTP apps: key development patterns — Microsoft Learn
  8. Zalo.Net (README) — GitHub (ppn-systems)

Related solutions

  • Integration & Consulting — Connect systems, APIs, data and operating processes without unnecessary rebuilds.
  • Software & SaaS — Design and build web platforms, business software and subscription products.

Continue reading