Square
Square's API without an SDK: card payments taken at once or held and completed later, refunds, customers and catalog items, lists followed through their cursors, an idempotency key on every write that a retry reuses, money in each currency's smallest unit, and webhook signatures checked over the notification URL and body.
Version 1.0.0 · 20 functions · 5 crates
Functions
What use square.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use square_new_key, square_money from square (Quillo 0.6).
square_new_key() → strA new idempotency key, a UUID in form: send the same key to make a retry of your own safe.
square_money(amount: str, currency: str) → anyor an errorMoney as Square wants it: square_money("19.99", "USD") is { amount: 1999, currency: "USD" }, in the currency's smallest unit (yen has none, dinars three). Takes text so no float rounding creeps in; an error for more decimals than the currency has.
square_pay(source_id: str, money: any, params: any) → anyor an errorTakes a payment from source_id (a nonce from the Web Payments SDK, or a card on file) for money (from square_money). params adds what Square documents: autocomplete (false only authorizes), location_id, customer_id, reference_id, note, idempotency_key (one is made if not given). Gives the payment; a decline is an error naming its code.
square_payment(id: str) → anyor an errorOne payment by id.
square_payment_complete(id: str) → anyor an errorCompletes a payment taken with autocomplete: false.
square_payment_cancel(id: str) → anyor an errorCancels a payment taken with autocomplete: false, before it is completed.
square_payments(filter: any, max_items: int) → listor an errorPayments through every page: filter is Square's (begin_time, end_time, location_id, sort_order, limit); max_items caps them (0: all).
square_refund(payment_id: str, money: any, reason: str, idempotency_key: str) → anyor an errorRefunds money of a payment; reason shows on the receipt and idempotency_key may be "" to make one. Gives the refund, usually PENDING at first.
square_refund_get(id: str) → anyor an errorOne refund by id, to see it leave PENDING.
square_customer_create(fields: any) → anyor an errorMakes a customer from Square's fields (given_name, family_name, email_address, phone_number, reference_id...), with idempotency_key when you want one kept.
square_customer(id: str) → anyor an errorOne customer by id.
square_customer_update(id: str, fields: any) → anyor an errorChanges a customer's fields; give version to be refused when someone changed it first.
square_customers(max_items: int) → listor an errorCustomers through every page; max_items caps them (0: all).
square_customer_by_email(email: str) → listor an errorCustomers with exactly this email address.
square_catalog_upsert(object: any) → anyor an errorPuts any catalog object (object with a type and, for a new one, an id starting with #). Gives { object, id_mappings }, the ids Square gave in place of yours.
square_catalog_item(name: str, variations: list) → anyor an errorMakes an ITEM with its variations: variations is a list of { name, price, currency } (price as text; Square needs at least one).
square_catalog_list(types: str, max_items: int) → listor an errorCatalog objects of the types given ("ITEM,CATEGORY", "" for all) through every page; max_items caps them (0: all).
square_catalog_delete(id: str) → anyor an errorDeletes a catalog object, and its children. Gives the ids deleted.
square_webhook(notification_url: str, body: str, signature: str, signature_key: str) → anyor an errorChecks a webhook: notification_url exactly as subscribed, body the raw body, signature the x-square-hmacsha256-signature header, signature_key the subscription's. Gives the event, or an error saying why it is refused.
square_webhook_sign(notification_url: str, body: str, signature_key: str) → strSigns a webhook body as Square does, for tests.
Get it
$ quillo install square # square.qwer into this folder$ quillo install square --example # and its example, square.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/square.qwer$ curl -O https://quillolang.org/plugins/files/square.qweThen, in your program, all of it or only what you call:
use square.qweruse square_new_key, square_money from square // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
ureq = "2"hmac = "0.12"sha2 = "0.10"base64 = "0.22"serde_json = { version = "1", features = ["preserve_order"] }Example
// Taking money with Square: a card payment, one authorized and captured later, a refund, a// customer found by email, an item in the catalog, lists past one page, and a webhook checked.//// SQUARE_ACCESS_TOKEN=... SQUARE_API_URL=https://connect.squareupsandbox.com quillo run square.qwe// then open /pay, /later, /refund, /customers, /catalog, /lists, /hook-check and /failures//// In the sandbox, `cnon:card-nonce-ok` is a good card and `cnon:card-nonce-declined` a declined// one. Point a webhook subscription at POST /square/webhook, and set SQUARE_WEBHOOK_KEY and// SQUARE_WEBHOOK_URL to its signature key and exact URL.use "square.qwer"route GET /pay { let paid = square_pay("cnon:card-nonce-ok", square_money("19.99", "USD")?, { reference_id: "order-1001", note: "Blue mug" })? let read = square_payment(paid.id)? { state: read.status, amount: read.amount_money.amount, reference: read.reference_id }}// The same key sent twice is one payment; a changed amount with that key is refused.route GET /again { let key = square_new_key() let one = square_pay("cnon:card-nonce-ok", square_money("5.00", "USD")?, { idempotency_key: key })? let two = square_pay("cnon:card-nonce-ok", square_money("5.00", "USD")?, { idempotency_key: key })? { same: one.id == two.id, changed: square_pay("cnon:card-nonce-ok", square_money("6.00", "USD")?, { idempotency_key: key }).error, long: square_pay("cnon:card-nonce-ok", square_money("6.00", "USD")?, { idempotency_key: repeat("k", 46) }).error, }}route GET /later { let held = square_pay("cnon:card-nonce-ok", square_money("40", "USD")?, { autocomplete: false })? let done = square_payment_complete(held.id)? let other = square_pay("cnon:card-nonce-ok", square_money("7", "USD")?, { autocomplete: false })? { held: held.status, done: done.status, cancelled: square_payment_cancel(other.id)?.status, twice: square_payment_complete(other.id).error }}route GET /refund { let paid = square_pay("cnon:card-nonce-ok", square_money("30", "USD")?, {})? let back = square_refund(paid.id, square_money("10", "USD")?, "Chipped mug", "")? { state: back.status, settled: square_refund_get(back.id)?.status, too_much: square_refund(paid.id, square_money("99", "USD")?, "", "").error, }}route GET /customers { let ada = square_customer_create({ given_name: "Ada", family_name: "Lovelace", email_address: "ada@example.com" })? square_customer_update(ada.id, { note: "Prefers mail", version: ada.version })? { name: square_customer(ada.id)?.given_name, note: square_customer(ada.id)?.note, found: map(square_customer_by_email("ada@example.com")?, (c) => c.family_name), stale: square_customer_update(ada.id, { note: "x", version: 0 }).error, bad: square_customer_create({ email_address: "not an address" }).error, }}route GET /catalog { let made = square_catalog_item("Blue mug", [{ name: "Small", price: "12.50", currency: "USD" }, { name: "Large", price: "1500", currency: "JPY" }])? let item = made.object { name: item.item_data.name, prices: map(item.item_data.variations, (v) => v.item_variation_data.price_money.amount), temp_ids: map(made.id_mappings, (m) => m.client_object_id), deleted: len(square_catalog_delete(item.id)?), none: square_catalog_item("x", []).error, }}route GET /lists = { payments: len(square_payments({ location_id: "L1" }, 0)?), capped: len(square_payments({ location_id: "L1" }, 130)?), customers: len(square_customers(0)?), items: len(square_catalog_list("ITEM", 0)?), categories: len(square_catalog_list("CATEGORY", 0)?),}route GET /money = { dollars: square_money("19.99", "usd")?, yen: square_money("1500", "JPY")?, dinars: square_money("1.250", "KWD")?, too_fine: square_money("10.5", "JPY").error, not_money: square_money("ten", "USD").error, code: square_money("1", "US").error,}route POST /square/webhook { let event = square_webhook(env("SQUARE_WEBHOOK_URL", ""), request.body, request.headers["x-square-hmacsha256-signature"] ?? "", env("SQUARE_WEBHOOK_KEY", "")) if is_err(event) { reply 403 event.error } { got: event.type }}// A signature made outside this plugin, by node's crypto.route GET /hook-check { let url = "https://example.com/square/webhook" let body = "{\"merchant_id\":\"6SSW7HV8K2ST5\",\"type\":\"payment.updated\",\"event_id\":\"e1\"}" let sig = "wIOiyGj8OCbrrClH8Rvf4GlTu6SITiDf5SqDHCIAbYk=" { good: square_webhook(url, body, sig, "whk-test-key")?.type, tampered: square_webhook(url, body + " ", sig, "whk-test-key").error, other_url: square_webhook("https://example.com/other", body, sig, "whk-test-key").error, unsigned: square_webhook(url, body, "", "whk-test-key").error, }}route GET /failures = { declined: square_pay("cnon:card-nonce-declined", square_money("1", "USD")?, {}).error, missing: square_payment("nope").error, waited: square_pay("cnon:rate-limit-once", square_money("2", "USD")?, {})?.status,}$ quillo run square.qwe$ curl 'localhost:8080/pay'$ curl 'localhost:8080/again'$ curl 'localhost:8080/later'$ curl 'localhost:8080/refund'$ curl 'localhost:8080/customers'$ curl 'localhost:8080/catalog'$ curl 'localhost:8080/lists'$ curl 'localhost:8080/money'$ curl 'localhost:8080/hook-check'$ 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
//! Square's API without an SDK: payments taken, completed and cancelled, refunds, customers and catalog items, every list followed through its cursors, an idempotency key on each write that a retry reuses, money in each currency's smallest unit, Square's error lists read, and webhook signatures checked over the notification URL and body.//! version: 1.0.0//!//! [dependencies]//! ureq = "2"//! hmac = "0.12"//! sha2 = "0.10"//! base64 = "0.22"//! serde_json = { version = "1", features = ["preserve_order"] }use base64::{engine::general_purpose::STANDARD, Engine};use hmac::{Hmac, Mac};use serde_json::{json, Value};use sha2::{Digest, Sha256};use std::sync::atomic::{AtomicU64, Ordering};use std::time::{Duration, SystemTime, UNIX_EPOCH};// SQUARE_ACCESS_TOKEN is the token; SQUARE_API_URL the API (https://connect.squareup.com when not// set; https://connect.squareupsandbox.com for the sandbox); SQUARE_VERSION an API version date.const ZERO_DECIMAL: &[&str] = &["BIF", "CLP", "DJF", "GNF", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "UGX", "VND", "VUV", "XAF", "XOF", "XPF"];const THREE_DECIMAL: &[&str] = &["BHD", "JOD", "KWD", "OMR", "TND"];const VERSION: &str = "2024-10-17";fn setting(k: &str) -> Option<String> { std::env::var(k).ok().filter(|v| !v.is_empty())}fn api() -> String { setting("SQUARE_API_URL").unwrap_or_else(|| "https://connect.squareup.com".into()).trim_end_matches('/').to_string()}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()}// Square gives a list: each with a category, a code, and usually a detail and a field.fn errors_text(status: u16, v: &Value, raw: &str) -> String { let list = v["errors"].as_array().cloned().unwrap_or_default(); if list.is_empty() { return format!("Square {}: {}", status, raw.trim().chars().take(300).collect::<String>()); } let parts: Vec<String> = list .iter() .map(|e| { let mut t = format!("{}: {}", e["code"].as_str().unwrap_or("ERROR"), e["detail"].as_str().unwrap_or(e["category"].as_str().unwrap_or(""))); if let Some(f) = e["field"].as_str() { t.push_str(&format!(" (field {})", f)); } t }) .collect(); let hint = match status { 401 => " (check SQUARE_ACCESS_TOKEN, and that it belongs to this environment)", 403 => " (the token lacks the permission this call needs)", _ => "", }; format!("Square {}: {}{}", status, parts.join("; "), hint)}// The body keeps its idempotency key through the retries, so a write sent twice happens once.fn call(method: &str, path: &str, body: Option<&Value>) -> Result<Value, String> { let token = setting("SQUARE_ACCESS_TOKEN").ok_or("SQUARE_ACCESS_TOKEN is not set")?; let url = format!("{}{}", api(), path); let version = setting("SQUARE_VERSION").unwrap_or_else(|| VERSION.into()); let agent = ureq::AgentBuilder::new().timeout(Duration::from_secs(30)).build(); for attempt in 0..5u32 { let req = agent.request(method, &url).set("Authorization", &format!("Bearer {}", token)).set("Square-Version", &version).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) => { let t = r.into_string().map_err(|e| e.to_string())?; return if t.trim().is_empty() { Ok(json!({})) } else { serde_json::from_str(&t).map_err(|_| format!("Square's answer is not JSON: {}", t.chars().take(200).collect::<String>())) }; } Err(ureq::Error::Status(429 | 500 | 502 | 503 | 504, r)) if attempt < 4 => { let wait = r.header("Retry-After").and_then(|s| s.parse::<u64>().ok()).map_or(250u64 << attempt, |s| s * 1000); std::thread::sleep(Duration::from_millis(wait.min(30_000))); } Err(ureq::Error::Status(c, r)) => { let t = r.into_string().unwrap_or_default(); return Err(errors_text(c, &serde_json::from_str(&t).unwrap_or(Value::Null), &t)); } Err(e) => return Err(format!("cannot reach Square at {}: {}", api(), e)), } } Err("Square stayed busy through five tries".into())}static COUNTER: AtomicU64 = AtomicU64::new(0);/// A new idempotency key, a UUID in form: send the same key to make a retry of your own safe.pub fn square_new_key() -> String { let n = COUNTER.fetch_add(1, Ordering::Relaxed); let t = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_nanos()); let seed = std::collections::hash_map::RandomState::new(); let d = Sha256::digest(format!("{}:{}:{}:{:?}", t, n, std::process::id(), seed)); let mut b: Vec<u8> = d[..16].to_vec(); b[6] = (b[6] & 0x0f) | 0x40; b[8] = (b[8] & 0x3f) | 0x80; let h: String = b.iter().map(|x| format!("{:02x}", x)).collect(); format!("{}-{}-{}-{}-{}", &h[..8], &h[8..12], &h[12..16], &h[16..20], &h[20..])}fn with_key(mut v: Value, key_of: &str) -> Result<Value, String> { let k = if key_of.is_empty() { v["idempotency_key"].as_str().map(str::to_string).unwrap_or_else(square_new_key) } else { key_of.to_string() }; if k.len() > 45 { return Err(format!("an idempotency key is at most 45 characters, not {}", k.len())); } v["idempotency_key"] = json!(k); Ok(v)}/// Money as Square wants it: `square_money("19.99", "USD")` is `{ amount: 1999, currency: "USD" }`, in the currency's smallest unit (yen has none, dinars three). Takes text so no float rounding creeps in; an error for more decimals than the currency has.pub fn square_money(amount: &str, currency: &str) -> Result<serde_json::Value, String> { let c = currency.to_uppercase(); if c.len() != 3 || !c.bytes().all(|b| b.is_ascii_uppercase()) { return Err(format!("`{}` is not a three-letter currency code", currency)); } let places = if ZERO_DECIMAL.contains(&c.as_str()) { 0 } else if THREE_DECIMAL.contains(&c.as_str()) { 3 } else { 2 }; let a = amount.trim(); let (whole, frac) = a.split_once('.').unwrap_or((a, "")); if whole.is_empty() || !whole.bytes().all(|b| b.is_ascii_digit()) || !frac.bytes().all(|b| b.is_ascii_digit()) { return Err(format!("`{}` is not an amount", amount)); } let frac = frac.trim_end_matches('0'); if frac.len() > places { return Err(format!("{} has {} decimals; {} has {}", amount, frac.len(), c, places)); } let n: i64 = format!("{}{}{}", whole, frac, "0".repeat(places - frac.len())).parse().map_err(|_| format!("`{}` is too large", amount))?; Ok(json!({ "amount": n, "currency": c }))}/// Takes a payment from `source_id` (a nonce from the Web Payments SDK, or a card on file) for `money` (from square_money). `params` adds what Square documents: `autocomplete` (false only authorizes), `location_id`, `customer_id`, `reference_id`, `note`, `idempotency_key` (one is made if not given). Gives the payment; a decline is an error naming its code.pub fn square_pay(source_id: &str, money: serde_json::Value, params: serde_json::Value) -> Result<serde_json::Value, String> { if source_id.is_empty() { return Err("a payment needs a source_id".into()); } if !money["amount"].is_i64() || !money["currency"].is_string() { return Err("money must be { amount, currency }: see square_money".into()); } let mut b = if params.is_object() { params } else { json!({}) }; b["source_id"] = json!(source_id); b["amount_money"] = money; let b = with_key(b, "")?; Ok(call("POST", "/v2/payments", Some(&b))?["payment"].clone())}/// One payment by id.pub fn square_payment(id: &str) -> Result<serde_json::Value, String> { Ok(call("GET", &format!("/v2/payments/{}", enc(id)), None)?["payment"].clone())}/// Completes a payment taken with `autocomplete: false`.pub fn square_payment_complete(id: &str) -> Result<serde_json::Value, String> { Ok(call("POST", &format!("/v2/payments/{}/complete", enc(id)), Some(&json!({})))?["payment"].clone())}/// Cancels a payment taken with `autocomplete: false`, before it is completed.pub fn square_payment_cancel(id: &str) -> Result<serde_json::Value, String> { Ok(call("POST", &format!("/v2/payments/{}/cancel", enc(id)), Some(&json!({})))?["payment"].clone())}fn query_of(filter: &Value) -> String { let mut q = vec![]; if let Some(m) = filter.as_object() { for (k, v) in m { let t = v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string()); if !v.is_null() { q.push(format!("{}={}", enc(k), enc(&t))); } } } q.join("&")}// GET lists: `cursor` in the query, `cursor` in the answer until the last page.fn walk_get(path: &str, filter: &Value, field: &str, max_items: i64) -> Result<Vec<Value>, String> { let mut out = vec![]; let mut cursor: Option<String> = None; let base_q = query_of(filter); loop { let mut q = base_q.clone(); if let Some(c) = &cursor { if !q.is_empty() { q.push('&'); } q.push_str(&format!("cursor={}", enc(c))); } let url = if q.is_empty() { path.to_string() } else { format!("{}?{}", path, q) }; let v = call("GET", &url, None)?; out.extend(v[field].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); } match v["cursor"].as_str() { Some(c) if !c.is_empty() => cursor = Some(c.to_string()), _ => return Ok(out), } }}/// Payments through every page: `filter` is Square's (`begin_time`, `end_time`, `location_id`, `sort_order`, `limit`); `max_items` caps them (0: all).pub fn square_payments(filter: serde_json::Value, max_items: i64) -> Result<Vec<serde_json::Value>, String> { walk_get("/v2/payments", &filter, "payments", max_items)}/// Refunds `money` of a payment; `reason` shows on the receipt and `idempotency_key` may be "" to make one. Gives the refund, usually PENDING at first.pub fn square_refund(payment_id: &str, money: serde_json::Value, reason: &str, idempotency_key: &str) -> Result<serde_json::Value, String> { let mut b = json!({ "payment_id": payment_id, "amount_money": money }); if !reason.is_empty() { b["reason"] = json!(reason); } let b = with_key(b, idempotency_key)?; Ok(call("POST", "/v2/refunds", Some(&b))?["refund"].clone())}/// One refund by id, to see it leave PENDING.pub fn square_refund_get(id: &str) -> Result<serde_json::Value, String> { Ok(call("GET", &format!("/v2/refunds/{}", enc(id)), None)?["refund"].clone())}/// Makes a customer from Square's fields (`given_name`, `family_name`, `email_address`, `phone_number`, `reference_id`...), with `idempotency_key` when you want one kept.pub fn square_customer_create(fields: serde_json::Value) -> Result<serde_json::Value, String> { let b = with_key(if fields.is_object() { fields } else { json!({}) }, "")?; Ok(call("POST", "/v2/customers", Some(&b))?["customer"].clone())}/// One customer by id.pub fn square_customer(id: &str) -> Result<serde_json::Value, String> { Ok(call("GET", &format!("/v2/customers/{}", enc(id)), None)?["customer"].clone())}/// Changes a customer's fields; give `version` to be refused when someone changed it first.pub fn square_customer_update(id: &str, fields: serde_json::Value) -> Result<serde_json::Value, String> { Ok(call("PUT", &format!("/v2/customers/{}", enc(id)), Some(&fields))?["customer"].clone())}/// Customers through every page; `max_items` caps them (0: all).pub fn square_customers(max_items: i64) -> Result<Vec<serde_json::Value>, String> { walk_get("/v2/customers", &json!({ "limit": 100 }), "customers", max_items)}/// Customers with exactly this email address.pub fn square_customer_by_email(email: &str) -> Result<Vec<serde_json::Value>, String> { let mut out = vec![]; let mut body = json!({ "limit": 100, "query": { "filter": { "email_address": { "exact": email } } } }); loop { let v = call("POST", "/v2/customers/search", Some(&body))?; out.extend(v["customers"].as_array().cloned().unwrap_or_default()); match v["cursor"].as_str() { Some(c) if !c.is_empty() => body["cursor"] = json!(c), _ => return Ok(out), } }}/// Puts any catalog object (`object` with a `type` and, for a new one, an `id` starting with `#`). Gives { object, id_mappings }, the ids Square gave in place of yours.pub fn square_catalog_upsert(object: serde_json::Value) -> Result<serde_json::Value, String> { if !object["type"].is_string() || !object["id"].is_string() { return Err("a catalog object needs a `type` and an `id` (a new one's starts with #)".into()); } let b = with_key(json!({ "object": object }), "")?; let v = call("POST", "/v2/catalog/object", Some(&b))?; Ok(json!({ "object": v["catalog_object"], "id_mappings": v["id_mappings"] }))}/// Makes an ITEM with its variations: `variations` is a list of { name, price, currency } (price as text; Square needs at least one).pub fn square_catalog_item(name: &str, variations: Vec<serde_json::Value>) -> Result<serde_json::Value, String> { if variations.is_empty() { return Err("an item needs at least one variation".into()); } let mut vars = vec![]; for (i, x) in variations.iter().enumerate() { let vn = x["name"].as_str().ok_or("each variation needs a `name`")?; let price = x["price"].as_str().ok_or("each variation needs a `price`, as text")?; vars.push(json!({ "type": "ITEM_VARIATION", "id": format!("#var{}", i), "item_variation_data": { "item_id": "#item", "name": vn, "pricing_type": "FIXED_PRICING", "price_money": square_money(price, x["currency"].as_str().unwrap_or("USD"))? }, })); } square_catalog_upsert(json!({ "type": "ITEM", "id": "#item", "item_data": { "name": name, "variations": vars } }))}/// Catalog objects of the `types` given ("ITEM,CATEGORY", "" for all) through every page; `max_items` caps them (0: all).pub fn square_catalog_list(types: &str, max_items: i64) -> Result<Vec<serde_json::Value>, String> { let filter = if types.is_empty() { json!({}) } else { json!({ "types": types }) }; walk_get("/v2/catalog/list", &filter, "objects", max_items)}/// Deletes a catalog object, and its children. Gives the ids deleted.pub fn square_catalog_delete(id: &str) -> Result<serde_json::Value, String> { Ok(call("DELETE", &format!("/v2/catalog/object/{}", enc(id)), None)?["deleted_object_ids"].clone())}fn sign(notification_url: &str, body: &str, signature_key: &str) -> String { let mut m = Hmac::<Sha256>::new_from_slice(signature_key.as_bytes()).expect("HMAC takes any key"); m.update(notification_url.as_bytes()); m.update(body.as_bytes()); STANDARD.encode(m.finalize().into_bytes())}/// Checks a webhook: `notification_url` exactly as subscribed, `body` the raw body, `signature` the `x-square-hmacsha256-signature` header, `signature_key` the subscription's. Gives the event, or an error saying why it is refused.pub fn square_webhook(notification_url: &str, body: &str, signature: &str, signature_key: &str) -> Result<serde_json::Value, String> { if signature.is_empty() { return Err("no x-square-hmacsha256-signature header: this did not come from Square".into()); } let want = sign(notification_url, body, signature_key); if want.len() != signature.len() || want.bytes().zip(signature.bytes()).fold(0u8, |a, (x, y)| a | (x ^ y)) != 0 { return Err("the webhook's signature does not match: a wrong signature key, a notification URL not exactly as subscribed, or a changed body".into()); } serde_json::from_str(body).map_err(|e| format!("the webhook's body is not JSON: {}", e))}/// Signs a webhook body as Square does, for tests.pub fn square_webhook_sign(notification_url: &str, body: &str, signature_key: &str) -> String { sign(notification_url, body, signature_key)}Related
adyen.qwer · 1.0.0Adyen's Checkout API without an SDK: sessions for Drop-in, payments with their result codes turned into paid, failed, pending or waiting on the shopper, 3-D Secure details sent on, captures, refunds, cancels and reversals, amounts by Adyen's own table of minor units, and each webhook notification's HMAC checked.2 views · 7 downloadsMollie mollie.qwer · 1.0.0Mollie's Payments API without an SDK: payments and refunds with amounts in the exact decimals Mollie insists on, customers, mandates and recurring charges, lists followed through their next links, and webhooks taken as Mollie means them: only an id comes in, and the payment is fetched back to learn what happened.4 views · 8 downloadsPaddle Billing paddle.qwer · 1.0.0Paddle Billing's API without an SDK: prices, transactions and subscriptions, lists followed through their next links, subscription changes with the proration Paddle insists on, pauses, resumes and cancels, money as Paddle's strings of the lowest denomination, and the Paddle-Signature header checked with its timestamp and rotating keys.5 views · 7 downloadsPayPal paypal.qwer · 1.0.0PayPal's Orders API without an SDK: an OAuth token reused until it expires and renewed on a 401, orders created and captured, refunds, webhooks checked through PayPal's verify call on their exact bytes, and retries made safe with PayPal-Request-Id, each error given with PayPal's issue and debug id.3 views · 6 downloadsRazorpay razorpay.qwer · 1.0.0Razorpay's API without an SDK: orders in paise, the signature Checkout hands the page checked against your own order, payments captured and refunded, lists walked by count and skip, webhook signatures, and rupees written with lakh and crore grouping.3 views · 7 downloadsWebhooks webhooks.qwer · 1.0.0Check that a webhook came from Stripe, GitHub, Slack, Shopify or a Standard Webhooks sender, with timestamp tolerance; and sign your own.5 views · 9 downloadsThread 0 posts
On Square: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.