Short Locates
A locate is a short-borrow reservation. Before you can sell short a stock that is not easy to borrow, you must locate the shares first - the locate secures your right to borrow at a specific price for the current session.
TradeZero exposes three locate pools, each suited to different borrow scenarios:
Locate- standard inventory for any non-easy-to-borrow name that is not on the Reg SHO threshold list. Reusable throughout the day; credit-back eligible.Pre-Borrow- reserved inventory for Reg SHO threshold securities. Reusable throughout the day; credit-back eligible.Single Use- one-and-done inventory for Reg SHO threshold securities. Consumed by the first short you open against it; credit-back eligible only if you have not shorted yet.
The system picks the pool automatically based on the symbol. Your job is to request a priced quote, decide which offer to accept, and credit back any inventory you do not use. The Locates API covers that entire workflow: check whether a symbol is easy to borrow, request a quote, accept the priced offer, manage live inventory, and credit back anything unused at the end of the session.
The Locates API completes the full quote → accept → inventory workflow on live accounts. On paper, the endpoints accept requests but do not allocate borrow inventory — see Account Types → Locates for how paper behaves.
GET /is-easy-to-borrowalways returns{ "isEasyToBorrow": true }regardless of symbol - including for Reg SHO threshold names like CYCU.POST /quotereturns200 OK { "locateQuoteSent": "true" }and the quote appears in/historyaslocateStatus: 56 Rejectedwith an explanatorytextfield — locates require a live account with offerable inventory.POST /acceptandPOST /sellack with 200 OK but nothing ever lands in/inventory.GET /inventoryon a paper account returns an empty array.
To run the full locate workflow you need a funded live account - see Account Types for the difference between the two.
The three locate types
TradeZero exposes three classes of locate. The system picks the type automatically based on the symbol and the inventory pools that are loaded - you cannot ask for a specific type on the quote request.
| Type | Code (locateType) | /sell string | Reusable in a day? | Credit-back eligible? | Applies to |
|---|---|---|---|---|---|
Locate | 1 or 2 | "Locate" | Yes | Yes, while not tied to a short | Non-easy-to-borrow equities |
PreBorrow | 3 | "PreBorrow" | Yes | Yes, while not tied to a short | Reg SHO threshold securities |
SingleUse | 4 | "SingleUse" | No - one-time use | Yes, only if never used to short | Reg SHO threshold securities |
A few rules worth committing to memory:
- A
Locatecovers any non-easy-to-borrow stock that is not on the Reg SHO threshold list. You can short and cover against it repeatedly throughout the day as long as the located share count is large enough. SettledLocaterows come back as eitherlocateType: 1orlocateType: 2depending on which inventory pool funded the row, but the two codes are the same product - always send"Locate"on/sell. Pre-Borrow(3) andSingle Use(4) are the two ways to short a Reg SHO threshold security.Pre-Borrowis reusable within the day;Single Useis gone the moment you open a short with it, even if you cover the position again.- All three credit-back paths work - sell back any locate that is not tied to an open short and the unused portion comes off your bill.
Reg SHO threshold securities
A Reg SHO threshold security is a stock that has accumulated significant "fails to deliver" - five consecutive settlement days of unsettled trades crossing the SEC's threshold. The SEC publishes the list daily, and brokers are required to apply stricter borrowing rules to any name on it. That is why these symbols never satisfy a regular Locate and instead need either a Pre-Borrow (reusable) or a Single Use (one-and-done). For background see the SEC's Reg SHO page.
How the lifecycle works
The locates API is asynchronous. Every POST and DELETE returns 200 OK with a { "*Sent": "true" } body as soon as the request is accepted — that confirms queuing, not completion. The outcome appears a second or two later in /locates/history.
Every short-locate workflow walks through the same six steps, regardless of whether you are integrating from a desktop client, a server backend, or a one-off script:
- Is the symbol easy to borrow?
GET /v1/api/accounts/{accountId}/is-easy-to-borrow/symbol/{symbol}returns{ "isEasyToBorrow": true }if the symbol is on the ETB list - no locate needed, send the short directly. Iffalse, continue. - Request a quote.
POST /v1/api/accounts/locates/quotewith the symbol, share count, and a freshquoteReqIDyou generate. The response is just an ack. - Poll
/locates/history. Look for the row whosequoteReqIDmatches yours. WhenlocateStatusreaches65(Offered) andlocatePrice > 0, the price is firm. For Reg SHO threshold symbols you will see two matching rows - one with your originalquoteReqID(the Pre-Borrow offer) and one with a<your-quoteReqID>.SUsuffix (the Single Use offer, which is typically several times cheaper). You pick which to accept. - Accept within 30 seconds. Every priced quote expires 30 seconds after it is first offered.
POST /v1/api/accounts/locates/acceptwith the chosenquoteReqID. Accepting one Reg SHO offer auto-expires the sibling - if you accept the.SUrow the Pre-Borrow row moves to status67(Expired), and vice versa. If the 30-second window elapses without an accept, both rows move to67(Expired) and a fresh/quoteis required to try again. - Watch the inventory. Once an accept is processed, the symbol appears in
GET /v1/api/accounts/{accountId}/locates/inventorywithavailableequal to the share count. You can short against this row up toavailableshares; covers return shares to the pool forLocateandPre-Borrowrows, but not forSingle Use. - Optionally credit back what you did not use.
POST /v1/api/accounts/locates/sellwith thelocateTypestring and the residual quantity. The shares move fromavailableintotoBeSold, then drop off the inventory once the credit clears. You canDELETEthe sellquoteReqIDto undo it before it fills.
Polling cadence
The locates API is asynchronous, so timing your polls correctly matters more than polling frequency.
Default cadence after POST /quote: wait 2 seconds, then poll /locates/history up to four times at 2-second intervals (an 8-to-10-second budget total). Quotes settle in 1.5 to 3 seconds on a healthy book. Symbols with no inventory either never produce a history row or come back canceled with text: "No shares available.".
While a Locates screen is open: refresh /locates/inventory every 5 seconds, and /locates/history every 8 seconds.
The general rule: poll aggressively (1.5–2 seconds) for the short window immediately after any write (/quote, /accept, /sell, /cancel), then back off to 5–8 seconds while idle. Polling faster than ~1 second rarely surfaces new data while a write is still being applied.
Endpoint reference
The base URL for production is https://webapi.tradezero.com. Every call needs the TZ-API-KEY-ID and TZ-API-SECRET-KEY headers; JSON POST calls also need Content-Type: application/json.
The field name for the account identifier in the request body is not the same on every write endpoint:
| Endpoint | Account field |
|---|---|
POST /locates/quote | account |
POST /locates/accept | accountId |
POST /locates/sell | account |
DELETE /locates/cancel/... | n/a - account ID is in the URL path |
Response payloads always echo the field as accountId. The easiest approach is to define one constant per endpoint in your client rather than sharing a single variable.
Check easy-to-borrow
GET /v1/api/accounts/{accountId}/is-easy-to-borrow/symbol/{symbol}
Returns a single boolean. The endpoint compares the supplied symbol against the platform-wide Easy-to-Borrow list using an exact, case-sensitive match - send the symbol in uppercase. Any value that is not on the list returns { "isEasyToBorrow": false }, so validate symbols upstream in your client before calling this endpoint. An empty symbol or a path segment like a single . returns a 404 from the router rather than a JSON body, so always supply a non-empty path segment.
{ "isEasyToBorrow": true }
{ "isEasyToBorrow": false }
The status does not flip after you successfully locate. ETB reflects the platform-wide classification of the symbol's borrow pool, not your account's locate inventory. Always uppercase the symbol before calling this endpoint, and run your own symbol validation upstream - the endpoint will not catch typos for you.
Request a locate quote
POST /v1/api/accounts/locates/quote
| Field | Type | Required | Notes |
|---|---|---|---|
account | string | Yes | Live account ID. Paper accounts will not produce real inventory. |
symbol | string | Yes | Equity ticker. Options are not locatable; this endpoint is equities-only. |
quantity | integer | Yes | Number of shares. Minimum 100, maximum 2,147,483,647, must be a multiple of 100. Sub-100 values return synchronous validation errors (HTTP 400). Non-multiples-of-100 (e.g. 150, 199, 250) return 200 OK, then a Rejected row in /locates/history with locateStatus: 56, locateError: 67, text: "R67: You can only offer for credit in lots of 100". |
quoteReqID | string | Yes | Unique client-generated identifier. You will use this to poll and accept. |
curl -X POST "https://webapi.tradezero.com/v1/api/accounts/locates/quote" \
-H "TZ-API-KEY-ID: $TZ_API_KEY" \
-H "TZ-API-SECRET-KEY: $TZ_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"account": "TTE12345678",
"symbol": "BB",
"quantity": 100,
"quoteReqID": "Q-BB-EXAMPLE-001"
}'
{ "locateQuoteSent": "true" }
The 200 OK is an acknowledgement that the request was queued. The priced quote arrives moments later in /locates/history (see next section). Symbol-level outcomes - including rejections - are surfaced asynchronously as a locateStatus: 56 row in history with a human-readable text and a locateError numeric code.
Synchronous validation
A handful of inputs are checked synchronously and return a 4xx. Validation responses come back in a few different body shapes depending on which check fails — the patterns below cover each one. Handle these response shapes in your client.
{
"errors": {
"Quantity": ["The field Quantity must be between 100 and 2147483647."]
},
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "0HNL06PEEJ8MJ:00000002"
}
This same body is returned for quantity: 0, quantity: 99, and negative quantities. The minimum is exactly 100 shares.
{
"statusCode": "BadRequest",
"message": "Account for User was not found, or User doesn't have entitlements.",
"detail": null
}
This response covers two scenarios: credentials that cannot be mapped to the account (for example, supplying a paper account ID with a live API key, or vice versa) and a request body that is missing the account field. In both cases the body is identical, so handle them with one code path. For TradeZero America accounts, a common trigger is sending the portal login instead of the 2TZ account number - use the account value returned by GET /v1/api/accounts. See Account Information.
- (root): quoteReqID is required
/accept and /sell report missing top-level fields as plain-text strings with one - (root): <field> is required line per missing field, joined by \n if more than one is missing. Because each endpoint expects a specific field name for the account ID, sending the wrong name can produce a - (root): accountId is required or - (root): account is required response - see the account ID field reference at the top of Endpoint reference.
To keep request bodies clean, send only the fields documented for each endpoint. Always generate a fresh, non-empty quoteReqID for every /quote request - it is the only way to track the outcome in /history. Always include the symbol field; an omitted symbol is processed as a malformed request and surfaces in /history as a status 56 row that you cannot act on.
Asynchronous rejections
When the API accepts the request (200 OK) but inventory is unavailable, the outcome surfaces moments later as a locateStatus: 56 row in /locates/history. Three of the most common shapes:
{
"accountId": "TTE12345678",
"symbol": "AAPL",
"quoteReqID": "Q-AAPL-EXAMPLE-001",
"locateShares": 100,
"filledShares": 0,
"locateStatus": 56,
"locateError": 56,
"locateType": 0,
"locatePrice": 0,
"preBorrow": false,
"text": "R56: This symbol is Easy to Borrow. No need to locate",
"createdDate": "2026-05-12T20:38:50.733Z",
"updatedDate": "2026-05-12T20:38:50.733Z"
}
{
"accountId": "TTE12345678",
"symbol": "EZGO",
"quoteReqID": "Q-EZGO-EXAMPLE-001",
"locateShares": 100,
"filledShares": 0,
"locateStatus": 56,
"locateError": 4,
"locateType": 0,
"locatePrice": 0,
"preBorrow": false,
"text": "R06: This symbol is restricted from trading at this price",
"createdDate": "2026-05-12T20:38:48.218Z",
"updatedDate": "2026-05-12T20:38:48.238Z"
}
{
"accountId": "TTE12345678",
"symbol": "BB",
"quoteReqID": "Q-BB-EXAMPLE-002",
"locateShares": 100,
"filledShares": 0,
"locateStatus": 56,
"locateError": 14,
"locateType": 0,
"locatePrice": 0,
"preBorrow": false,
"text": "14 Locate is already pending.",
"createdDate": "2026-05-12T21:23:10.235Z",
"updatedDate": "2026-05-12T21:23:20.983Z"
}
14 Locate is already pending. fires when another /quote for the same symbol is still in flight - the visible signal is an earlier row at locateStatus: 65 Offered or 54 Pending whose lifecycle has not finished yet. TradeZero allows one open quote per symbol at a time and rejects the second submission immediately while the first continues to live out its lifecycle normally. To proceed, wait for the first row to reach a terminal state (50 Filled, 52 Canceled, 56 Rejected, or 67 Expired) - or call DELETE /cancel on it - then resubmit. Pending credit-backs (toBeSold > 0 on the inventory row) do not trigger this rejection; you can /quote a symbol even while a /sell for it is queued.
Different rejection causes produce different locateError codes and different text prefixes. R<n>: prefixes come from the venue (R06 restricted, R56 already easy-to-borrow); bare numeric prefixes come from the locate service (11 No shares available., 14 Locate is already pending., 16 Insufficient shares., 1 System error.). Parse the prefix if you want to bucket rejections, and log the full text so you also capture any venue-specific detail that follows the prefix. locateError and locateStatus are independent fields - read each separately rather than expecting them to match.
Watch /locates/history for the priced offer
GET /v1/api/accounts/{accountId}/locates/history
Returns every locate row for the trading day (quotes, accepts, sells, expirations) in a wrapped object. Poll this endpoint after a quote and filter by your quoteReqID:
{
"locateHistory": [
{
"accountId": "TTE12345678",
"symbol": "BB",
"quoteReqID": "Q-BB-EXAMPLE-001",
"locateShares": 100,
"filledShares": 0,
"locateStatus": 65,
"locateType": 1,
"locatePrice": 0.0045,
"preBorrow": false,
"locateError": 0,
"text": "",
"createdDate": "2026-05-12T20:24:52.640Z",
"updatedDate": "2026-05-12T20:24:55.230Z"
}
]
}
| Field | Type | Notes |
|---|---|---|
accountId | string | The account that owns the row. |
symbol | string | Underlying ticker. |
quoteReqID | string | Echoes the value you sent on /quote or /sell. Note the capital ID suffix. |
locateShares | number | Shares for this row. Negative on credit-back (sell) rows. |
filledShares | number | How many have been filled - reaches locateShares when the row is fully Filled (status 50). |
locateStatus | number | See the status table. Drives the state machine. |
locateType | number | 0 on Rejected (56) rows where the system never picked a pool; otherwise 1, 2, 3, or 4. See the type table. |
locatePrice | number | Cost per share. 0 during the in-flight states (81 Quoting and 54 Pending). Non-zero once locateStatus reaches 65 (Offered) or 50 (Filled). |
preBorrow | boolean | Independent boolean flag retained on every history row. The canonical pool indicator is locateType (3 = Pre-Borrow, 4 = Single Use, 1/2 = standard Locate); use that field rather than preBorrow when classifying a row. |
locateError | number | 0 on success rows. On Rejected (56) rows it carries the rejection-reason code (e.g. 1, 4, 11, 40, 56); the value mirrors the text prefix rather than locateStatus itself. |
text | string | Free-form: null during Quoting, "" once Offered, "R<code>: <reason>" on R-prefixed rejections, "<n> <reason>" (e.g. "11 No shares available. Please try again later.") on numeric-prefixed rejections, "Quote expired." when status moves to 67. |
createdDate | string | ISO 8601 UTC timestamp of the original row. |
updatedDate | string | ISO 8601 UTC timestamp of the most recent state transition. |
The Single Use shadow row
For Reg SHO threshold symbols, a single /quote call produces two rows in /history, not one. The original quoteReqID carries the Pre-Borrow side; a parallel row with a .SU suffix carries the Single Use side. The Single Use price is normally several times cheaper - SOBR, for example, typically prices at $0.01/share for Single Use vs $0.03/share for Pre-Borrow, and 3-to-5x gaps between the two pools are common. Pick whichever you want and /accept that quoteReqID - the sibling automatically moves to 67 (Expired) with text: "Quote expired.". The recommended default is to take the .SU row for cost reasons, unless you specifically need a Pre-Borrow's reusable-through-the-day behavior.
{
"locateHistory": [
{
"symbol": "SOBR",
"quoteReqID": "Q-SOBR-EXAMPLE-001",
"locateType": 3,
"locatePrice": 0.03,
"locateStatus": 65,
"locateShares": 100
},
{
"symbol": "SOBR",
"quoteReqID": "Q-SOBR-EXAMPLE-001.SU",
"locateType": 4,
"locatePrice": 0.01,
"locateStatus": 65,
"locateShares": 100
}
]
}
It is also normal for only one side of the pair to be priced. If a threshold symbol is loaded for Single Use but has no Pre-Borrow inventory, the base row will sit at locateStatus: 54 (Pending) with a human-readable text like "11 No shares available. Please try again later." while the .SU row reaches 65 (Offered). That is the system telling you the SU is your only option:
{
"locateHistory": [
{
"symbol": "CYCU",
"quoteReqID": "Q-CYCU-EXAMPLE-001",
"locateType": 3,
"locatePrice": 0,
"locateStatus": 54,
"locateError": 11,
"text": "11 No shares available. Please try again later."
},
{
"symbol": "CYCU",
"quoteReqID": "Q-CYCU-EXAMPLE-001.SU",
"locateType": 4,
"locatePrice": 0.01,
"locateStatus": 65,
"text": ""
}
]
}
Always poll for both quoteReqID and <quoteReqID>.SU, prefer the Single Use row when it is Offered and priced, and fall back to the base row otherwise.
If neither pool has inventory at the moment, both rows remain at locateStatus: 54 (Pending) with text: "11 No shares available. Please try again later." and ultimately expire to 67 rather than ever reaching 65. The symbol is unborrowable for both Single Use and Pre-Borrow until inventory loads; request a fresh quote later.
Accept a quote
POST /v1/api/accounts/locates/accept
| Field | Type | Required | Notes |
|---|---|---|---|
accountId | string | Yes | Live account ID. This endpoint uses the field name accountId - not account. See the field name reference at the top of Endpoint reference. |
quoteReqID | string | Yes | The exact quoteReqID of the history row you want to commit. For Reg SHO threshold symbols you must include the .SU suffix to accept the Single Use side; omit it to accept the Pre-Borrow side. |
{ "locateAcceptSent": "true" }
Selecting between offered types
There is no locateType field on the accept request. The system identifies the type purely by which quoteReqID you send. The selection rules are:
| What you quoted | What is in /history | Which quoteReqID you accept |
|---|---|---|
Non-easy-to-borrow, non-threshold (e.g. BB) | One row, your original quoteReqID, locateType: 1 or 2 (Locate) | Your original quoteReqID - there is no choice. |
Reg SHO threshold, both pools priced (e.g. SOBR) | Two rows: base quoteReqID (PB, locateType: 3) and <base>.SU (Single Use, locateType: 4) | Send the base quoteReqID for PB or <base>.SU for Single Use. |
Reg SHO threshold, only SU priced (e.g. CYCU) | Base row stays at locateStatus: 54 (Pending) with "No shares available"; .SU row reaches 65 (Offered) | Send <base>.SU - the base PB row will never reach Offered. |
| Reg SHO threshold, only PB priced | Base row reaches 65 (Offered); .SU row stays Pending or never appears at 65 | Send the base quoteReqID. |
The selection is binding: once you accept one row, the system automatically expires the sibling. Accepting one side of a Reg SHO pair reserves exactly one inventory pool per /quote.
curl -X POST "https://webapi.tradezero.com/v1/api/accounts/locates/accept" \
-H "TZ-API-KEY-ID: $TZ_API_KEY" \
-H "TZ-API-SECRET-KEY: $TZ_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"accountId": "TTE12345678",
"quoteReqID": "Q-SOBR-EXAMPLE-002.SU"
}'
curl -X POST "https://webapi.tradezero.com/v1/api/accounts/locates/accept" \
-H "TZ-API-KEY-ID: $TZ_API_KEY" \
-H "TZ-API-SECRET-KEY: $TZ_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"accountId": "TTE12345678",
"quoteReqID": "Q-SOBR-EXAMPLE-002"
}'
Both calls return { "locateAcceptSent": "true" }. The difference is purely in which quoteReqID you send - and consequently, which locateType ends up on your inventory row a moment later.
After accepting the .SU side of a SOBR quote, /history shows the picked row Filled and its sibling Expired:
{
"locateHistory": [
{
"quoteReqID": "Q-SOBR-EXAMPLE-002.SU",
"locateType": 4,
"locatePrice": 0.01,
"locateStatus": 50,
"text": ""
},
{
"quoteReqID": "Q-SOBR-EXAMPLE-002",
"locateType": 3,
"locatePrice": 0,
"locateStatus": 67,
"text": "Quote expired."
}
]
}
locateType: 4 is now in the account's inventory; locateType: 3 is gone. To go the other way, send the base quoteReqID (without .SU) and the picked / expired roles flip.
Additional acceptance details
The endpoint always responds 200 - including for a quoteReqID that does not exist, has already been accepted, or has already expired. To confirm the accept landed, watch the history row transition to locateStatus: 50 (Filled) and check that the symbol now shows up in /locates/inventory with available > 0. If the quote has already expired or never existed, you will see no inventory row and the original history row stays at its terminal status.
You have 30 seconds from the moment a row reaches locateStatus: 65 (Offered) to accept it. After that the platform automatically moves the row to 67 (Expired) and the quoteReqID is no longer acceptable. Treat the 30-second window as a hard deadline; if it lapses, request a fresh quote rather than retrying an accept against an expired row. Confirm any accept by watching /history for the row to transition to 50 (Filled).
Re-sending an accept for a quoteReqID you already accepted is safe: the API returns { "locateAcceptSent": "true" } again and de-duplicates downstream, so inventory does not double. The endpoint is safe to retry under network timeouts.
The synchronous 400s from this endpoint come back as plain text - one - (root): <field> is required line per missing field, joined with \n if more than one is missing. The three shapes you can hit:
- (root): quoteReqID is required
- (root): accountId is required
- (root): accountId is required
- (root): quoteReqID is required
View live inventory
GET /v1/api/accounts/{accountId}/locates/inventory
Returns one row per (symbol, locateType) pair that has not yet been fully sold or shorted away. Wrapped in { "locateInventory": [...] }. The primary key is the pair, not the symbol alone - a Reg SHO threshold symbol can produce two inventory rows on the same account, one per pool, and you must treat them as independent positions when shorting and crediting back.
Live accounts always return the wrapped { "locateInventory": [...] } shape. Paper accounts (which do not hold locate inventory) may respond with a bare [] instead. Normalize both shapes in client code: data.locateInventory ?? data ?? [].
{
"locateInventory": [
{
"accountId": "TTE12345678",
"symbol": "SOBR",
"available": 300,
"sold": 0,
"toBeSold": 0,
"unavailable": 0,
"locateType": 4
},
{
"accountId": "TTE12345678",
"symbol": "SOBR",
"available": 100,
"sold": 0,
"toBeSold": 0,
"unavailable": 0,
"locateType": 3
},
{
"accountId": "TTE12345678",
"symbol": "BB",
"available": 200,
"sold": 0,
"toBeSold": 0,
"unavailable": 0,
"locateType": 1
},
{
"accountId": "TTE12345678",
"symbol": "AUUD",
"available": 200,
"sold": 0,
"toBeSold": 0,
"unavailable": 0,
"locateType": 4
},
{
"accountId": "TTE12345678",
"symbol": "AUUD",
"available": 100,
"sold": 0,
"toBeSold": 0,
"unavailable": 0,
"locateType": 3
}
]
}
In the snapshot above the account holds:
- 300 SOBR Single Use shares (
locateType: 4) and 100 SOBR Pre-Borrow shares (locateType: 3) as two independent rows - 200 BB Locate shares (
locateType: 1) - 200 AUUD Single Use shares and 100 AUUD Pre-Borrow shares
To size a short on SOBR, draw from whichever inventory row matches the type you want to use. To credit back, send a /sell per row with the correct locateType string for each one - mixing the types up returns 16 Insufficient shares. because availability is checked per (symbol, locateType).
| Field | Type | Notes |
|---|---|---|
accountId | string | The owning account. |
symbol | string | Ticker. |
available | number | Shares that can still be shorted (or credited back) against this row. |
sold | number | Shares already credited back and processed. |
toBeSold | number | Shares queued for credit-back but not yet processed. Effectively a pending-sell counter. |
unavailable | number | Shares tied up - either pending sell-back or used to open a short. Equals toBeSold whenever a credit-back is in flight on a row with no open shorts. |
locateType | number | The pool that funded the row (1/2 Locate, 3 Pre-Borrow, 4 Single Use). Determines the locateType string you must send on /sell. |
available is the only field you should subtract from when sizing a short order. toBeSold and unavailable are bookkeeping - the row still appears in the response while a sell is in flight, and the cancel endpoint can flip those numbers back if you change your mind (see Cancel a quote or a pending sell).
The live API consistently returns exactly the seven fields above. Some older client libraries still declare sharesAvailable and quantity on the locate type, but neither field appears in current responses. If you are porting code from one of those libraries, fall back through available ?? sharesAvailable ?? quantity to stay compatible with old fixtures while preferring available for new code.
Credit back unused locates
POST /v1/api/accounts/locates/sell
| Field | Type | Required | Notes |
|---|---|---|---|
account | string | Yes | Live account ID. This endpoint uses the field name account - not accountId. See the field name reference at the top of Endpoint reference. |
symbol | string | Yes | Must match a row in your inventory. |
quantity | integer | Yes | Up to the current available for that symbol. Must be a multiple of 100 - non-round-lot quantities return R67: You can only offer for credit in lots of 100 (see the reason-code table). |
locateType | string | Yes | Send "Locate" for codes 1 or 2, "PreBorrow" for 3, "SingleUse" for 4. Codes 1 and 2 are the same product (a plain Locate); always credit them back as "Locate". See the type table for details, including the legacy enum strings the server still accepts. |
quoteReqID | string | Yes | A fresh identifier - do not reuse the buy-side quoteReqID. |
curl -X POST "https://webapi.tradezero.com/v1/api/accounts/locates/sell" \
-H "TZ-API-KEY-ID: $TZ_API_KEY" \
-H "TZ-API-SECRET-KEY: $TZ_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"account": "TTE12345678",
"symbol": "BB",
"quantity": 100,
"locateType": "Locate",
"quoteReqID": "S-BB-EXAMPLE-001"
}'
{ "locateSellbackSent": "true" }
After the sell request is accepted, history records it with locateShares: -100 (negative for credit-back) and locateStatus: 48 (New). The inventory row immediately reflects the pending sell: available drops to 0, toBeSold rises to the queued quantity, and unavailable reflects the locked total. When the credit clears, the row falls off the inventory entirely.
A /sell row sitting at locateStatus: 48 with toBeSold > 0 on the inventory side does not prevent a fresh /quote for the same symbol. A symbol with toBeSold > 0 will still accept a new /quote and return a clean Offered row. To re-locate a symbol with a pending credit-back, submit a new /quote — you do not need to cancel the pending sell first. What does block a second /quote is an earlier /quote on the same symbol that has not yet reached a terminal status (see Request a locate quote above).
Credit-back rules:
Locate(code1or2) andPre-Borrow(3) credit back the full unused portion, even if you have already shorted (and then covered) part of the locate earlier in the day. Use"Locate"on the/sellbody for both.Single Use(4) is the exception: it credits back only if no shares from the locate have been used to open a short. Once you open even a single share of short against an SU locate, the credit-back path is gone for the rest of the row - the remaining shares can still be shorted up to the locate quantity, but they can no longer be sold back for a refund.- Any locate that has shares tied to an open short cannot be credited back until you close the position.
Synchronous validation
The sell endpoint shares the same quantity floor as /quote: anything below 100 returns the same ProblemDetails 400 shown earlier, even when crediting back a row whose available is smaller than that. In normal flow this is not an issue because locates are sized in 100-share lots; scripts that mirror fractional fills should round up to the 100-share floor before submitting.
Missing or invalid locateType returns a plain-string body with the full enum spelled out by the server:
- (root): locateType is required
- locateType: locateType must be one of the following: "Unknown", "Locate", "IntraDay", "PreBorrow", "SingleUse"
Selling more than available is not a synchronous error — the API returns 200 OK { "locateSellbackSent": "true" } and queues the credit-back. Moments later /history shows a row with locateShares: -<oversize>, locateStatus: 56, locateError: 16, text: "16 Insufficient shares.". The inventory is untouched. The same outcome applies when crediting back a symbol you do not own.
{
"accountId": "TTE12345678",
"symbol": "CYCU",
"quoteReqID": "S-CYCU-EXAMPLE-001",
"locateShares": -600,
"filledShares": 0,
"locateStatus": 56,
"locateError": 16,
"locateType": 4,
"locatePrice": 0,
"preBorrow": false,
"text": "16 Insufficient shares.",
"createdDate": "2026-05-12T20:47:39.342Z",
"updatedDate": "2026-05-12T20:47:39.353Z"
}
Cancel a quote or a pending sell
DELETE /v1/api/accounts/locates/cancel/accounts/{accountId}/quoteReqID/{quoteReqID}
curl -X DELETE \
"https://webapi.tradezero.com/v1/api/accounts/locates/cancel/accounts/TTE12345678/quoteReqID/Q-BB-EXAMPLE-001" \
-H "TZ-API-KEY-ID: $TZ_API_KEY" \
-H "TZ-API-SECRET-KEY: $TZ_API_SECRET"
{ "locateCancelSent": "true" }
As with every other write endpoint, 200 OK is an ack - the cancel does not synchronously validate that the quoteReqID exists or that it points to anything cancellable. Confirm by watching /locates/history for the row to flip to locateStatus: 52 (Canceled) and, where relevant, /locates/inventory for the row to revert.
This one endpoint covers four different scenarios, each with different observable effects. The next four subsections walk through them in detail.
1. Decline an Offered quote before accepting
The canonical use case: you got a price you do not like, and you want the system to stop holding the row open. Cancel before the 30-second window elapses.
For a Reg SHO threshold symbol with a .SU shadow, canceling either quoteReqID (the base PB or the .SU row) cancels both sides. The selection mechanic is symmetric to accept: pick one side, the other side gets terminated automatically. You do not need to cancel the sibling yourself.
{
"locateHistory": [
{
"quoteReqID": "Q-CYCU-EXAMPLE-002",
"locateType": 3,
"locatePrice": 0,
"locateStatus": 52,
"text": "No shares available."
},
{
"quoteReqID": "Q-CYCU-EXAMPLE-002.SU",
"locateType": 4,
"locatePrice": 0,
"locateStatus": 52,
"text": "Quote canceled."
}
]
}
Both rows land at locateStatus: 52 (Canceled). The .SU row, which was at Offered when the cancel arrived, carries text: "Quote canceled."; the base row, which never reached Offered (no PB inventory), keeps its prior "No shares available." text. Declining the quote leaves the existing CYCU inventory completely untouched.
2. Cancel a pending credit-back to revert the sell
A POST /sell returns 200 OK { "locateSellbackSent": "true" }, the credit-back is queued, and the inventory immediately reflects toBeSold > 0 and available: 0. If you change your mind before the credit clears, send DELETE /cancel with the sell's quoteReqID and the row reverts.
This is the right move when you want to keep the shares instead of releasing them - the cancel rolls inventory back to its pre-sell numbers without round-tripping through another /quote. It is not a prerequisite for re-quoting that symbol; pending credit-backs do not block fresh /quote calls.
The full cycle, end to end:
| Step | /inventory for AUUD SU row | /history for the sell quoteReqID |
|---|---|---|
Before POST /sell | available: 300, toBeSold: 0, unavailable: 0 | row does not exist |
Right after POST /sell 300 | available: 0, toBeSold: 300, unavailable: 300 | locateStatus: 48, locateShares: -300, text: null |
Right after DELETE /cancel | available: 300, toBeSold: 0, unavailable: 0 (fully reverted) | locateStatus: 52, locateShares: -300, text: null |
POST /sell again with new quoteReqID | available: 0, toBeSold: 300, unavailable: 300 (pending again) | new row at locateStatus: 48 |
The canceled sell row's text stays null throughout - it never gains a human-readable string. Use the status transition 48 -> 52 and the matching inventory revert as your signal that the cancel succeeded, not the text field.
3. Cancel an already-terminal quoteReqID
A cancel against a quoteReqID that is already at a terminal status - Filled (50), Canceled (52), Rejected (56), Expired (67) - returns the same 200 OK { "locateCancelSent": "true" } acknowledgement, but no state change occurs. The same applies to every terminal case:
- The auto-Expired Pre-Borrow sibling of an accepted
.SUrow: cancel returns 200, no inventory change. - An already-Filled
.SUquoteReqID(the one used to acquire the inventory that is now sitting on the account): cancel returns 200, no inventory change - the locate stays where it is. - A
quoteReqIDthat does not exist on the account: cancel returns the same 200 OK acknowledgement.
In short, the cancel endpoint targets in-flight quotes and pending sells. Once a row reaches a terminal status, cancel has no further effect. Treat the 200 OK from /cancel as confirmation the request was received, and verify any expected state change by reading /history or /inventory.
4. Cancel as cleanup for an unsettled toBeSold row
If a /sell row remains in toBeSold for longer than expected (for example, your client process exited before the row settled), DELETE the sell's quoteReqID to revert it. Inventory rolls back and the row clears from /history. The row does not block new /quote calls for the same symbol; this cancel is purely an inventory-hygiene step. If you no longer have the original quoteReqID, recover it from /history by filtering for symbol, negative locateShares, and locateStatus: 48.
Reference tables
Locate status codes
| Code | Status | When you see it |
|---|---|---|
| 48 | New | A sell-back row has just been queued; not yet processed. text is normally null here. |
| 50 | Filled | Terminal success - the quote was accepted into inventory, or a sell was processed. |
| 52 | Canceled | Terminal - the row was canceled. For Offered quote rows the text becomes "Quote canceled."; for /sell rows the text stays null even after cancel. A base PB row that never reached Offered (because the SU sibling was preferred) keeps its prior "No shares available." text after the cancel transitions it from 54 to 52. |
| 54 | Pending | A row that the system is still working on. Reg SHO threshold names park their Pre-Borrow side here with text: "11 No shares available. Please try again later." when only Single Use inventory is loaded. |
| 56 | Rejected | Terminal - the system declined the request. Check locateError and text for the reason (R06, R56, etc.). |
| 65 | Offered | A priced quote is live and waiting for accept or cancel. 30-second window before it auto-expires. |
| 67 | Expired | Terminal - the 30-second offer window elapsed without an accept, or the sibling row in a Reg SHO Pre-Borrow + Single Use pair was accepted (the unaccepted side moves to 67). text is "Quote expired.". An explicitly canceled 65 Offered row goes to 52 Canceled, not 67. |
| 81 | Quoting | Brief in-flight state immediately after /quote. During polling, rows usually first appear in /history at 65 Offered, 54 Pending, or 56 Rejected. |
A clean UI only needs to render explicit states for 50 Filled, 52 Canceled, 56 Rejected, and 67 Expired. Anything else - 48 New, 54 Pending, 65 Offered, and the rarely-visible 81 Quoting - can be treated as a generic in-flight row and labeled with the text field directly. Special-case the four terminal-ish states, and let text speak for everything else.
The status state machine itself is small. A fresh /quote produces a row that appears in /history at either 65 Offered (price firm) or 54 Pending / 56 Rejected (no inventory or no offer available). From 65 Offered, a row goes to 50 Filled (you accepted), 52 Canceled (you or the sibling row was canceled), or 67 Expired (the 30-second window elapsed). Sell rows live on a parallel track: 48 New -> 50 Filled for a successful credit-back, or 48 New -> 52 Canceled if you cancel before it clears.
Reason codes (rejections, pending, and synchronous responses)
Three places surface a locateError + text pair: rejected rows (locateStatus: 56), some pending rows (locateStatus: 54, e.g. the Pre-Borrow side parked while only Single Use is loaded), and a couple of synchronous error responses on the write endpoints. Venue reason codes start with R; locate service codes use bare numeric prefixes — the table covers both forms.
locateError | text prefix | Where it appears | Meaning |
|---|---|---|---|
1 | 1 System error. | 56 Rejected | Required field missing or malformed request (e.g. omitted symbol). |
4 | R06: This symbol is restricted... | 56 Rejected | Venue restricts the symbol from trading at the requested price. |
11 | 11 No shares available... | 54 Pending or 56 Rejected | Pool has no inventory for the requested type. Commonly seen on the Pre-Borrow side of a Reg SHO quote when only Single Use is loaded - that row stays at 54 Pending rather than transitioning to 56. |
14 | 14 Locate is already pending. | 56 Rejected | An earlier /quote for the same symbol is still in flight. The POST /quote call returns 200 OK synchronously; the rejection lands a moment later as a row in /locates/history with locateStatus: 56 and locateError: 14. Wait for the in-flight quote to reach terminal status or DELETE it, then resubmit. |
16 | 16 Insufficient shares. | 56 Rejected | A /sell for more shares than you actually hold for that (symbol, locateType) pair, or a /sell where the locateType string does not match any of your rows. The POST /sell call returns 200 OK synchronously; the rejection lands a moment later as a /locates/history row with locateStatus: 56 and locateError: 16. |
40 | R40: Shorting stocks under $0.500000 is not allowed | 56 Rejected | Sub-$0.50 sub-penny restriction. The venue refuses to short the name at the current price; no locate quote can be produced. |
56 | R56: This symbol is Easy to Borrow. | 56 Rejected | The symbol is on the ETB list - no locate is needed, just place the short. |
67 | R67: You can only offer for credit in lots of 100 | 56 Rejected | The quantity on /quote (or /sell) was not a multiple of 100. Round-lot only. The POST returns 200 OK synchronously; the rejection lands asynchronously in /locates/history. |
A less common synchronous code is R114: Invalid duplicate UserOrderId, returned when a request's quoteReqID collides with a recently-used value. Generate a fresh, non-empty quoteReqID for every write call to avoid this.
Always log the full text rather than relying on the prefix alone; new venue codes are added periodically and the human-readable suffix often carries the meaningful detail.
Locate type codes
The numeric locateType on history and inventory rows maps to the string enum the /sell endpoint expects.
| Code | /sell string | Pool |
|---|---|---|
| 0 | Unknown | Transient. Quoting phase before the system picks a pool, or rejection rows where no pool was assigned. |
| 1 or 2 | Locate | Standard locate for non-easy-to-borrow stocks. Codes 1 and 2 are the same product, just funded from different inventory pools - always send "Locate" on /sell. |
| 3 | PreBorrow | Reg SHO threshold security, reusable through the day. |
| 4 | SingleUse | Reg SHO threshold security, one-and-done. |
Practical mapping when crediting back from a numeric locateType:
const LOCATE_TYPE_STRING: Record<number, string> = {
0: 'Unknown', // transient - do not /sell against this
1: 'Locate',
2: 'Locate',
3: 'PreBorrow',
4: 'SingleUse',
};
The full server-side enum, surfaced verbatim by the /sell 400 on an invalid value, is "Unknown" | "Locate" | "IntraDay" | "PreBorrow" | "SingleUse". The "IntraDay" string is a legacy spelling the server still accepts but should be treated as deprecated - it is not a distinct product, just an older alias for "Locate". New integrations should always send "Locate".
End-to-end example
The snippet below walks one symbol from ETB check through credit-back. The polling cadence - 2-second sleep before each of four poll attempts after the initial POST /quote - is the recommended default for new integrations.
const BASE = 'https://webapi.tradezero.com';
const HEADERS = {
Accept: 'application/json',
'Content-Type': 'application/json',
'TZ-API-KEY-ID': process.env.TZ_API_KEY!,
'TZ-API-SECRET-KEY': process.env.TZ_API_SECRET!,
};
const ACCOUNT = process.env.TZ_ACCOUNT!;
const LOCATE_TYPE_STRING: Record<number, string> = {
0: 'Unknown',
1: 'Locate',
2: 'Locate', // codes 1 and 2 are both plain locates - same product, different inventory pool
3: 'PreBorrow',
4: 'SingleUse',
};
async function isEasyToBorrow(symbol: string): Promise<boolean> {
const res = await fetch(
`${BASE}/v1/api/accounts/${ACCOUNT}/is-easy-to-borrow/symbol/${symbol}`,
{headers: HEADERS},
);
const json = await res.json();
return json.isEasyToBorrow === true;
}
async function quote(symbol: string, quantity: number, quoteReqID: string) {
await fetch(`${BASE}/v1/api/accounts/locates/quote`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({account: ACCOUNT, symbol, quantity, quoteReqID}),
});
}
async function pollForOffer(quoteReqID: string, preferSingleUse = true) {
for (let attempt = 0; attempt < 4; attempt++) {
await new Promise((r) => setTimeout(r, 2_000));
const res = await fetch(
`${BASE}/v1/api/accounts/${ACCOUNT}/locates/history`,
{headers: HEADERS},
);
const {locateHistory = []} = await res.json();
const su = locateHistory.find(
(r: any) => r.quoteReqID === `${quoteReqID}.SU` && r.locateStatus === 65,
);
const base = locateHistory.find(
(r: any) => r.quoteReqID === quoteReqID && r.locateStatus === 65,
);
const offer = preferSingleUse && su ? su : (base ?? su);
if (offer && offer.locatePrice > 0) return offer;
}
throw new Error(`No offered quote for ${quoteReqID} after 4 polls`);
}
async function accept(quoteReqID: string) {
await fetch(`${BASE}/v1/api/accounts/locates/accept`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({accountId: ACCOUNT, quoteReqID}),
});
}
async function inventoryFor(symbol: string) {
const res = await fetch(
`${BASE}/v1/api/accounts/${ACCOUNT}/locates/inventory`,
{headers: HEADERS},
);
const {locateInventory = []} = await res.json();
return locateInventory.find((r: any) => r.symbol === symbol);
}
async function sellBack(row: any, quoteReqID: string) {
const locateType = LOCATE_TYPE_STRING[row.locateType] ?? 'Locate';
await fetch(`${BASE}/v1/api/accounts/locates/sell`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({
account: ACCOUNT,
symbol: row.symbol,
quantity: row.available,
locateType,
quoteReqID,
}),
});
}
async function main() {
const symbol = 'ERNA';
if (await isEasyToBorrow(symbol)) {
console.log(`${symbol} is easy to borrow; no locate needed.`);
return;
}
const quoteReqID = `Q-${symbol}-${Date.now()}`;
await quote(symbol, 100, quoteReqID);
const offer = await pollForOffer(quoteReqID, /* preferSingleUse */ true);
console.log(
`Accepting ${symbol} ${LOCATE_TYPE_STRING[offer.locateType]} @ ${offer.locatePrice}/share`,
);
await accept(offer.quoteReqID);
// Allow a moment for the inventory row to appear.
await new Promise((r) => setTimeout(r, 1_500));
const row = await inventoryFor(symbol);
if (!row) throw new Error(`${symbol} did not land in inventory`);
// ... place short orders against row.available here ...
// Credit back anything still available at the end of the session.
if (row.available > 0) {
await sellBack(row, `S-${symbol}-${Date.now()}`);
}
}
main().catch(console.error);
Common workflows
The endpoints above can be combined into a handful of named flows that come up over and over in real integrations. Each one is short enough to inline, but worth memorizing because the order and the polling pauses matter.
Locate-and-short with end-of-day credit-back
The default lifecycle. Use this for any short-side strategy that does not need to short the same name multiple times.
GET /is-easy-to-borrow/symbol/<SYMBOL>- bail out with no locate needed iftrue.POST /quotewith a freshquoteReqID.- Poll
/historyevery 2 seconds, up to 4 times, looking for an Offered (65) row whosequoteReqIDmatches either your value or<value>.SU. Prefer.SUif both are Offered. POST /acceptwith the chosenquoteReqID. Do it within 30 seconds of seeing65- past that the row auto-Expires.- Wait ~1.5 seconds, then
GET /inventoryand confirm the symbol appears withavailable > 0. - Place short orders against that row, sized to at most
available. - At the end of the session, for any row whose
available > 0,POST /sellwith the matchinglocateTypestring (1->"Locate",2->"Locate",3->"PreBorrow",4->"SingleUse").
For a working implementation in Python and TypeScript, see the Locate & Sell Short recipe.
Decline a quote you do not want
Sometimes the priced offer is uneconomic and you want to release it before the 30-second window elapses.
- After your
POST /quote, poll/historyuntil the row reaches65 OfferedwithlocatePrice > 0. - Instead of
/accept, sendDELETE /cancelwith thatquoteReqID. For Reg SHO threshold names you can cancel either the base or the.SUrow - the sibling cancels automatically. - Wait one poll cycle and re-read
/history. The canceled row(s) should now sit atlocateStatus: 52withtext: "Quote canceled."(for the row that was Offered) or whatever54 Pendingtext the sibling already had.
Your existing inventory for that symbol is unaffected by a decline. The Cancel a Pending Locate recipe walks through this end to end.
Handle a 14 Locate is already pending. rejection
TradeZero allows only one open quote per symbol at a time. If you submit a second /quote while a previous one is still in flight - visible at 65 Offered or 54 Pending - the new row returns locateStatus: 56 with locateError: 14 and text: "14 Locate is already pending.". The first quote continues its normal lifecycle and is unaffected.
Two recovery paths, depending on whether you still want the first quote's price:
- Accept or wait it out. Resolve the existing in-flight row:
POST /acceptif the price is fine,DELETE /cancelif it is not, or simply let the 30-second Offered window elapse to67 Expired. As soon as the row is terminal you can/quoteagain with a freshquoteReqID. - Cancel the in-flight quote and re-submit. If you want to retry immediately with different sizing or a fresh price discovery,
DELETE /cancelon the firstquoteReqID. Once/historyshowslocateStatus: 52, submit your new/quote.
The original Q1 takes precedence here on purpose: canceling Q2 (the rejected row) has no effect because it never held inventory in the first place. Always operate on the first, in-flight quoteReqID to clear the pending quote. The Handle Locate Rejections recipe shows the full classification + recovery flow for this and the other common rejection codes.
Re-quote a symbol you already own
A re-quote is allowed as long as no earlier /quote for the same symbol is still in flight (visible at locateStatus: 65 Offered or 54 Pending). Pending credit-backs do not block re-quoting. If your last quote for the symbol has already reached 50 Filled, 52 Canceled, 56 Rejected, or 67 Expired, you can POST /quote again immediately:
POST /quotewith a brand-newquoteReqID(do not reuse the original).- Poll, accept or decline as in the standard flow.
- If the accept succeeds, you end up with the existing inventory row's
availableplus the new locate's quantity added. The two locates merge under the same(symbol, locateType)row in/inventoryrather than appearing as two separate rows.
This works because in-flight locates are keyed by symbol-plus-type and settled inventory accumulates by symbol-plus-type. The Reuse Locate Inventory recipe shows the full pattern for checking inventory first and only re-quoting when needed.
Pick between Single Use and Pre-Borrow at accept time
For Reg SHO threshold symbols a single /quote produces two offered rows. Your client picks by sending the right quoteReqID to /accept:
- Default to
.SUfor one-direction shorts - SU is typically 3-5x cheaper. Reg SHO threshold names like CYCU, SOBR, and AUUD commonly price at$0.01/sharefor SU vs$0.03-plus for PB. - Use the base PB
quoteReqIDonly when you expect to open and cover the same short repeatedly during the day, because PB is reusable and credit-back-eligible across multiple shorts while SU is consumed by the first short. - Accept one side only — accepting one side auto-expires the sibling. Accept the one you want, ignore the other.
The Locate Type Selection recipe walks through quoting a Reg SHO threshold name, collecting both the PB and SU offers, and selecting based on cost or reuse needs.
FAQ
Can I choose Single Use vs Pre-Borrow when I request a quote?
No. The system fans the request out to every pool that has inventory for the symbol and returns parallel offers in /locates/history. You choose by accepting the quoteReqID whose locateType you want. The unaccepted sibling expires on its own.
Why are there two history rows for one quote on some symbols?
On Reg SHO threshold securities only. The base row carries the Pre-Borrow side at your original quoteReqID. The shadow row carries the Single Use side at <quoteReqID>.SU. Either side can be Offered, Pending, or Expired independently - if only one pool has inventory, only that side will price; the other will park at locateStatus: 54 with a "No shares available" text. On non-threshold names only a single row is produced.
What does the response of every POST mean?
200 OK with { "*Sent": "true" } means the request was accepted and queued. Confirm the outcome by polling /locates/history for the resulting state transition.
Why did my /quote succeed with 200 but no offer ever arrived?
The pool has no inventory, the symbol is on the Reg SHO threshold list with no SU or PB loaded, or the symbol is restricted from trading at the moment. Look for the asynchronous outcome in /history: a locateStatus: 56 row with text: "R06: This symbol is restricted from trading at this price" for restricted symbols, "R56: This symbol is Easy to Borrow. No need to locate" for ETB symbols, or "11 No shares available. Please try again later." when no inventory is found.
What do the R codes in the text field mean?
On Rejected rows (locateStatus: 56), text is prefixed with R<n>: where n is a system reason code. The two most common: R06 = the symbol is restricted from trading at this price (no quote will be produced); R56 = the symbol is easy to borrow, so no locate is needed. There are other codes for venue-specific rejections - always log the full text rather than just the prefix.
Why does the cancel endpoint return 200 OK for a quoteReqID that does not exist?
POST and DELETE on the locates API are asynchronous — 200 OK confirms the request was accepted, not that the underlying state changed. Confirm cancellations by reading /locates/history for the resulting status transition.
Why is my /quote rejected with "14 Locate is already pending."?
Because another /quote for the same symbol is still in flight - the visible signal is an earlier row at locateStatus: 65 Offered or 54 Pending. TradeZero allows only one open quote per symbol at a time and rejects the second one immediately while the first continues its normal lifecycle. Pending credit-backs (toBeSold > 0 in inventory) do not trigger this code; you can /quote a symbol whose row has a queued sell. To proceed, resolve the first quote: accept it, cancel it, or wait for the 30-second Offered window to expire, then resubmit. See Handle a 14 Locate is already pending. rejection for the step-by-step.
Why do I see two inventory rows for the same symbol?
Because /inventory is keyed by (symbol, locateType), not by symbol alone. A Reg SHO threshold name where you accepted Single Use on one quote and Pre-Borrow on another will show up as two rows - one with locateType: 4, one with locateType: 3. Short-size and credit-back against each row independently, and remember to send the correct locateType string on /sell.
Can I cancel an accept I already sent?
No. Once the /accept lands and the inventory row shows available > 0, the locate is purchased and the cost is recorded. DELETE /cancel on a Filled (50) quoteReqID still returns 200 OK { "locateCancelSent": "true" }, but the system does not roll back the accept. The only way to free up the cost is /sell to credit-back the unused shares.
Does the text field always say "Quote canceled." after a cancel?
No - the wording depends on what state the row was in. An Offered (65) quote that you cancel transitions to 52 Canceled with text: "Quote canceled.". A Pending (54) sibling that gets canceled as collateral damage keeps its existing "No shares available." text. A pending /sell row that you cancel transitions to 52 Canceled with text: null. Match on locateStatus rather than text when deciding whether a cancel has landed.
Can I credit back a Single Use locate? Yes, only if no shares from the locate have been used to open a short. Once you have shorted even a single share against an SU row, the credit-back option is gone for that row - the remaining shares can still be shorted up to the row quantity, but they can no longer be sold back for a refund. Plan SU sizing carefully.
How long do I have to accept a quote?
30 seconds from the moment locateStatus first reaches 65 (Offered). After that the platform automatically transitions the row to 67 (Expired). Confirm an accept by watching /history for the row to reach 50 (Filled) rather than relying on the synchronous acknowledgement alone. If the 30-second window lapses, request a fresh quote with a new quoteReqID.
Should I prefer Single Use or Pre-Borrow when both are offered? Default to Single Use for cost - it is typically several times cheaper per share. Pre-Borrow is worth its premium when you plan to open and cover the same short repeatedly across the day, since Single Use is one-and-done on credit-back (see the previous answer). For most one-direction short setups, prefer Single Use.
Why does locateShares go negative on sell rows?
By design. Negative locateShares marks credit-back rows; positive marks acquisition rows. It lets a single /history feed describe both halves of the lifecycle without a separate status code for direction.
Does the is-easy-to-borrow endpoint validate symbol existence?
The endpoint answers a single question - "is this symbol on the platform-wide ETB list?" - so any symbol that is not on that list returns false. Use your own symbol-validation pipeline before quoting if you need to confirm the symbol exists at all.
Can I locate options? No. The locates API is equities-only. Options short selling uses the underlying's borrow state implicitly when the broker permits a particular strategy.
Are locate endpoints rate-limited?
Yes. Locate writes and reads are subject to per-API-key rate limits on webapi.tradezero.com, and /quote has an additional 30-second same-symbol cooldown per account. A duplicate quote inside that window may return a business rejection rather than 429. See API Rate Limits for the REST limits and backoff guidance.
Is the preBorrow boolean on history rows the same as locateType: 3?
No - they are independent fields. The pool a row belongs to is determined by locateType (3 = Pre-Borrow, 4 = Single Use, 1/2 = standard Locate). Use locateType as the canonical pool indicator.
What is the minimum quantity per quote?
100 shares. The API enforces Quantity must be between 100 and 2147483647 with a synchronous 400 on both /quote and /sell. Plan your share sizing around that floor.
Is it safe to retry /accept after a network timeout?
Yes. Re-accepting the same quoteReqID returns 200 OK { "locateAcceptSent": "true" } again and the system de-duplicates downstream - the inventory row does not double. Retries on /quote and /sell are not safe in the same way because each call needs a fresh quoteReqID; if you got a 200 OK back, the request was queued and you should not retry it.
What 400 response body shapes should I plan to handle?
The locate endpoints return validation errors in three shapes depending on which field is at fault. Branch on Content-Type (or the first character of the body) to handle each:
- JSON ProblemDetails for range and type checks.
{ "errors": { "Quantity": [...] }, "type", "title", "traceId" }- returned forquantitybelow 100, aboveInt32.MaxValue, or non-integer values. - JSON account-resolution error.
{ "statusCode": "BadRequest", "message": "...Account for User was not found, or User doesn't have entitlements.", "detail": null }- returned when the account ID cannot be resolved against the supplied credentials. - Plain-text required-field errors on
/acceptand/sell.- (root): <field> is required\n- (root): <other field> is required- one line per missing required field, joined by\n.
Branching on Content-Type (or the leading character of the body) is the cleanest way to handle each shape.
Additional Resources
- Account Types - paper vs. live accounts, and why locates are live-only.
- Account Information - check
availableCashandbpbefore sizing a short to make sure the position fits. - Equity Trading - once the locate is in inventory, open the short with
side: "Sell", openClose: "Open"and close it later withside: "Buy", openClose: "Close". The wire format only accepts"Buy"and"Sell"; the conceptual "Short" and "Cover" trader actions are derived from theside+openClosepair (see Trader actions). - Open Positions - read back
side: "Short"rows to monitor your active shorts against your locate inventory. (Position rows do use"Long"/"Short"directly, even though order requests do not.) - API Reference - Locates - the OpenAPI-generated reference for all locate endpoints.
Recipes
Ready-to-run recipes for the locate lifecycle:
- Locate & Sell Short - the canonical ETB-check → quote → poll → accept → short → credit-back walk-through.
- Handle Locate Rejections - classify and recover from
R56,R06,11 No shares available.,14 Locate is already pending., and the rest of the rejection codes covered above. - Locate Type Selection (PB vs SU) - quote a Reg SHO threshold name and pick between the Pre-Borrow and Single Use offers based on cost and reuse needs.
- Cancel a Pending Locate - decline an Offered quote inside the 30-second window without consuming inventory.
- Cancel a Pending Sell-Back - revert a queued credit-back so the shares stay in inventory.
- Reuse Locate Inventory - check
/locates/inventoryfirst and short against existing rows before quoting fresh inventory. - Locate Basket - parallel ETB-check + quote a basket of symbols and accept inside a budget.
- Pre-Market Short Setup - composite morning workflow that combines reuse, ETB filtering, parallel locate quoting, and final inventory confirmation.