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.
  • from is inclusive and to exclusive, 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 let LeadID drop 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 ID for the message callbacks, on MessageID + MessageStatus for 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

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