Services

BigCommerce

A BigCommerce store run from outside it: the catalog on V3 and its data and meta envelope, orders on V2 with bare lists and an empty 204, both shapes of error, batches of ten with their partial failures, the rate-limit headers waited out and reported, and an app's signed payloads checked in both the old form and the JWT.

Version 1.0.0 · 17 functions · 6 crates

#ecommerce#catalog#jwt#rate-limits

Functions

What use bigcommerce.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use bigcommerce_rate_limit, bigcommerce_call from bigcommerce (Quillo 0.6).

bigcommerce_rate_limit() → any

What the last answer said of the rate limit: { left, quota, reset_ms, window_ms }, or null before one.

bigcommerce_call(method: str, path: str, body: any) → anyor an error

Any other call: path under the store, with its version, like "/v3/customers", body or null. Gives the answer as it came.

bigcommerce_products(filter: any, max_items: int) → listor an error

Products through every page (V3): filter is BigCommerce's (include as "variants,images", sku, name:like, is_visible...), max_items caps them (0: all).

bigcommerce_product(id: int, include: str) → anyor an error

One product by id; include is "variants,images" or "".

bigcommerce_product_create(fields: any) → anyor an error

Makes a product: name, type ("physical" or "digital"), weight and price are required.

bigcommerce_product_update(id: int, fields: any) → anyor an error

Changes a product.

bigcommerce_product_delete(id: int) → boolor an error

Deletes a product.

bigcommerce_products_update(records: list) → anyor an error

Changes many products, 10 at a time as the API wants: each record has its id. Gives { updated, errors }.

bigcommerce_variants(product_id: int) → listor an error

A product's variants through every page.

bigcommerce_categories(filter: any, max_items: int) → listor an error

Categories through every page; filter as BigCommerce's, max_items caps them (0: all).

bigcommerce_orders(filter: any, max_items: int) → listor an error

Orders through every page (V2): filter is BigCommerce's (status_id, min_date_created, customer_id, sort...), max_items caps them (0: all). No orders is an empty list, though the API says 204.

bigcommerce_order(id: int) → anyor an error

One order by id (V2).

bigcommerce_order_products(id: int) → listor an error

The products of an order (V2), through every page.

bigcommerce_order_create(fields: any) → anyor an error

Makes an order (V2): products and billing_address are required.

bigcommerce_order_update(id: int, fields: any) → anyor an error

Changes an order (V2), as { status_id: 10 }.

bigcommerce_signed_payload(signed_payload: str, client_secret: str) → anyor an error

Checks the older signed_payload an app's load callback gets (base64 of JSON.base64 of the hex HMAC), with the app's client secret. Gives the payload { user, owner, context, store_hash, timestamp }.

bigcommerce_signed_jwt(token: str, client_id: str, client_secret: str, at: int) → anyor an error

Checks a signed_payload_jwt (HS256 with the client secret), with its aud, iss, exp and nbf. Gives the claims, store_hash taken out of sub. at is the time to check against (0: now).

Get it

$ quillo install bigcommerce               # bigcommerce.qwer into this folder$ quillo install bigcommerce --example     # and its example, bigcommerce.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/bigcommerce.qwer$ curl -O https://quillolang.org/plugins/files/bigcommerce.qwe

Then, in your program, all of it or only what you call:

main.qwe
use bigcommerce.qweruse bigcommerce_rate_limit, bigcommerce_call from bigcommerce      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
ureq = "2"hmac = "0.12"sha2 = "0.10"hex = "0.4"base64 = "0.22"serde_json = { version = "1", features = ["preserve_order"] }

Example

bigcommerce.qwe
// A BigCommerce store run from outside it: products on V3, orders on V2, a batch with part refused,// the rate limit, and an app's signed payloads checked in both forms.////   BIGCOMMERCE_STORE_HASH=... BIGCOMMERCE_ACCESS_TOKEN=... BIGCOMMERCE_CLIENT_ID=... BIGCOMMERCE_CLIENT_SECRET=... quillo run bigcommerce.qwe//   then open /products, /orders, /batch, /limits, /signed and /failures//// BIGCOMMERCE_API_URL reaches a test server. An app's load callback gets `signed_payload_jwt`: see// GET /load.use "bigcommerce.qwer"route GET /products {    let p = bigcommerce_product_create({ name: "Blue mug", type: "physical", weight: 1, price: 12.5, sku: "MUG-B" })?    bigcommerce_product_update(p.id, { price: 13 })?    let read = bigcommerce_product(p.id, "variants")?    {        name: read.name, price: read.price,        variants: len(read.variants), same: len(bigcommerce_variants(p.id)?),        all: len(bigcommerce_products({ is_visible: true }, 0)?),        capped: len(bigcommerce_products({}, 300)?),        taken: bigcommerce_product_create({ name: "Blue mug", type: "physical", weight: 1, price: 1 }).error,        bad: bigcommerce_product_create({ name: "x" }).error,        deleted: bigcommerce_product_delete(p.id)?,        categories: len(bigcommerce_categories({}, 0)?),    }}route GET /orders {    let o = bigcommerce_order_create({ products: [{ name: "Mug", quantity: 2, price_inc_tax: 10, price_ex_tax: 10 }], billing_address: { first_name: "Ada", email: "ada@example.com" } })?    {        total: o.total_inc_tax,        shipped: bigcommerce_order_update(o.id, { status_id: 10 })?.status,        lines: map(bigcommerce_order_products(o.id)?, (p) => p.quantity),        all: len(bigcommerce_orders({ status_id: 11 }, 0)?),        capped: len(bigcommerce_orders({ status_id: 11 }, 260)?),        none: bigcommerce_orders({ status_id: 5 }, 0)?,        no_lines: bigcommerce_order_create({ billing_address: {} }).error,    }}route GET /batch {    let found = bigcommerce_products({ "name:like": "Item" }, 25)?    let changes = map(found, (p) => { id: p.id, price: 2 })    let r = bigcommerce_products_update(changes + [{ id: 999999, price: 1 }])?    { updated: r.updated, errors: len(r.errors), first: r.errors[0].title, no_id: bigcommerce_products_update([{ price: 1 }]).error }}route GET /limits {    let before = bigcommerce_call("get", "/v3/catalog/summary", null)?    { summary: before.data.variant_count, seen: bigcommerce_rate_limit(), waited: bigcommerce_call("get", "/v3/busy", null)?.data }}route GET /load {    let claims = bigcommerce_signed_jwt(request.query.signed_payload_jwt ?? "", env("BIGCOMMERCE_CLIENT_ID", ""), env("BIGCOMMERCE_CLIENT_SECRET", ""), 0)    if is_err(claims) { reply 401 claims.error }    { store: claims.store_hash, user: claims.user.email }}// Payloads made outside this plugin, by node's crypto.route GET /signed {    let old = "eyJ1c2VyIjp7ImlkIjoxLCJlbWFpbCI6ImFkYUBleGFtcGxlLmNvbSJ9LCJvd25lciI6eyJpZCI6MX0sImNvbnRleHQiOiJzdG9yZXMvYWJjMTIzIiwic3RvcmVfaGFzaCI6ImFiYzEyMyIsInRpbWVzdGFtcCI6MTczMDAwMDAwMC41fQ==.M2NlODMxZWZhYmFhY2NmNmY4NDQ4OTllODYzZjk0NzEyNGEwMDJkMzM0MDk3OGExY2YxMTk4NTljY2UxMTRkYQ=="    let jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJjbGllbnQtaWQiLCJpc3MiOiJiYyIsImlhdCI6MTczMDAwMDAwMCwibmJmIjoxNzMwMDAwMDAwLCJleHAiOjE3MzAwODY0MDAsImp0aSI6ImoxIiwic3ViIjoic3RvcmVzL2FiYzEyMyIsInVzZXIiOnsiaWQiOjEsImVtYWlsIjoiYWRhQGV4YW1wbGUuY29tIn0sIm93bmVyIjp7ImlkIjoxLCJlbWFpbCI6Im93bmVyQGV4YW1wbGUuY29tIn0sInVybCI6Ii8ifQ.6b3xmXbd45b4eOjG-XOT2dNYYx8QMCFsLfoUqfbXJsA"    let at = 1730000100    {        old: bigcommerce_signed_payload(old, "client-secret")?.store_hash,        changed: bigcommerce_signed_payload("x" + old, "client-secret").error,        wrong: bigcommerce_signed_payload(old, "other").error,        one_part: bigcommerce_signed_payload("abc", "client-secret").error,        jwt: [bigcommerce_signed_jwt(jwt, "client-id", "client-secret", at)?.store_hash, bigcommerce_signed_jwt(jwt, "client-id", "client-secret", at)?.user.email],        late: bigcommerce_signed_jwt(jwt, "client-id", "client-secret", at + 100000).error,        other_app: bigcommerce_signed_jwt(jwt, "another", "client-secret", at).error,        other_secret: bigcommerce_signed_jwt(jwt, "client-id", "other", at).error,        none_alg: bigcommerce_signed_jwt("eyJhbGciOiJub25lIn0.e30.", "client-id", "client-secret", at).error,    }}route GET /failures = {    missing: bigcommerce_product(999999, "").error,    order: bigcommerce_order(999999).error,}
$ quillo run bigcommerce.qwe$ curl 'localhost:8080/products'$ curl 'localhost:8080/orders'$ curl 'localhost:8080/batch'$ curl 'localhost:8080/limits'$ curl 'localhost:8080/signed'$ curl 'localhost:8080/failures'

It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.

Source

bigcommerce.qwer
//! BigCommerce's APIs without an SDK: the catalog on V3 and its data and meta envelope, orders on V2 with bare lists and an empty 204, both kinds of error, batch answers that partly fail, the rate-limit headers waited out and reported, and an app's signed payloads checked in both the old form and the JWT.//! version: 1.0.0//!//! [dependencies]//! ureq = "2"//! hmac = "0.12"//! sha2 = "0.10"//! hex = "0.4"//! base64 = "0.22"//! serde_json = { version = "1", features = ["preserve_order"] }use base64::{engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, Engine};use hmac::{Hmac, Mac};use serde_json::{json, Value};use std::sync::Mutex;use std::time::{Duration, SystemTime, UNIX_EPOCH};// BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN reach the store; BIGCOMMERCE_API_URL the API// (https://api.bigcommerce.com when not set).const PAGE: i64 = 250;fn setting(k: &str) -> Option<String> {    std::env::var(k).ok().filter(|v| !v.is_empty())}fn root() -> Result<String, String> {    let host = setting("BIGCOMMERCE_API_URL").unwrap_or_else(|| "https://api.bigcommerce.com".into());    let hash = setting("BIGCOMMERCE_STORE_HASH").ok_or("BIGCOMMERCE_STORE_HASH is not set")?;    Ok(format!("{}/stores/{}", host.trim_end_matches('/'), hash))}fn enc(s: &str) -> String {    s.bytes().map(|b| if b.is_ascii_alphanumeric() || b"-_.~:".contains(&b) { (b as char).to_string() } else { format!("%{:02X}", b) }).collect()}fn query(filter: &Value) -> String {    filter.as_object().map_or(String::new(), |m| {        m.iter()            .map(|(k, v)| {                let t = match v {                    Value::Array(a) => a.iter().map(|x| x.as_str().map(str::to_string).unwrap_or_else(|| x.to_string())).collect::<Vec<_>>().join(","),                    Value::String(s) => s.clone(),                    o => o.to_string(),                };                format!("&{}={}", enc(k), enc(&t))            })            .collect()    })}static LIMITS: Mutex<Option<Value>> = Mutex::new(None);fn note(r: &ureq::Response) {    let g = |h: &str| r.header(h).and_then(|v| v.parse::<i64>().ok());    if let Some(left) = g("X-Rate-Limit-Requests-Left") {        *LIMITS.lock().unwrap_or_else(|e| e.into_inner()) = Some(json!({ "left": left, "quota": g("X-Rate-Limit-Requests-Quota"), "reset_ms": g("X-Rate-Limit-Time-Reset-Ms"), "window_ms": g("X-Rate-Limit-Time-Window-Ms") }));    }}/// What the last answer said of the rate limit: { left, quota, reset_ms, window_ms }, or null before one.pub fn bigcommerce_rate_limit() -> serde_json::Value {    LIMITS.lock().unwrap_or_else(|e| e.into_inner()).clone().unwrap_or(Value::Null)}// V3 gives { status, title, errors: { field: text } }; V2 a list of { status, message }.fn error_text(status: u16, text: &str) -> String {    let v: Value = serde_json::from_str(text).unwrap_or(Value::Null);    if let Some(list) = v.as_array() {        let parts: Vec<String> = list.iter().filter_map(|e| e["message"].as_str().map(|m| {            let d = e["details"].as_object().map_or(String::new(), |o| o.iter().map(|(k, x)| format!(" ({}: {})", k, x.as_str().map(str::to_string).unwrap_or_else(|| x.to_string()))).collect());            format!("{}{}", m, d)        })).collect();        if !parts.is_empty() {            return format!("BigCommerce {}: {}", status, parts.join("; "));        }    }    if let Some(t) = v["title"].as_str() {        let fields = v["errors"].as_object().map_or(String::new(), |o| {            let l: Vec<String> = o.iter().map(|(k, x)| format!("{}: {}", k, x.as_str().unwrap_or(""))).collect();            if l.is_empty() { String::new() } else { format!(" ({})", l.join("; ")) }        });        return format!("BigCommerce {}: {}{}", status, t, fields);    }    let hint = if status == 401 { " (check BIGCOMMERCE_ACCESS_TOKEN)" } else if status == 403 { " (the token lacks this scope)" } else { "" };    format!("BigCommerce {}: {}{}", status, text.trim().chars().take(300).collect::<String>(), hint)}fn call(method: &str, path: &str, body: Option<&Value>) -> Result<Value, String> {    let token = setting("BIGCOMMERCE_ACCESS_TOKEN").ok_or("BIGCOMMERCE_ACCESS_TOKEN is not set")?;    let url = format!("{}{}", root()?, path);    let agent = ureq::AgentBuilder::new().timeout(Duration::from_secs(60)).build();    for attempt in 0..5 {        let req = agent.request(method, &url).set("X-Auth-Token", &token).set("Accept", "application/json");        let res = match body {            Some(b) => req.set("Content-Type", "application/json").send_string(&b.to_string()),            None => req.call(),        };        match res {            Ok(r) => {                note(&r);                let code = r.status();                let t = r.into_string().map_err(|e| e.to_string())?;                // A V2 list with nothing in it is a 204 with no body.                if code == 204 || t.trim().is_empty() {                    return Ok(Value::Null);                }                return serde_json::from_str(&t).map_err(|_| format!("BigCommerce's answer is not JSON: {}", t.chars().take(200).collect::<String>()));            }            // The window's end is in the header: a 429 costs nothing, so it is safe for any method.            Err(ureq::Error::Status(429, r)) if attempt < 4 => {                note(&r);                let wait = r.header("X-Rate-Limit-Time-Reset-Ms").and_then(|s| s.parse::<u64>().ok()).unwrap_or(1000);                std::thread::sleep(Duration::from_millis(wait.min(30_000) + 50));            }            Err(ureq::Error::Status(c, r)) => {                note(&r);                return Err(error_text(c, &r.into_string().unwrap_or_default()));            }            Err(e) => return Err(format!("cannot reach BigCommerce at {}: {}", url, e)),        }    }    Err("BigCommerce stayed over its limit through five tries".into())}/// Any other call: `path` under the store, with its version, like "/v3/customers", `body` or null. Gives the answer as it came.pub fn bigcommerce_call(method: &str, path: &str, body: serde_json::Value) -> Result<serde_json::Value, String> {    call(&method.to_uppercase(), path, if body.is_null() { None } else { Some(&body) })}// V3: { data, meta: { pagination: { current_page, total_pages } } }.fn walk_v3(path: &str, filter: &Value, max_items: i64) -> Result<Vec<Value>, String> {    let q = query(filter);    let mut out = vec![];    let mut page = 1;    loop {        let v = call("GET", &format!("{}?limit={}&page={}{}", path, PAGE, page, q), None)?;        out.extend(v["data"].as_array().cloned().unwrap_or_default());        if max_items > 0 && out.len() as i64 >= max_items {            out.truncate(max_items as usize);            return Ok(out);        }        if page >= v["meta"]["pagination"]["total_pages"].as_i64().unwrap_or(1) {            return Ok(out);        }        page += 1;    }}// V2: a bare list, and a 204 past the end.fn walk_v2(path: &str, filter: &Value, max_items: i64) -> Result<Vec<Value>, String> {    let q = query(filter);    let mut out = vec![];    let mut page = 1;    loop {        let v = call("GET", &format!("{}?limit={}&page={}{}", path, PAGE, page, q), None)?;        let items = v.as_array().cloned().unwrap_or_default();        let n = items.len() as i64;        out.extend(items);        if max_items > 0 && out.len() as i64 >= max_items {            out.truncate(max_items as usize);            return Ok(out);        }        if n < PAGE {            return Ok(out);        }        page += 1;    }}/// Products through every page (V3): `filter` is BigCommerce's (`include` as "variants,images", `sku`, `name:like`, `is_visible`...), `max_items` caps them (0: all).pub fn bigcommerce_products(filter: serde_json::Value, max_items: i64) -> Result<Vec<serde_json::Value>, String> {    walk_v3("/v3/catalog/products", &filter, max_items)}/// One product by id; `include` is "variants,images" or "".pub fn bigcommerce_product(id: i64, include: &str) -> Result<serde_json::Value, String> {    let q = if include.is_empty() { String::new() } else { format!("?include={}", enc(include)) };    Ok(call("GET", &format!("/v3/catalog/products/{}{}", id, q), None)?["data"].clone())}/// Makes a product: `name`, `type` ("physical" or "digital"), `weight` and `price` are required.pub fn bigcommerce_product_create(fields: serde_json::Value) -> Result<serde_json::Value, String> {    Ok(call("POST", "/v3/catalog/products", Some(&fields))?["data"].clone())}/// Changes a product.pub fn bigcommerce_product_update(id: i64, fields: serde_json::Value) -> Result<serde_json::Value, String> {    Ok(call("PUT", &format!("/v3/catalog/products/{}", id), Some(&fields))?["data"].clone())}/// Deletes a product.pub fn bigcommerce_product_delete(id: i64) -> Result<bool, String> {    call("DELETE", &format!("/v3/catalog/products/{}", id), None)?;    Ok(true)}/// Changes many products, 10 at a time as the API wants: each record has its `id`. Gives { updated, errors }.pub fn bigcommerce_products_update(records: Vec<serde_json::Value>) -> Result<serde_json::Value, String> {    if records.iter().any(|r| !r["id"].is_i64()) {        return Err("each record needs its `id`".into());    }    let mut updated = 0;    let mut errors = vec![];    for chunk in records.chunks(10) {        let v = call("PUT", "/v3/catalog/products", Some(&Value::Array(chunk.to_vec())))?;        updated += v["data"].as_array().map_or(0, |d| d.len());        // A 207: some went in, and the rest are here.        errors.extend(v["errors"].as_array().cloned().unwrap_or_default());    }    Ok(json!({ "updated": updated, "errors": errors }))}/// A product's variants through every page.pub fn bigcommerce_variants(product_id: i64) -> Result<Vec<serde_json::Value>, String> {    walk_v3(&format!("/v3/catalog/products/{}/variants", product_id), &Value::Null, 0)}/// Categories through every page; `filter` as BigCommerce's, `max_items` caps them (0: all).pub fn bigcommerce_categories(filter: serde_json::Value, max_items: i64) -> Result<Vec<serde_json::Value>, String> {    walk_v3("/v3/catalog/categories", &filter, max_items)}/// Orders through every page (V2): `filter` is BigCommerce's (`status_id`, `min_date_created`, `customer_id`, `sort`...), `max_items` caps them (0: all). No orders is an empty list, though the API says 204.pub fn bigcommerce_orders(filter: serde_json::Value, max_items: i64) -> Result<Vec<serde_json::Value>, String> {    walk_v2("/v2/orders", &filter, max_items)}/// One order by id (V2).pub fn bigcommerce_order(id: i64) -> Result<serde_json::Value, String> {    call("GET", &format!("/v2/orders/{}", id), None)}/// The products of an order (V2), through every page.pub fn bigcommerce_order_products(id: i64) -> Result<Vec<serde_json::Value>, String> {    walk_v2(&format!("/v2/orders/{}/products", id), &Value::Null, 0)}/// Makes an order (V2): `products` and `billing_address` are required.pub fn bigcommerce_order_create(fields: serde_json::Value) -> Result<serde_json::Value, String> {    call("POST", "/v2/orders", Some(&fields))}/// Changes an order (V2), as `{ status_id: 10 }`.pub fn bigcommerce_order_update(id: i64, fields: serde_json::Value) -> Result<serde_json::Value, String> {    call("PUT", &format!("/v2/orders/{}", id), Some(&fields))}fn now() -> i64 {    SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs() as i64)}/// Checks the older `signed_payload` an app's load callback gets (`base64 of JSON`.`base64 of the hex HMAC`), with the app's client secret. Gives the payload { user, owner, context, store_hash, timestamp }.pub fn bigcommerce_signed_payload(signed_payload: &str, client_secret: &str) -> Result<serde_json::Value, String> {    let (data, sig) = signed_payload.split_once('.').ok_or("a signed payload has two parts, joined by a dot")?;    let json_bytes = STANDARD.decode(data).map_err(|_| "the payload's first part is not base64".to_string())?;    let given = STANDARD.decode(sig).map_err(|_| "the payload's signature is not base64".to_string())?;    let raw = hex::decode(&given).map_err(|_| "the payload's signature is not a hex digest".to_string())?;    let mut m = Hmac::<sha2::Sha256>::new_from_slice(client_secret.as_bytes()).expect("HMAC takes any key");    m.update(&json_bytes);    if m.verify_slice(&raw).is_err() {        return Err("the signed payload does not match: a wrong client secret, or it was changed".into());    }    serde_json::from_slice(&json_bytes).map_err(|e| format!("the payload is not JSON: {}", e))}/// Checks a `signed_payload_jwt` (HS256 with the client secret), with its `aud`, `iss`, `exp` and `nbf`. Gives the claims, `store_hash` taken out of `sub`. `at` is the time to check against (0: now).pub fn bigcommerce_signed_jwt(token: &str, client_id: &str, client_secret: &str, at: i64) -> Result<serde_json::Value, String> {    let parts: Vec<&str> = token.split('.').collect();    if parts.len() != 3 {        return Err("a JWT has three parts".into());    }    let head: Value = URL_SAFE_NO_PAD.decode(parts[0]).ok().and_then(|b| serde_json::from_slice(&b).ok()).ok_or("the JWT's header is not readable")?;    if head["alg"] != "HS256" {        return Err(format!("the JWT's algorithm is {}, not HS256", head["alg"]));    }    let sig = URL_SAFE_NO_PAD.decode(parts[2]).map_err(|_| "the JWT's signature is not base64url".to_string())?;    let mut m = Hmac::<sha2::Sha256>::new_from_slice(client_secret.as_bytes()).expect("HMAC takes any key");    m.update(format!("{}.{}", parts[0], parts[1]).as_bytes());    if m.verify_slice(&sig).is_err() {        return Err("the JWT's signature does not match: a wrong client secret, or it was changed".into());    }    let mut c: Value = URL_SAFE_NO_PAD.decode(parts[1]).ok().and_then(|b| serde_json::from_slice(&b).ok()).ok_or("the JWT's claims are not readable")?;    let t = if at > 0 { at } else { now() };    if c["aud"].as_str() != Some(client_id) {        return Err(format!("the JWT is for {}, not this app", c["aud"]));    }    if c["iss"] != "bc" {        return Err(format!("the JWT's issuer is {}, not bc", c["iss"]));    }    if c["exp"].as_i64().map_or(true, |e| e < t) {        return Err("the JWT has expired".into());    }    if c["nbf"].as_i64().map_or(false, |n| n > t + 30) {        return Err("the JWT is not valid yet".into());    }    let hash = c["sub"].as_str().and_then(|s| s.strip_prefix("stores/")).map(str::to_string);    c["store_hash"] = json!(hash);    Ok(c)}
Apple push notifications apns.qwer · 1.0.0Send push notifications to iPhones through APNs over HTTP/2: alert and background payloads built and sized, an ES256 provider token made and renewed, collapse ids and push types set, many devices on one kept connection, and Apple's reasons such as BadDeviceToken and Unregistered read back so dead tokens can be dropped.pushmobilehttp2jwt4 views · 6 downloadsGitHub API github_api.qwer · 1.0.0Work with GitHub from a program: issues, comments, pull requests and check runs, every list followed through its Link header, primary and secondary rate limits waited out or named, webhook signatures checked, and a GitHub App's RS256 JWT and installation tokens made and kept.gitwebhooksjwtpagination2 views · 7 downloadsReddit reddit.qwer · 1.0.0Reddit's API without an SDK: OAuth by a script app's password or an app-only token, the User-Agent form Reddit insists on checked first, listings walked with `after`, comment trees flattened with their depth, posts and comments made with errors dug out of their 200 answers, and the rate-limit headers obeyed before Reddit has to refuse.socialoauth2listingsrate-limits2 views · 6 downloadsSalesforce salesforce.qwer · 1.0.0Salesforce's REST API without an SDK: sign-in by client credentials or a JWT bearer, renewed when a session dies, SOQL with values quoted and nextRecordsUrl followed, sObjects made, read, changed, upserted by external id and deleted, composite and collection calls with each item's result, Salesforce's lists of errors read, and the day's API use reported.crmsoqloauth2jwt3 views · 7 downloadsShopify shopify.qwer · 1.0.0Run a store through Shopify's Admin GraphQL API: queries paced by the cost and bucket it reports and THROTTLED answers waited out, user errors in a 200 turned into errors, lists walked by cursor, global ids made and read, and webhook and OAuth HMACs checked.ecommercegraphqlwebhooksoauth3 views · 7 downloadsSpotify spotify.qwer · 1.0.0Spotify's Web API without an SDK: client-credentials tokens kept until they run out, PKCE sign-in with its verifier, challenge, address and code exchange, search, tracks asked for in fifties, playlists read through `next`, made and added to in hundreds, 429s waited out by their Retry-After, and ids taken from any link or URI.musicoauth2pkcerate-limits2 views · 6 downloads
Thread
37Useful? Like it, or add to its thread below.

Thread 0 posts

On BigCommerce: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.