I was totally confounded how difficult it was to pass metadata for both subscriptions
and payments
, so I'm posting some C# in hopes it may provide some illumination. Here, invoices are being created for both subscriptions
and payments
, and provisioning happens only in invoice.paid
. Search for metadata
.
First create the checkout session:
[Authorize]
[HttpPost("create-checkout-session/{id}")]
public async Task<IActionResult> CreateCheckoutSession(string id)
{
this.options.Value.Domain = $"https://{this.ControllerContext.HttpContext.Request.Host}";
var email = Account.Email;
var price = prices.First(prices => prices.Id == id);
if (price == null)
{
return BadRequest();
}
var priceId = price.PriceId;
var mode = price.Mode;
var modeStr = (mode == PurchaseMode.Subscription ? "subscription" : "payment");
var metadata = new Dictionary<string, string>()
{
{ "priceId", priceId },
{ "accountId", Account.Id},
{ "domains", domains }
};
var options = new Stripe.Checkout.SessionCreateOptions
{
SuccessUrl = $"{this.options.Value.Domain}/payday/ok",
CancelUrl = $"{this.options.Value.Domain}/payday/cancel",
Mode = modeStr,
LineItems = new List<SessionLineItemOptions>
{
new SessionLineItemOptions
{
Price = priceId,
Quantity = 1,
},
},
Metadata = metadata,
};
if (mode == PurchaseMode.Payment)
{
options.InvoiceCreation = new SessionInvoiceCreationOptions()
{
InvoiceData = new SessionInvoiceCreationInvoiceDataOptions() { Metadata = metadata },
Enabled = true,
};
options.PaymentIntentData = new SessionPaymentIntentDataOptions() { Metadata = metadata };
}
else if (mode == PurchaseMode.Subscription)
{
options.SubscriptionData = new SessionSubscriptionDataOptions() { Metadata = metadata };
}
// Blech. Can't set both email and customerId, so pick one.
if (Account?.StripeInfo?.CustomerId != null)
{
// already has an account, so use it!
options.Customer = Account.StripeInfo.CustomerId;
}
else
{
// we'll be creating a new account.
options.CustomerEmail = email;
options.CustomerCreation = (mode == PurchaseMode.Payment) ?
"always" : "if_required";
}
var service = new Stripe.Checkout.SessionService(this.client);
...
Then, in the webhook, fetch out the metadata (WHICH CONFUSINGLY IS DIFFERENT FOR SUBSCRIPTIONS vs PAYMENTS in invoice.paid
!)
// This webhook is called by Stripe as a customer is established, and then for Subscription and invoicing events
[HttpPost("webhook")]
public async Task<IActionResult> Webhook()
{
var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
Event stripeEvent;
try
{
stripeEvent = EventUtility.ConstructEvent(
json,
Request.Headers["Stripe-Signature"],
this.options.Value.WebhookSecret,
throwOnApiVersionMismatch: false
);
Console.WriteLine($"Webhook notification with type: {stripeEvent.Type} found for {stripeEvent.Id}");
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
return BadRequest();
}
string accountId = null; // our account id
string domains = null; // comma separated list of domains
Stripe.Customer customer = null;
Stripe.Invoice invoice = null;
Stripe.Checkout.Session session = null;
string customerId = null; // Stripe customer id
string email = null;
string mode = null; // payment, setup, or Subscription.
string priceId = null;
string invoiceId = null;
string subscriptionId = null;
string subscriptionStatus = null; // incomplete, incomplete_expired, trialing, active, past_due, canceled, or unpaid.
switch (stripeEvent.Type)
{
case "customer.created":
customer = stripeEvent.Data.Object as Stripe.Customer;
customerId = customer.Id;
email = customer.Email;
break;
case "invoice.paid":
// Continue to provision the Subscription as payments continue to be made.
// Store the status in your database and check when a user accesses your service.
// This approach helps you avoid hitting rate limits.
invoice = stripeEvent.Data.Object as Stripe.Invoice;
// HERE'S THE TRICKY PART
if (invoice?.SubscriptionDetails?.Metadata != null)
{
invoice.SubscriptionDetails.Metadata.TryGetValue("priceId", out priceId);
invoice.SubscriptionDetails.Metadata.TryGetValue("accountId", out accountId);
invoice.SubscriptionDetails.Metadata.TryGetValue("domains", out domains);
}
else
{
invoice.Metadata.TryGetValue("priceId", out priceId);
invoice.Metadata.TryGetValue("accountId", out accountId);
invoice.Metadata.TryGetValue("domains", out domains);
}
SubscriptionDetails.Metadata!!!??? What?