Osy#betaRun it on Osyrin: osyrin.com

Kits · Text messages

46elks

SMS through 46elks — one message or a fan-out, with delivery reports and a dry run.

Package
Osysharp.Elks
Install
use Osysharp.Elks@0 {
  egress "api.46elks.com";
}
Group
Text messages
Tests
12 in the kit’s own suite
Builds on
—
Used by
46elks text sender

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 SPACESo 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 invoiceOne 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 login46elks 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.