Du willst das Prinzip verstehen, ohne dich durch jeden ERP-Sonderfall zu wühlen? Hier ist eine vereinfachte Variante meiner C#-Azure-Function, die UPS-Sendungen anlegt und das ZPL-Label zurückgibt.
Wir gehen Schritt für Schritt vor, zeigen kleine Code-Stücke – und am Ende bekommst du den kompletten Beispielcode.


1) Zielbild in einem Satz

Eine HTTP-Function nimmt ein JSON an (Token, Basisdaten, Wunsch „Standard“ oder „Express“), probiert automatisch den passenden UPS-Servicecode, baut die UPS-Payload und gibt Trackingnummer & ZPL zurück.


2) Einfache Ein-/Ausgabe (Contract)

Request (minimal)

jsonCopyEdit{
  "upsToken": "eyJhbGciOi...",
  "mode": "STD",                     // oder "EXP"
  "fileRef": "CONT-000123",
  "shipper": { "name":"Contoso", "phone":"004921042333100", "country":"DE", "postal":"42781", "city":"Haan", "street":"Musterstr. 1" },
  "shipTo":  { "name":"Max Mustermann", "phone":"0049210423", "country":"DE", "postal":"10115", "city":"Berlin", "street":"Hauptstr. 5" },
  "package": { "weightKg": 12.5 }
}

Response (Erfolg)

jsonCopyEdit{
  "trackingNumber": "1Z999AA10123456784",
  "zpl": "^XA^PW812^LL1218...^XZ",
  "serviceCode": "11"
}

3) Body lesen & validieren (Snippet)

Wir lesen den Request-Body, prüfen Pflichtfelder und halten den Code schlank (keine fremden Helper).

csharpCopyEditstring body = await new StreamReader(req.Body).ReadToEndAsync();
if (string.IsNullOrWhiteSpace(body))
    return Json(req, HttpStatusCode.BadRequest, new { error = "Empty body" });

var input = JsonSerializer.Deserialize<Input>(body, JsonOpts);
if (input is null || string.IsNullOrWhiteSpace(input.UpsToken))
    return Json(req, HttpStatusCode.BadRequest, new { error = "upsToken missing" });

Warum? Früh scheitern spart Zeit. Klare Fehlermeldungen helfen beim Testen mit Postman/curl.


4) Servicecode-Fallback – das Herzstück (Snippet aus meinem Programm)

UPS kann einen Dienst „zeitweise nicht verfügbar“ melden. Wir probieren automatisch weiter.

csharpCopyEditvar serviceCodesToTry = input.Mode?.ToUpper() == "EXP"
    ? new[] { "07", "11" }  // Express bevorzugt, dann Standard
    : new[] { "11", "07" }; // Standard bevorzugt, dann Express

foreach (var code in serviceCodesToTry)
{
    var requestJson = BuildUpsRequest(input, code);
    var (ok, resp, status) = await CallUpsAsync(input.UpsToken!, requestJson);

    if (ok && status == 200)
        return ParseSuccess(req, resp, code);

    if (resp?.IndexOf("unavailable", StringComparison.OrdinalIgnoreCase) >= 0)
        continue; // nächsten Code testen

    // anderer Fehler -> abbrechen und Rohantwort zurückgeben
    return Json(req, HttpStatusCode.OK, new { error = resp, serviceCode = code });
}

Merke: 11 = Standard, 07 = Express. „unavailable“ → nächster Code, alles andere → sauber zurückmelden.


5) UPS-Payload bauen (kompakt & lesbar)

Wir liefern genau das, was UPS braucht: Shipper, ShipTo, Service, Package, Label-Spezifikation.

csharpCopyEditstatic string BuildUpsRequest(Input input, string serviceCode)
{
    var payload = new
    {
        ShipmentRequest = new
        {
            Request = new { RequestOption = "nonvalidate", SubVersion = "2108" },
            Shipment = new
            {
                Description = "Goods",
                Shipper = new {
                    Name = input.Shipper.Name,
                    AttentionName = input.Shipper.Name,
                    Phone = new { Number = input.Shipper.Phone ?? "0" },
                    ShipperNumber = "9FE219",
                    Address = new {
                        AddressLine = new[] { input.Shipper.Street },
                        City = input.Shipper.City,
                        PostalCode = input.Shipper.Postal,
                        CountryCode = input.Shipper.Country
                    }
                },
                ShipTo = new {
                    Name = input.ShipTo.Name,
                    AttentionName = input.ShipTo.Name,
                    Phone = new { Number = input.ShipTo.Phone ?? "0" },
                    Address = new {
                        AddressLine = new[] { input.ShipTo.Street },
                        City = input.ShipTo.City,
                        PostalCode = input.ShipTo.Postal,
                        CountryCode = input.ShipTo.Country
                    }
                },
                PaymentInformation = new {
                    ShipmentCharge = new[] {
                        new {
                            Type = "01",
                            BillShipper = new { AccountNumber = "9FE219" }
                        }
                    }
                },
                Service = new { Code = serviceCode },
                Package = new[] {
                    new {
                        Description = "Goods",
                        Packaging = new { Code = "02" },
                        PackageWeight = new {
                            UnitOfMeasurement = new { Code = "KGS" },
                            Weight = input.Package.WeightKg.ToString(System.Globalization.CultureInfo.InvariantCulture)
                        },
                        PackageServiceOptions = new { }
                    }
                },
                ShipmentServiceOptions = new { },
                ReferenceNumber = new[] { new { Value = input.FileRef ?? "" } }
            },
            LabelSpecification = new {
                LabelImageFormat = new { Code = "zpl" },
                LabelStockSize = new { Height = "6", Width = "4" },
                CharacterSet = "UTF-8"
            }
        }
    };

    return JsonSerializer.Serialize(payload, JsonOpts);
}

Warum so? Ein klarer, anonymes Objekt → JSON. Keine riesigen Model-Klassen nötig, schnell anzupassen.


6) UPS aufrufen & ZPL extrahieren (Snippet)

HTTP-Call mit Pflicht-Headern; danach Tracking & ZPL rausziehen.

csharpCopyEditstatic async Task<(bool ok, string body, int status)> CallUpsAsync(string token, string json)
{
    using var http = new HttpClient();
    http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
    http.DefaultRequestHeaders.Add("transId", "demo-123");
    http.DefaultRequestHeaders.Add("transactionSrc", "demo");
    http.DefaultRequestHeaders.Add("version", "v2403");

    var resp = await http.PostAsync(
        "https://onlinetools.ups.com/api/shipments/v2403/ship?additionaladdressvalidation=string",
        new StringContent(json, Encoding.UTF8, "application/json"));

    var body = await resp.Content.ReadAsStringAsync();
    return (resp.IsSuccessStatusCode, body, (int)resp.StatusCode);
}

static HttpResponseData ParseSuccess(HttpRequestData req, string raw, string code)
{
    using var doc = JsonDocument.Parse(raw);
    var root = doc.RootElement;
    string tracking = root.GetProperty("ShipmentResponse")
                          .GetProperty("ShipmentResults")
                          .GetProperty("ShipmentIdentificationNumber")
                          .GetString() ?? "";

    string zplBase64 = root.GetProperty("ShipmentResponse")
                           .GetProperty("ShipmentResults")
                           .GetProperty("PackageResults")[0]
                           .GetProperty("ShippingLabel")
                           .GetProperty("GraphicImage")
                           .GetString() ?? "";

    string zpl = string.IsNullOrEmpty(zplBase64)
        ? ""
        : Encoding.ASCII.GetString(Convert.FromBase64String(zplBase64));

    return Json(req, HttpStatusCode.OK, new { trackingNumber = tracking, zpl, serviceCode = code });
}

7) Testen & erweitern

  • Postman/curl: Request wie oben, Token als echtes UPS-Bearer Token.
  • Edge-Fälle: Gewicht leer? → 0 blockt; immer Punkt als Dezimaltrenner verwenden.
  • Ausbau:
    • BillReceiver (Account + PLZ) einbauen
    • Mehrere Pakete → Array aufbauen
    • Token via Key Vault / Managed Identity holen
    • Idempotenz (pro fileRef) & Telemetrie (App Insights)

Vollständiges Beispiel (eine Datei)

Hinweis: Das ist eine vereinfachte Demo ohne ERP-Abhängigkeiten. Sie läuft als .NET-8-Azure-Function (isolated). Passe die Namespaces an dein Projekt an.

csharpCopyEditusing System.Net;
using System.Text;
using System.Text.Json;
using System.Net.Http.Headers;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;

namespace UpsLabelDemo;

public class CreateUpsShipmentSimple
{
    private static readonly JsonSerializerOptions JsonOpts = new()
    { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false };

    public record Party(string Name, string? Phone, string Country, string Postal, string City, string Street);
    public record Pack(double WeightKg);
    public record Input(string? UpsToken, string? Mode, string? FileRef, Party Shipper, Party ShipTo, Pack Package);

    [Function("CreateUpsShipmentSimple")]
    public async Task<HttpResponseData> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
    {
        string body = await new StreamReader(req.Body).ReadToEndAsync();
        if (string.IsNullOrWhiteSpace(body))
            return Json(req, HttpStatusCode.BadRequest, new { error = "Empty body" });

        Input? input;
        try { input = JsonSerializer.Deserialize<Input>(body, JsonOpts); }
        catch (Exception ex) { return Json(req, HttpStatusCode.BadRequest, new { error = $"JSON parse error: {ex.Message}" }); }

        if (input is null || string.IsNullOrWhiteSpace(input.UpsToken))
            return Json(req, HttpStatusCode.BadRequest, new { error = "upsToken missing" });

        if (input.Package is null || input.Package.WeightKg <= 0)
            return Json(req, HttpStatusCode.BadRequest, new { error = "package.weightKg must be > 0" });

        var serviceCodesToTry = input.Mode?.ToUpper() == "EXP" ? new[] { "07", "11" } : new[] { "11", "07" };

        foreach (var code in serviceCodesToTry)
        {
            var requestJson = BuildUpsRequest(input, code);
            var (ok, resp, status) = await CallUpsAsync(input.UpsToken!, requestJson);

            if (ok && status == 200)
                return ParseSuccess(req, resp, code);

            if (!string.IsNullOrEmpty(resp) && resp.IndexOf("unavailable", StringComparison.OrdinalIgnoreCase) >= 0)
                continue;

            return Json(req, HttpStatusCode.OK, new { error = resp, serviceCode = code });
        }

        return Json(req, HttpStatusCode.OK, new { error = "No service code available (STD/EXP)", serviceCode = "n/a" });
    }

    static string BuildUpsRequest(Input input, string serviceCode)
    {
        var payload = new
        {
            ShipmentRequest = new
            {
                Request = new { RequestOption = "nonvalidate", SubVersion = "2108" },
                Shipment = new
                {
                    Description = "Goods",
                    Shipper = new
                    {
                        Name = input.Shipper.Name,
                        AttentionName = input.Shipper.Name,
                        Phone = new { Number = input.Shipper.Phone ?? "0" },
                        ShipperNumber = "9FE219",
                        Address = new
                        {
                            AddressLine = new[] { input.Shipper.Street },
                            City = input.Shipper.City,
                            PostalCode = input.Shipper.Postal,
                            CountryCode = input.Shipper.Country
                        }
                    },
                    ShipTo = new
                    {
                        Name = input.ShipTo.Name,
                        AttentionName = input.ShipTo.Name,
                        Phone = new { Number = input.ShipTo.Phone ?? "0" },
                        Address = new
                        {
                            AddressLine = new[] { input.ShipTo.Street },
                            City = input.ShipTo.City,
                            PostalCode = input.ShipTo.Postal,
                            CountryCode = input.ShipTo.Country
                        }
                    },
                    PaymentInformation = new
                    {
                        ShipmentCharge = new[] {
                            new {
                                Type = "01",
                                BillShipper = new { AccountNumber = "9FE219" }
                            }
                        }
                    },
                    Service = new { Code = serviceCode },
                    Package = new[] {
                        new {
                            Description = "Goods",
                            Packaging = new { Code = "02" },
                            PackageWeight = new
                            {
                                UnitOfMeasurement = new { Code = "KGS" },
                                Weight = input.Package.WeightKg.ToString(System.Globalization.CultureInfo.InvariantCulture)
                            },
                            PackageServiceOptions = new { }
                        }
                    },
                    ShipmentServiceOptions = new { },
                    ReferenceNumber = new[] { new { Value = input.FileRef ?? "" } }
                },
                LabelSpecification = new
                {
                    LabelImageFormat = new { Code = "zpl" },
                    LabelStockSize = new { Height = "6", Width = "4" },
                    CharacterSet = "UTF-8"
                }
            }
        };

        return JsonSerializer.Serialize(payload, JsonOpts);
    }

    static async Task<(bool ok, string body, int status)> CallUpsAsync(string token, string json)
    {
        using var http = new HttpClient();
        http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        http.DefaultRequestHeaders.Add("transId", "demo-123");
        http.DefaultRequestHeaders.Add("transactionSrc", "demo");
        http.DefaultRequestHeaders.Add("version", "v2403");

        var resp = await http.PostAsync(
            "https://onlinetools.ups.com/api/shipments/v2403/ship?additionaladdressvalidation=string",
            new StringContent(json, Encoding.UTF8, "application/json"));

        var body = await resp.Content.ReadAsStringAsync();
        return (resp.IsSuccessStatusCode, body, (int)resp.StatusCode);
    }

    static HttpResponseData ParseSuccess(HttpRequestData req, string raw, string code)
    {
        using var doc = JsonDocument.Parse(raw);
        var root = doc.RootElement;
        string tracking = root.GetProperty("ShipmentResponse")
                              .GetProperty("ShipmentResults")
                              .GetProperty("ShipmentIdentificationNumber")
                              .GetString() ?? "";

        string zplBase64 = root.GetProperty("ShipmentResponse")
                               .GetProperty("ShipmentResults")
                               .GetProperty("PackageResults")[0]
                               .GetProperty("ShippingLabel")
                               .GetProperty("GraphicImage")
                               .GetString() ?? "";

        string zpl = string.IsNullOrEmpty(zplBase64)
            ? ""
            : Encoding.ASCII.GetString(Convert.FromBase64String(zplBase64));

        return Json(req, HttpStatusCode.OK, new { trackingNumber = tracking, zpl, serviceCode = code });
    }

    static HttpResponseData Json(HttpRequestData req, HttpStatusCode code, object obj)
    {
        var res = req.CreateResponse(code);
        res.Headers.Add("Content-Type", "application/json; charset=utf-8");
        var text = JsonSerializer.Serialize(obj, JsonOpts);
        res.WriteString(text);
        return res;
    }
}

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert