Trust is earned, not given

A different perspective

2018-09-18 · Projects

Shopify in C#, part 1: the Admin API, a custom app, and your first customers

Part 1from the Shopify in C# series · 6 parts in all

Stripe and Shopify are the two API surfaces of MapleCart's stack, and they could not be more different: Stripe is a payments ledger you call with intents; Shopify is a whole store — customers, products, orders, fulfillments — that you extend. This series teaches the Admin API to a .NET developer in six parts. Part 1: get a credential, make a call, and be the person who "knows Shopify".

Where the token comes from (2018 edition)

Two kinds of apps talk to a store's Admin API. Public apps are distributed to many merchants and use OAuth (the authorization-code redirect dance). A private app is your own credential for your own store: you create it in the admin, and it authenticates with plain HTTP basic auth — the API key and password joined by a colon. That is the simplest possible start:

using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;

// 2018: private app = basic auth. No OAuth, no tokens to refresh.
var http = new HttpClient();
var shop = "maplecart.myshopify.com";
http.BaseAddress = new System.Uri($"https://{shop}/admin/");
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
    Convert.ToBase64String(Encoding.ASCII.GetBytes(
        $"{apiKey}:{password}")));   // from Shopify admin: Apps -> Manage private apps

// The resource grid that organizes the whole REST API:
//   /admin/customers.json        customers
//   /admin/orders.json           orders
//   /admin/draft_orders.json     "invoices": orders you create for the customer
//   /admin/products.json         catalog
var resp = await http.GetAsync("customers.json?limit=5");
var json = await resp.Content.ReadAsStringAsync();   // Shopify's JSON, verbatim

Note the shape: everything is {resource}.json and Shopify's JSON comes back exactly as documented — a thin C# layer over raw HttpClient and System.Text.Json keeps you closest to the docs (and is precisely how our generic integration library is built).

The first real query: customers

// customers.json wraps the list in a root property named after the resource.
// { "customers": [ { "id": 8822423351, "first_name": "Beth", ... } ] }
using System.Text.Json;

var doc = JsonDocument.Parse(json);
foreach (var c in doc.RootElement.GetProperty("customers").EnumerateArray())
{
    var id = c.GetProperty("id").GetInt64();
    var email = c.GetProperty("email").GetString();
    var orders = c.GetProperty("orders_count").GetInt32();   // rollup field
    Console.WriteLine($"{id} {email} orders={orders}");
}
// Get one: /admin/customers/{id}.json -> { "customer": {...} }
// Search:  /admin/customers/search.json?query=email:[email protected]

Pagination, 2018 style

REST pagination this year is the simple kind: ?page=2&limit=50 and you keep asking for pages until an empty array returns. It works, it is easy to script — and it is also the thing the API team will spend 2019 replacing (part 2 shows the cursor scheme that replaces it).

Writing: create and update

Create POSTs the resource wrapped in its root property; send exactly the fields you have and Shopify fills in the rest. One habit from the start: build request bodies with the serializer, never with hand-concatenated JSON strings — quoting accidents are the classic integration bug, and the serializer is what this whole series uses:

var body = JsonSerializer.Serialize(new {
    customer = new {
        email = "[email protected]",
        first_name = "Beth",
        last_name = "Carpenter",
        tags = "vip,wholesale"
    }
});
var resp = await http.PostAsync("customers.json",
    new StringContent(body, Encoding.UTF8, "application/json"));

// Update: PUT to the id URL. Note PUT - Shopify's REST API has no PATCH verb;
// if your client library exposes "Patch", it is translating to PUT under the hood.
var upd = JsonSerializer.Serialize(new { customer = new { tags = "vip" } });
var resp2 = await http.PutAsync($"customers/{id}.json",
    new StringContent(upd, Encoding.UTF8, "application/json"));

// Errors come back as { "errors": { "email": ["is not valid"] } } - check
// IsSuccessStatusCode before parsing, then surface that structure verbatim.

That is the whole loop: authenticate, GET/POST/PUT {resource}.json, inspect the root property. Parts 2–6 go deep — customers done properly (search, addresses, metafields), the GraphQL pivot, orders and draft-order invoices, Shopify Payments transactions and refunds, and fulfillments in the 2025 GraphQL-first world.