Neura.CRM.SDK 1.9.0-qa.2
Neura.CRM.SDK
A typed .NET client for the NeuraCore CRM WhatsApp API and for the leads your Facebook Pages
collect, plus the models for the callbacks the platform POSTs back to you. netstandard2.0, so it drops into .NET Framework and .NET alike.
The live documentation for the same contract is served by the API itself:
| Page | What it is |
|---|---|
/api/developers |
The quickstart: credentials, token, first send, callbacks. |
/api/swagger |
Every endpoint with its request and response shapes, and a console. |
/api/callbacks |
Every callback, an example body per message type, the retry rules, and a throwaway inbox you can fire a real delivery at. |
Sending
Register the client, authenticate once, send:
services.AddNeuraWhatsappClient("https://your-deployment.example.com/");
public class QuoteNotifier
{
private readonly INeuraWhatsappClient _whatsapp;
private static readonly AuthenticationModel Credentials = new AuthenticationModel
{
ApiKey = "your-api-key", // issued per company
ApiSecret = "00000000-0000-..." // the number you send from
};
public QuoteNotifier(INeuraWhatsappClient whatsapp) => _whatsapp = whatsapp;
public async Task<Guid?> SendAsync(string number)
{
// Authenticates only when the cached token is missing or nearly expired: tokens last
// 7 days and Authenticate is limited to 5 attempts a minute.
await _whatsapp.EnsureAuthenticatedAsync(Credentials);
var result = await _whatsapp.SendMessageAsync(new WhatsAppMessage
{
To = number, // digits only, country code, no plus
Message = "Your quote is ready.",
Reference = "ORDER-1042" // your own reference, not shown to the contact
});
// A send the API understood but could not do comes back 200 with Success false -
// check it as well as catching NeuraApiException.
if (!result.Success)
throw new InvalidOperationException(result.Error?.ErrorMessage);
return result.MessageID; // every callback about this message quotes it
}
}
Anything that is not 2xx throws NeuraApiException (a HttpRequestException), which carries the
status and body - IsUnauthorized means re-authenticate, IsRateLimited means back off.
Beyond plain text there is media, buttons, lists, flows, templates, authentication codes, carousels,
URL buttons and generated documents; plus templates, automations and the business profile. One method
each on INeuraWhatsappClient.
Templates on WhatsApp or email
INeuraMessagingClient (services.AddNeuraMessagingClient(baseUrl)) sends templates on whichever
channel the token is for: authenticate with a WhatsApp number's secret and it sends WhatsApp, with an
email sender's secret and it sends email. The same request works for both.
await _messaging.EnsureAuthenticatedAsync(new AuthenticationModel
{
ApiKey = "your-api-key",
ApiSecret = "00000000-0000-..." // an email sender's secret
});
var result = await _messaging.SendTemplateMessageAsync(new MessagingTemplateMessage
{
To = "jane@example.com",
Template = new MessagingTemplate
{
Name = "order-confirmation",
// Fills the template's {{ data.Order }}. GetTemplatesAsync lists the names each template needs.
Parameters = new List<TemplateParamater> { new TemplateParamater { Name = "Order", Value = "A-1183" } }
},
Email = new MessagingEmailOptions { ReplyTo = "sales@example.com" } // ignored on WhatsApp
});
Email templates must be published and Transactional or Notification - marketing email is sent as a
campaign. An address that bounced or reported an earlier email as spam is refused with the reason in
Error. GetTemplatesAsync marks each template Sendable, so check that rather than its status.
GetMessageStatusAsync reports where a message has got to on either channel.
Email statuses arrive on /StatusReceived as WhatsappStatus, the same as WhatsApp. They add
bounced and complained (a spam report). Both are failures (IsFailure) and rank with failed,
so a spam report that follows delivered or read is still kept. read on an email means it was
opened.
Leads
When someone submits a Lead Ads form on one of your connected Facebook Pages, the platform retrieves
their answers and stores the lead. You can have each one pushed to you as it arrives - the
/LeadReceived callback, under Receiving - and you can pull them:
services.AddNeuraLeadsClient("https://your-deployment.example.com/");
public class LeadImporter
{
private readonly INeuraLeadsClient _leads;
public LeadImporter(INeuraLeadsClient leads) => _leads = leads;
public async Task ImportAsync(DateTimeOffset from, DateTimeOffset to)
{
// The same credentials and the same 7-day token as the WhatsApp client. A token for any
// of your numbers reads the leads of all your Pages.
await _leads.EnsureAuthenticatedAsync(Credentials);
// Follows the pages for you. GetLeadsAsync returns one page at a time instead.
foreach (var lead in await _leads.GetAllLeadsAsync(from, to))
{
// The same lead can reach you twice - pushed and then pulled, or a callback retried.
// LeadID is the same every time.
if (AlreadyHave(lead.LeadID))
continue;
Save(lead.LeadID, lead.FullName, lead.Email, lead.PhoneNumber,
lead.GetValue("car_make")); // any other question, by its key on the form
}
}
}
Three things to know about a pull:
- The two dates are on when the lead was stored on the platform (
DateReceived), not when the form was submitted (LeadTime). A lead Facebook announced late is stored well after it was submitted, and a window on submission time would already have moved past it. fromis inclusive andtoexclusive, so the end of one pull can be the start of the next. If you poll on a schedule, start each pull a minute or two before the last one ended anyway and letLeadIDdrop the overlap.- Pull is what covers the gaps in push: a callback endpoint is only sent the leads that arrive after it was added, and a callback that used up its nine attempts is not sent again.
Every id on a lead is a string. They are 64-bit numbers - keep them as text.
Receiving
Set the CallbackUrl on your number to one base URL and the platform POSTs each callback to that URL
plus its own endpoint. Implement INeuraCallbackHandler (or derive from NeuraCallbackHandler and
override only what you need) and let CallbackDispatcher route the bodies:
public class Receiver : NeuraCallbackHandler
{
public override Task MessageReceivedAsync(MessageModel m, CancellationToken ct = default)
{
if (m.MessageType == MessageTypes.Interactive)
{
var reply = CallbackPayload.ParseInteractiveReply(m.Content);
// Match on the id you set, never on the title.
if (reply?.ID == "quote_yes") { /* ... */ }
}
return Task.CompletedTask;
}
public override Task StatusReceivedAsync(WhatsappStatus s, CancellationToken ct = default)
{
// Statuses arrive out of order - keep the highest rank you have seen, not the last one.
if (MessageStatuses.Rank(s.MessageStatus) > RankStoredFor(s.MessageID))
Store(s);
return Task.CompletedTask;
}
}
services.AddNeuraCallbackHandler<Receiver>(); // registers the handler and the dispatcher
// ASP.NET Core: one route for every callback.
app.MapPost("/callbacks/{endpoint}", async (HttpRequest request, CallbackDispatcher dispatcher) =>
{
using var reader = new StreamReader(request.Body);
var result = await dispatcher.DispatchAsync(request.Path, await reader.ReadToEndAsync());
return Results.StatusCode(result.SuggestedStatusCode);
});
The endpoints, and the body each one sends:
| Endpoint | Body |
|---|---|
/WhatsappReceived |
MessageModel (IsFromMe false) |
/WhatsappSent |
MessageModel (IsFromMe true) |
/StatusReceived |
WhatsappStatus |
/AutomationStep |
AutomationStepCallback |
/AutomationComplete |
AutomationCompleteCallback |
/UserPreferenceReceived |
UserPreferenceCallback |
/LeadReceived |
LeadReceivedCallback - the Lead is under Data |
/LeadReceived is the one callback not set on a number: it goes to the callback endpoints configured
on the Facebook Page. Give a Page endpoint the same base URL as your number's and one receiver takes
both.
CallbackEndpoints, MessageTypes, MessageStatuses, InteractiveTypes, AutomationStatuses and
PreferenceValues hold the vocabularies as constants, and CallbackDelivery holds the delivery
rules (nine attempts over about 28 minutes; 404 and 405 are terminal).
Reading Content and Data
Content and Data change shape completely with MessageType, and Data arrives as a JSON string
rather than an object. CallbackPayload reads them, returning null instead of throwing when a
payload is not the shape asked for:
var interactive = CallbackPayload.ParseInteractiveData(m.Data); // what was sent, or what was tapped
var media = CallbackPayload.ParseMedia(m.Data); // original file name, mime type, thumbnail
var location = CallbackPayload.ParseLocation(m.Content); // latitude/longitude
var reply = CallbackPayload.ParseInteractiveReply(m.Content); // button/list reply id and title
var form = CallbackPayload.ParseFlowResponse(m.Content); // a submitted WhatsApp Flow
Three rules worth building in from the start
- Answer 2xx first, work afterwards. A receiver that holds the request open past the delivery timeout is retried even though it was working, which reaches you as duplicates.
- De-duplicate. On
IDfor the message callbacks, onMessageID+MessageStatusfor statuses. - Nothing is ordered. Inbound, outbound and statuses are delivered independently, and a retry reorders whatever it lands behind.
Proving a callback came from us
There is no signature header. Configure CallbackAuth against your number and check it with
CallbackAuthValidator - basic, a custom header, or a query parameter. Do not check the source IP;
it is not stable.
var auth = new CallbackAuthValidator(CallbackAuthModes.Header, "X-Api-Key", secret);
if (!auth.IsValid(name => request.Headers[name]))
return Results.Unauthorized();
No packages depend on Neura.CRM.SDK.
.NET Standard 2.0
- Microsoft.Extensions.Http (>= 10.0.12)
- System.Text.Json (>= 10.0.12)
| Version | Downloads | Last updated |
|---|---|---|
| 1.9.0-qa.2 | 5 | 10/22/2026 |
| 1.9.0-qa.1 | 3 | 10/22/2026 |
| 1.5.0 | 15 | 09/29/2026 |
| 1.4.0 | 10 | 09/10/2026 |
| 1.2.0 | 11 | 08/26/2026 |
| 1.1.0 | 21 | 05/26/2026 |
| 1.0.9 | 10 | 05/26/2026 |
| 1.0.8 | 17 | 05/11/2026 |
| 1.0.6 | 14 | 04/24/2026 |
| 1.0.5 | 17 | 04/23/2026 |
| 1.0.4 | 14 | 04/23/2026 |
| 1.0.3 | 15 | 04/23/2026 |
| 1.0.2 | 19 | 04/18/2026 |
| 1.0.1 | 15 | 04/17/2026 |
| 1.0.0 | 13 | 04/17/2026 |