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
| Field | What it is |
|---|---|
id | Internal id, given by the server (from 1000000). |
od | Outside id: your own id for the thing (SKU, item number, URL…). Optional, up to 255 characters. |
s | Section, a group for names (dims, price). Optional, up to 100 characters; empty when left out. |
n | Name, up to 255 characters. |
v | Value, always stored and returned as text. Numbers and true/false become text; objects become JSON text. |
createdat | When 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)) ) )
- The time is UTC. A request is good for 60 seconds from it (and up to 10 seconds early, for a fast clock).
GET /v1/timegives the server's time for a client whose clock is off. - The signature covers the method, path, query and body, so it fits exactly one request. It can be used once: sending the same signed request twice is refused.
- An empty body (GET) hashes to
e3b0c442…b855, the SHA-256 of nothing. - Everything travels over HTTPS.
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
| Query | Answer |
|---|---|
?od=SKU-1 | Current values: {"od": "SKU-1", "data": {"dims": {"width": "12"}}}. Add &s=dims for one section. |
?od=SKU-1&history=1 | Every row of that od, oldest first: {"od", "rows": [{id, s, n, v, createdat}], "next"}. |
?id=1000002 | One row: {"row": {id, od, s, n, v, createdat}}. |
?s=dims&after=1000500 | A 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": "…"}.
| Status | Code | Meaning |
|---|---|---|
| 400 | input | The body or query isn't right; the message says what. |
| 401 | format | No or malformed Authorization header. |
| 401 | key | Unknown key, or switched off. |
| 401 | time | Time outside the window; the answer has the server's time. |
| 401 | signature | Wrong secret, or the request isn't the one that was signed. |
| 401 | replay | This signed request was already used. |
| 403 | scope | The key can't insert into or view that section. |
| 403 | origin | The key can't be used from this website. |
| 404 | missing | No such row (that this key can view), or no such page. |
| 413 | size | Body over 1 MB. |
| 429 | limit | The 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")