Valet by Storey

Developer docs

Everything you need to take a card payment.

Two credentials, three endpoints, and a widget or a plain API. No payments background required.

The vocabulary

Three words cover the whole system.

Valet Key

The API key issued per location. Every request identifies itself with one — it says which of your businesses and which counter is charging the card.

Valet Tag

The device ID issued per station. A tag is either a physical terminal — the customer taps where they already are — or a key-entry station, where the card is typed into Storey's hosted payment window. The sale endpoint serves terminals; key-entry stations go through a session.

Valet Ticket

The transaction ID returned after every sale (storeyTransactionId). Reference it to refund or void later. No ticket, no refund.

Authentication

Two headers on every request.

Every call carries the location's Valet Key and the station's Valet Tag. Together they resolve which business, which counter and which terminal the request belongs to — there is nothing else to sign, token or renew. Keep both values server-side; requests are rate limited and every call is logged.

Headers

X-Valet-Key: vk_live_XXXXXXXXXXXXXXXXXXXXXXXX
X-Valet-Tag: vt_XXXXXXXXXXXXXXXXXXXX

Endpoints

Three calls do everything.

The base URL is https://storeypos.com. All three are POSTs and all three answer with the same field names, so one response handler covers the lot.

Create a sale — POST /api/public/valet/sale

Send your own referenceId — it is your idempotency key, and repeating it returns the original answer instead of charging twice. subtotal is required; taxAmount, tipAmount and shippingAmount default to 0; and total must equal their sum — the server checks the maths and rejects the call otherwise. A replayed flag of true means this referenceId was seen before and the original result was returned.

Status is approved, declined, no_connection or error.

This endpoint is for terminal stations only. If the Valet Tag is a key-entry station, the call is rejected and you are pointed at POST /api/public/valet/session instead — a card number can never be sent to Storey over this API.

Request — cURL

curl -X POST https://storeypos.com/api/public/valet/sale \
  -H "X-Valet-Key: vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "X-Valet-Tag: vt_XXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -d '{"referenceId":"order-10482","subtotal":40.00,"taxAmount":2.50,"total":42.50}'

Response

{
  "storeyTransactionId": "vlt_9f2c8e1a4b3d",
  "referenceId": "order-10482",
  "status": "approved",
  "authCode": "0F3C21",
  "rrn": "042619005831",
  "card": { "brand": "visa", "last4": "4242", "entryMode": "contactless" },
  "receipt": {
    "batchNo": "12",
    "tranNo": "VC07",
    "responseText": "SUCCESS",
    "surchargeAmount": "0.00",
    "cashDiscount": "0.00",
    "cardholderName": "JANE SAMPLE",
    "terminal": "2319993484"
  },
  "amounts": {
    "subtotal": 40.00,
    "taxAmount": 2.50,
    "tipAmount": 0,
    "shippingAmount": 0,
    "total": 42.50
  },
  "refundedAmount": 0,
  "voidedAt": null,
  "error": null,
  "capturedAt": "2026-09-16T18:22:04Z",
  "createdAt": "2026-09-16T18:21:58Z",
  "replayed": false
}

Decline categories

Sale and reversal responses also carry declineCategory. It is null unless the processor's own code is one of the few worth acting on; the plain error text is always the full reason.

retain_card
04, 07, 41, 43, 208, 209, 704, 767 — pick-up, lost or stolen card. Hold the card.
suspected_fraud
34, 129, 246
invalid_cvv
N7, 82, E78, E122, 517, 954, 958
invalid_card
14, E06, VI, 540, 564, 605, 606
insufficient_funds
51
expired_card
54

Refund a sale — POST /api/public/valet/sale/:storeyTransactionId/refund

Include amount for a partial refund, or omit the body to return everything left. The refund runs on the very terminal that took the payment. The response carries refundedTotal and remaining so you always know where the sale stands; a refund beyond what remains is rejected before anything is sent to the terminal. Refunds can be switched off per location — the error tells you plainly if they are.

Request — cURL

curl -X POST https://storeypos.com/api/public/valet/sale/vlt_9f2c8e1a4b3d/refund \
  -H "X-Valet-Key: vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "X-Valet-Tag: vt_XXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -d '{"amount": 42.50}'

Response

{
  "storeyTransactionId": "vlt_9f2c8e1a4b3d",
  "referenceId": "order-10482",
  "reversalId": "rvs_5d71b09c2e44",
  "kind": "refund",
  "amount": 42.50,
  "status": "approved",
  "authCode": "1A7D04",
  "rrn": "042619005831",
  "error": null,
  "refundedTotal": 42.50,
  "remaining": 0,
  "createdAt": "2026-09-17T09:14:31Z"
}

Void a sale — POST /api/public/valet/sale/:storeyTransactionId/void

No body. A void cancels the sale the same day, before settlement; after that the money has moved and a refund is the only route — the response says so if you try. A sale that has already been partially refunded can only be refunded, never voided.

Request — cURL

curl -X POST https://storeypos.com/api/public/valet/sale/vlt_9f2c8e1a4b3d/void \
  -H "X-Valet-Key: vk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "X-Valet-Tag: vt_XXXXXXXXXXXXXXXXXXXX"

Response

{
  "storeyTransactionId": "vlt_9f2c8e1a4b3d",
  "referenceId": "order-10482",
  "reversalId": "rvs_8c20e5f71a93",
  "kind": "void",
  "amount": 42.50,
  "status": "approved",
  "authCode": null,
  "rrn": "042619005831",
  "error": null,
  "refundedTotal": 0,
  "remaining": 0,
  "createdAt": "2026-09-16T18:26:12Z"
}

Every response carries the real reason for a decline — never a generic message — so your software can show the customer something useful.

The widget

Or skip the endpoints entirely.

The widget is a self-contained, shadow-DOM component — it always looks like Storey, never like your site, and needs no styling from you. Open it with a session token before charging the card; it walks the customer through confirm, tap and receipt, and reports the outcome back to your code.

The session tells the widget which kind of station it is working with. For a terminal it waits at the card machine; for a key-entry station it opens Storey's hosted payment window, where the card is typed into the payment provider's own secure field. Either way your code sees the same result.

HTML

<script src="https://storeypos.com/valet-widget.js"></script>
<script>
  StoreyValet.open({
    token: session.token,
    onComplete: (result) => { /* result.status: "approved" | "declined" */ },
    onCancel: () => {},
  });
</script>

Valet, by Storey

Ready to create a Valet account?

Tell us about your software and we'll set you up with your first Valet Key and Valet Tag.

Talk to us