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