Send SMS through 46elks — one message or a fan-out, with delivery reports and a dry run — from a class written entirely in Osy# over the public Http.* surface.
Use case
Anything where a Nordic app has to reach a phone: a one-time code at sign-in, a booking reminder the morning of, a "your order has shipped", a maintenance notice to every account, a two-factor step for an admin action.
Reach for 46elks when your recipients are Nordic and you care where the data lives: Swedish alphanumeric sender IDs that actually deliver, EU-only handling, and per-message pricing a small app can read. Twilio is the same shape of kit and would be about the same size — this is the local answer, the way Osysharp.Resend is for mail.
It has no user interface, so there is nothing to screenshot. What you see is a text message.
Install and use
osy
// app.osy app Shop { model "**/*.osy"; use Osysharp.Elks@0 { egress "api.46elks.com"; } }
osy
using Osysharp.Elks; app.Secrets = [ new Secret("ElksUsername"), new Secret("ElksPassword") ]; ElksClient Sms() { return new ElksClient { Username = Secret.ElksUsername, Password = Secret.ElksPassword, From = "Acme" }; } void SendBookingReminder(Booking booking) { Sms().Send(new SmsMessage { To = booking.Customer.Mobile, Message = $"Reminder: {booking.Service} at {booking.StartsAt:HH:mm} on {booking.StartsAt:d MMMM}.", }); }
⭐ One factory, then every call site uses it. A function that RETURNS the configured client compiles: the compiler notes that the credential field was assigned a secret, so the object may travel between your functions while the field itself cannot be read back out — returned, logged, stored, copied or sent to a page. Holding the client in a component is refused (an object reaches the browser with its fields), and so is calling the factory from a request, so the key cannot leave the server by any of those routes. See osy docs security → credential fields.
The whole network grant is egress "api.46elks.com", and an app that uses the kit grants that host or does not compile. It asks for no secret: the credentials are settings on the client, so your app names its own.
Four things that bite everyone
| Recipients must be E.164 | +46701234567, never 0701234567 — and your own database column very likely holds the local form. Refused here, naming the fix, instead of by a provider message about a form field. |
A bare + in a form body means SPACE | So an unescaped recipient arrives as to= 46701234567. FormBody percent-encodes it, and a test holds that. This is the single most common hand-rolled-SMS bug. |
parts explains your invoice | One emoji or one curly quote pasted from a document switches the message to UCS-2 and drops the per-part limit from 160 to 70 — so a message that looks like one SMS is three. parts is the only field that says so. Log it. |
| A 401 is the API credentials, not your login | 46elks issues separate API credentials. The refusal says this, because it is almost always the confusion. |
Fan-out is N requests, and the kit says so
46elks takes one recipient per call, so unlike Osysharp.Resend's batch there is no single-round-trip form. SendToMany therefore attempts every recipient and raises at the end naming the ones that failed:
osy
var sms = new ElksClient { Username = Secret.ElksUsername, Password = Secret.ElksPassword, From = "Acme" }; sms.SendToMany(members.Select(m => m.Mobile).ToList(), "We are upgrading at 02:00.");
Stopping at the first failure would leave you unable to say who was reached, which is the worst state for a notification run. One bad number in a list of two hundred must not decide whether the other 199 hear from you.
Delivery reports
Set WhenDelivered to a URL and 46elks POSTs to it when the status changes. The report is form-encoded and carries the message id, a status of delivered or failed, and — only when delivered — a delivered timestamp.
Reading it is your endpoint's job, not this kit's:
osy
[AllowAnonymous] void SmsDelivered(string id, string status) { var sent = SentSms.Where(s => s.ProviderId == id).FirstOrDefault(); if (sent != null) { sent.Delivered = status == "delivered"; } }
⚠ That endpoint is public and unauthenticated, and 46elks does not sign delivery reports. So treat the report as a hint rather than as authority: it may say a message was delivered that was not, because anyone can post to the URL. Never let one grant anything — if a delivery report is load-bearing in your flow, re-read the message from the API. (This is the opposite of Osysharp.Payments.Stripe's webhook, which is signed and is authority.)
What is not covered yet
- Message history (
GET /a1/SMS) — the endpoint exists; the response envelope is not verified here, and a guessed shape would be worse than the gap. - MMS, voice calls and number provisioning — 46elks does all three. Same shape of kit, not written.
- Parsing the delivery report — see above; it belongs to whatever receives your endpoint's form body.
Source and tests
Everything is in model/ — two files, no C# anywhere. tests/ is the producer's own project: tests that assert what went on the wire rather than that a call returned. It does not ship, and osy publish runs it and refuses a version that fails.
Get it
Add the kit to your app’s manifest:
osy
app MyApp { use Osysharp.Elks@0 { egress "api.46elks.com"; } }
Then run osy lock: it downloads the release, verifies it by its content address and pins that version in osyrin.lock. osy kits Osysharp.Elks shows what the kit is for, from inside your app.