16Oct, 2025
Adding User to a Send Subscription List on Form Submission, and Why the Right API Method Matters
I recently built a helper method for our Send gateway which adds or resubscribe users to specified lists. I reported a bug since resubscribing Users kept reverting to unsubscribed. I'll cover the workaround and the explanation from Send for this behaviour.
The API's Unexpected Behaviour
Ok, if you're familiar with Send's API, you may know that the Add multiple subscribers method will allow you to provide any number of subscribers, which will either add or update them into a specified list. Nice, and recyclable for a lot of different cases. There is one little caveat though.
If the subscriber was previously unsubscribed from the list, resubscribing them using this method appears to have worked based on the response, but they revert to unsubscribed.
I contacted support about this, and their response was:
When you have the setting: Unsubscribe from my account and never sent to them again, this will make someone unsubscribed in ALL mailing lists if this person chooses to unsubscribe from a campaign.
You can re-activate this recipient (regardless of the above setting) by doing a manual edit to the recipient via the UI or via the single subscribe call.
You cannot re-active this recipient if you do a bulk action from the UI or use the Add multiple subscribers call.
Now, an easy way around this is to just change the setting for “Unsubscribe from my account and never sent to them again”, but this conflicts with other business rules.
Add a new subscriber to the rescue
The new form helper I'm building deals with single subscribers in each execution, so it's time to stop being lazy and call Add a new subscriber.
I'm expanding on the gateway I've covered earlier, so the examples here leverage that. Let's start with the gateway changes.
Funny enough, with Send the model you need to use when adding or updating a subscriber is different from the one it sends back. It has to do with the custom fields, so I decided to name the model for its purpose. You can see it's passed here and the log is set up:
internal async Task<bool> AddSubscribersAsync(string mailingListID, AddOrUpdateSubscriber addOrUpdateSubscriber) { var logPrefix = $"[{GetType().FullName}.{MethodBase.GetCurrentMethod().Name}] ->";
Next, I use the endpoint formatter along with a global config item and the field I want to get. The stored value for this fiels in the config item is "{baseAddress}/subscribers/{MailingListID}/subscribe.{Format}?apikey={apiKey}", where baseAddress is "https://api.sitecoresend.io/v3/".
var endpointFormatter = new GatewayUtility.SendGatewayEndpointFormatter(_httpClient, GlobalConfigItem, Templates.GlobalComposableDXPSettings.Send.Fields.AddSubscriber); endpointFormatter.AddToken("mailingListID", mailingListID); var endpoint = endpointFormatter.Process(); if (string.IsNullOrWhiteSpace(endpoint)) { MyLogger.Error($"{logPrefix} Endpoint is not configured."); throw new InvalidOperationException("Endpoint is not configured."); }
Finally I call the API and return a response. Couldn't be simpler.
var json = JsonConvert.SerializeObject(addOrUpdateSubscriber); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync(endpoint, content); if (!response.IsSuccessStatusCode) { MyLogger.Error($"{logPrefix} Failed to post subscriber. Status code: {response.StatusCode}, Reason: {response.ReasonPhrase}"); return false; } return true; }
Setting up a Helper Method for Our Forms
Ok so the whole reason for all of this is so the development team can easily send submission data to Send lists. We're using a custom form module, and a custom submit action, depending on which form it is. First, let's talk about some supporting items.
I've stored some values to support the API calls, as seen here with this Enum:
public enum MailingListMemberStatus { Subscribed = 1, Unsubscribed = 2, Bounced = 3, Removed = 5, }
By now you probably recognize this pattern. I'll go over the method step by step.
Like before, I'm setting up the logger and then I'm finding the subscriber by email address. Why do I care, and not just post the new data straight away? You'll see further down in the method.
public static async Task<bool> AddOrUpdateSubscriberToList(string mailingListID, AddOrUpdateSubscriber addOrUpdateSubscriber) { var myLogger = LoggerFactory.GetLogger(SitecoreExtensions.Constants.Logging.Constants.CustomLogger); var logPrefix = $"[{typeof(Send).FullName}.{MethodBase.GetCurrentMethod().Name}] ->"; using (var sendGateway = new Gateways.SendGateway()) { SubscriberDetails subscriberDetails = null; try { subscriberDetails = await sendGateway.GetSubscriberbyEmailAddressAsync(mailingListID, addOrUpdateSubscriber.Email); } catch (Exception ex) { myLogger.Error($"Error retrieving subscriber {addOrUpdateSubscriber.Email} in list {mailingListID}. ({ex.Message})"); return false; }
If the subscriber is not found it's pretty obvious to just add them.
if (subscriberDetails == null) { if (await sendGateway.AddSubscribersAsync(mailingListID, addOrUpdateSubscriber)) { myLogger.Info($"{logPrefix} New subscriber {addOrUpdateSubscriber.Email} added to list {mailingListID}."); return true; } else { myLogger.Error($"{logPrefix} Failed to add subscriber {addOrUpdateSubscriber.Email} to list {mailingListID}."); return false; } } else {
Here's where it's a little different. First, I resubscribe them using the Enum above, and then I take all existing tags and combine them with any new ones coming from the object passed in. This is important because they would otherwise be overwritten.
// We always want to set them to subscribed when updating in this case. addOrUpdateSubscriber.SubscribeType = (int)MailingListMemberStatus.Subscribed; if (addOrUpdateSubscriber.Tags.Any()) { var existingTags = subscriberDetails.Context.Tags ?? new List<string>(); var newTags = addOrUpdateSubscriber.Tags.Except(existingTags).ToList(); if (newTags.Any()) { existingTags.AddRange(newTags); addOrUpdateSubscriber.Tags = existingTags; } } var updatedSubscriber = await sendGateway.UpdateSubscriberByIdAsync(mailingListID, subscriberDetails.Context.Id, addOrUpdateSubscriber); if (updatedSubscriber != null && updatedSubscriber.Context != null) { myLogger.Info($"{logPrefix} Existing subscriber {addOrUpdateSubscriber.Email} updated in list {mailingListID}."); return true; } else { myLogger.Error($"{logPrefix} Failed to update subscriber {addOrUpdateSubscriber.Email} in list {mailingListID}."); return false; } } } }
Making Things Even Easier for the Developers
The above method is all well and good, but I want to provide potentially a single line of code to do all this work, and that's where this method comes in.
You can see this method just accepts a few values and it builds the required object for the above one, so potentially someone could just call:
Helpers.Send.AddOrUpdateSubscriberToList("08533cfd-4802-4107-acb2-8eef814a15b9", "jane.doe@nothing.com");
The record for the subscriber only changes what values were provided, so their other existing values remain. Neat!
public async static Task<bool> AddOrUpdateSubscriberToList(string mailingListID, string email, string name, List<string> tags = null, Dictionary<string, string=""> customFields = null) { if (string.IsNullOrWhiteSpace(email)) { var myLogger = LoggerFactory.GetLogger(SitecoreExtensions.Constants.Logging.Constants.CustomLogger); var logPrefix = $"[{typeof(Send).FullName}.{MethodBase.GetCurrentMethod().Name}] ->"; myLogger.Warn($"{logPrefix} Email is required to create a subscriber."); return false; } var addOrUpdateSubscriber = new AddOrUpdateSubscriber(); if (!string.IsNullOrWhiteSpace(name)) addOrUpdateSubscriber.Name = name; if (!string.IsNullOrWhiteSpace(email)) addOrUpdateSubscriber.Email = email; if (tags != null && tags.Any()) addOrUpdateSubscriber.Tags = tags.Distinct().ToList(); if (customFields != null && customFields.Any()) { foreach (var field in customFields) { addOrUpdateSubscriber.AddCustomField(field.Key, field.Value); } } return await AddOrUpdateSubscriberToList(mailingListID, addOrUpdateSubscriber); }
What's Next?
You saw in the last method I provided an example where a Guid was used. I don't want our Authors working hard for this, so in the next post I'm going to cover a base template the forms will use which let's them hand pick list names instead.