delstorage API

Insert and view rows of data from places that can't reach a database: browser extensions, Shopify pages, small scripts. There are two operations, insert and view. Nothing can be changed or deleted through the API.

Rows

FieldWhat it is
idInternal id, given by the server (from 1000000).
odOutside id: your own id for the thing (SKU, item number, URL…). Optional, up to 255 characters.
sSection, a group for names (dims, price). Optional, up to 100 characters; empty when left out.
nName, up to 255 characters.
vValue, always stored and returned as text. Numbers and true/false become text; objects become JSON text.
createdatWhen it was stored, UTC, 20260929T150835.

Rows are never updated. The current value of an od + s + n is its newest row. A row whose value equals that current value is not stored again. A value that comes back after a different one is stored again, so the history shows 10 → 12 → 10. Rows without an od are always stored. od, s and n are exact: case and spaces count.

Keys and signing

Each app has a key: a name (chrome-ebay) and a secret. Every request carries one header:

Authorization: DS chrome-ebay.20260929T150835.<signature>

signature = base64url( HMAC-SHA256( secret,
              time + "\n" + METHOD + "\n" + path?query + "\n" + hex(SHA-256(body)) ) )

A key has limits: the sections it may insert into, the sections it may view, the browser sites that may use it, and calls per hour. A key used in public code, such as a Shopify page, can be read by anyone. Give it only what any visitor may do.

The client

For pages, extensions and Node 20+. It signs, and it fixes its clock by itself if the server says it's off.

import { DelStorage } from 'https://api.del-duca.com/delstorage.js';
const ds = new DelStorage({ key: 'chrome-ebay', secret: '…' });

await ds.in({ od: 'SKU-1', s: 'dims', data: { width: '12', height: '3' } });
await ds.in([{ od: 'SKU-1', s: 'price', n: 'ebay', v: '19.99' }]);

const { data } = await ds.out({ od: 'SKU-1' });   // { dims: { width: '12', height: '3' }, price: { ebay: '19.99' } }

Without modules: <script src="https://api.del-duca.com/delstorage.global.js"></script> gives window.DelStorage. In an extension, copy the file into the extension instead of loading it.

Insert: POST /v1/in

{"rows": [{"od": "SKU-1", "s": "dims", "n": "width", "v": "12"}, …]}
{"od": "SKU-1", "s": "dims", "data": {"width": "12", "height": "3"}}

A top-level od / s applies to every row that doesn't give its own. Up to 500 rows and 1 MB per call. All rows are stored, or none.

{"rows": [{"id": 1000002, "new": true}, {"id": 1000001, "new": false}], "added": 1, "same": 1}

new: false means the value was already the current one; id is that existing row.

View: GET /v1/out

QueryAnswer
?od=SKU-1Current values: {"od": "SKU-1", "data": {"dims": {"width": "12"}}}. Add &s=dims for one section.
?od=SKU-1&history=1Every row of that od, oldest first: {"od", "rows": [{id, s, n, v, createdat}], "next"}.
?id=1000002One row: {"row": {id, od, s, n, v, createdat}}.
?s=dims&after=1000500A section's rows after an id: {"s", "rows": [{id, od, n, v, createdat}], "next"}.

Lists come 500 at a time (&limit= up to 1000). When next isn't null, ask again with &after=next. Only sections the key may view are ever returned.

Errors

Every error is {"error": "readable message", "code": "…"}.

StatusCodeMeaning
400inputThe body or query isn't right; the message says what.
401formatNo or malformed Authorization header.
401keyUnknown key, or switched off.
401timeTime outside the window; the answer has the server's time.
401signatureWrong secret, or the request isn't the one that was signed.
401replayThis signed request was already used.
403scopeThe key can't insert into or view that section.
403originThe key can't be used from this website.
404missingNo such row (that this key can view), or no such page.
413sizeBody over 1 MB.
429limitThe key has used its calls for this hour.

Without the client

bash

KEY=my-script SECRET='…' BODY='{"od":"SKU-1","s":"dims","data":{"width":"12"}}'
TIME=$(date -u +%Y%m%dT%H%M%S)
HASH=$(printf %s "$BODY" | sha256sum | cut -d' ' -f1)
SIG=$(printf '%s\n%s\n%s\n%s' "$TIME" POST /v1/in "$HASH" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64 | tr '+/' '-_' | tr -d '=')
curl -s https://api.del-duca.com/v1/in -H "Authorization: DS $KEY.$TIME.$SIG" -H 'Content-Type: application/json' -d "$BODY"

Python

import base64, hashlib, hmac, json, time, urllib.request

def call(key, secret, method, path, payload=None):
    body = b"" if payload is None else json.dumps(payload).encode()
    stamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime())
    text = f"{stamp}\n{method}\n{path}\n{hashlib.sha256(body).hexdigest()}"
    sig = base64.urlsafe_b64encode(hmac.new(secret.encode(), text.encode(), hashlib.sha256).digest()).rstrip(b"=").decode()
    req = urllib.request.Request("https://api.del-duca.com" + path, data=body or None, method=method,
                                 headers={"Authorization": f"DS {key}.{stamp}.{sig}", "Content-Type": "application/json"})
    return json.load(urllib.request.urlopen(req))

call("my-script", "…", "GET", "/v1/out?od=SKU-1")