Create a paid order from your own storefront
A small, signed REST API for Merchant and Elite accounts: create prepaid orders on a customer's behalf, read them back, cancel the unclaimed ones, and check your wallet.
Who can call it
API access follows the membership tier of the account that owns the key. Merchant keys are read-only. Elite keys can write. A key on a Buyer account is rejected with tier_forbidden.
Base URL
Every endpoint below is relative to this base, and every request must be HTTPS.
https://elect-i-global.com
01
Authentication
Every request is signed. There is no bearer token: a captured header is useless without the secret, and a captured request is useless after five minutes.
| Header | What it carries |
|---|---|
| X-EIG-Key-Id | The public key identifier issued with the key pair. |
| X-EIG-Timestamp | Unix time in seconds. A request more than 300 seconds from server time is rejected. |
| X-EIG-Nonce | A value you have never sent before on this key. It is stored server-side, so a repeat is rejected. |
| X-EIG-Signature | Lowercase hex HMAC-SHA256 of the canonical string below, keyed on your own secret. |
The canonical string
Five fields joined by a single newline character. The path excludes the query string. The body is the exact bytes you send — for a GET request that is the empty string.
<timestamp>
POST
/api/v1/orders
<nonce>
<raw request body>Sign the body you actually transmit. Re-serialising the JSON after signing changes the bytes and breaks the signature.
Signing a request
This example uses WebCrypto, so it runs unchanged on Node 18+, Deno, Bun and Cloudflare Workers.
const canonical = [timestamp, "POST", path, nonce, body].join("\n");
const key = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"],
);
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(canonical));
const signature = [...new Uint8Array(sig)]
.map((b) => b.toString(16).padStart(2, "0")).join("");The same request with curl
curl -X POST https://elect-i-global.com/api/v1/orders \
-H "X-EIG-Key-Id: $KEY_ID" \
-H "X-EIG-Timestamp: $TS" \
-H "X-EIG-Nonce: $(uuidgen)" \
-H "X-EIG-Signature: $SIG" \
-H "content-type: application/json" \
--data "$BODY"02
Endpoints
Five endpoints. Everything else on the platform is deliberately not exposed.
/api/v1/ordersorders:writeEliteCharges the wallet or credit line named in pay_from, then returns a claim link for the recipient. Catalog lines are repriced on the server from the SKU, so a unit price sent for a line that resolves to a catalog product is ignored. Send the same idempotency_key twice and the original order comes back untouched.
/api/v1/ordersorders:readMerchant+Your orders, newest first. Accepts limit (default 25, maximum 100) and status.
/api/v1/orders/:idorders:readMerchant+Full detail including line items and the claim link. Recipient data is limited to name, country and city, and only after the recipient has confirmed their details: you paid for the order, but you do not need the buyer's full address to reconcile it.
/api/v1/orders/:id/cancelorders:writeEliteCancels an order the recipient has not yet claimed and returns the money to the source it came from. Once a recipient has confirmed their delivery details the order can no longer be cancelled through the API.
/api/v1/walletwallet:readMerchant+Balance, credit headroom, your tier's monthly prepaid-order allowance and the last twenty ledger entries.
Request
POST /api/v1/orders
X-EIG-Key-Id: eig_live_1a2b3c4d
X-EIG-Timestamp: 1769000000
X-EIG-Nonce: 6f9c1e07-...-b2
X-EIG-Signature: 4d5e...c1
{
"idempotency_key": "order-9f2c",
"external_ref": "I20260713175900",
"on_behalf": true,
"pay_from": "wallet",
"items": [{ "sku": "EIG-4A7C21B9", "qty": 24 }],
"external_items": [
{ "name": "USB-C 65W charger", "sku": "EIG-4A7C21B9",
"qty": 24, "unit_price_cents": 1720 }
],
"external_digest": "9b1f...e4"
}Response
201 Created
{
"order_id": "b6f1...",
"order_number": "EIG-20260722-0F3A1",
"status": "awaiting_recipient",
"claim_url": "https://elect-i-global.com/en/claim/xY...",
"amount_charged": "412.80",
"currency": "USD",
"wallet_balance_after": "8190.40",
"items_digest": "9b1f...e4",
"digest_matches": true
}If you send external_items together with the external_digest you computed over them, the response reports whether your digest matches the one ELECT-I computed independently. That comparison is what makes the recipient's claim page meaningful, so compute the digest on your side rather than copying ours.
03
Webhooks
Elite accounts can register HTTPS endpoints and receive order lifecycle events. Deliveries are queued first and attempted afterwards, so an endpoint that is down loses nothing.
Verifying a delivery
Each POST carries x-eig-event, x-eig-timestamp and x-eig-signature. The signature is a lowercase hex HMAC-SHA256 over the string <timestamp>.<raw body>, keyed on that endpoint's own signing secret. Compare it in constant time and reject anything that does not match.
Retries
A delivery is attempted up to six times with growing gaps of roughly 30 seconds, 2 minutes, 8 minutes, 30 minutes and 2 hours. Any 2xx response ends the sequence. Endpoints must be idempotent, because a delivery you have already processed can arrive again.
04
Errors
Every failure returns the same envelope: an error object with a stable code and a human-readable message. Match on the code, never on the message.
| Code | Status | Meaning |
|---|---|---|
| missing_headers | 401 | One of the four signing headers was absent. |
| unknown_key | 401 | No such key, or the stored secret failed its own integrity cross-check. |
| key_revoked | 401 | The key exists but has been revoked in the dashboard. |
| stale_timestamp | 401 | The timestamp is more than 300 seconds from server time. |
| replayed_nonce | 409 | That nonce has already been used on this key. |
| bad_signature | 401 | The signature did not match the canonical string. |
| account_suspended | 403 | The account behind the key is suspended. |
| tier_forbidden | 403 | The tier has no API access, or the membership is not active. |
| insufficient_funds | 402 | The wallet balance does not cover the order total. |
| credit_limit | 402 | The order would exceed the agreed credit line. |
| quota_exceeded | 429 | The tier's monthly prepaid-order allowance is used up. |
| moq_not_met | 422 | A line is below the minimum order quantity that applies at your tier. |
| unknown_product | 422 | A SKU or product id does not resolve to an available listing. |
| not_found | 404 | No such order for this account. Orders belonging to another merchant return the same 404 rather than confirming that they exist. |
| already_claimed | 409 | The recipient has already confirmed delivery details, so the order cannot be cancelled. |
| invalid_state | 409 | The order's current status does not allow that transition. |
| server_error | 500 | Something failed on our side. Retry with the same idempotency key. |
Ready to build?
Create a key in the dashboard, or ask us about the integration first.