Appearance
Your first request
Five minutes, three calls: one that works, one that is refused because the key was not minted for it, and one that writes. You need an admin seat on a Tightly organisation, on a plan that includes the public API, which is sold with Essentials: below that the Developer page is not there to mint from. Nothing here is a sandbox: a key reaches your own live organisation, so the write is left until last and it is a preview that changes nothing.
Choose your region
Use the API in the same region as your Tightly organisation.
| Region | App | API base URL |
|---|---|---|
| US | app.tightly.io | https://api.app.tightly.io |
| EU | app.eu.tightly.io | https://api.eu.tightly.io |
The examples below use the US URL. For EU, replace it with https://api.eu.tightly.io. The reference's Try it currently uses the US endpoint.
1. Mint a key
In Tightly, open Settings, then Developer, then API keys, and press New key.
Give it a label you will recognise in a month, then tick the reach. For this page, tick Read on Stock. Leave Expires at a year and Allowed addresses empty.
Press Create. The key is shown once, in full, and never again. Copy it now.
bash
export TIGHTLY_API_KEY="tly_live_..."A key carries the organisation it was minted in, so you never send an organisation header. Anything holding this key can do what you ticked, as that organisation, until it expires or you revoke it.
2. Read your stock
bash
curl "https://api.app.tightly.io/api/v1/inventory/table?limit=3" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"json
{
"data": {
"filtered_max_size": 4120,
"filtered_max_unique_size": 1284,
"offset": 0,
"rows": [
{
"available_quantity": 261,
"available_to_sell": 245,
"in_basket_quantity": 16,
"on_order": 800,
"weeks_of_cover": 2.4,
"health": "caution",
"sales_velocity_7_days": 108.5,
"sales_velocity_30_days": 452.1,
"capital_quadrant": "high_volume_low_value",
"location_id": "loc_0004",
"location_name": "London DC",
"product_id": "4410092",
"product_title": "Terry Crew",
"sku": "TB-CREW-BLK-M",
"variant_id": "44100920011",
"variant_title": "Black / M"
}
],
"size": 1
},
"message": { "desc": "", "service": "inventory", "severity": "INFO" }
}Every response carries X-Request-Id, the handle on that one call, and X-Tightly-Region, the home that served it. Every response the key reached also carries the three rate-limit headers, so a client can see its own budget without guessing:
X-Request-Id: req_8f3c1d0a6b2e4a519c772e0a4b6d1f83
X-Tightly-Region: us
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 599
X-RateLimit-Reset: 1793826000That is the whole of authentication. If it returned 200, you are done with the hard part.
3. See a refusal, on purpose
The key you minted carries inventory:read and nothing else. Ask it for purchase orders:
bash
curl -i "https://api.app.tightly.io/api/v1/organizations/$ORG/purchase-orders" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"HTTP/1.1 403 Forbiddenjson
{
"code": "scope_missing",
"message": {
"code": "scope_missing",
"desc": "This key cannot read Purchase orders.",
"doc_url": "https://docs.tightly.io/api/guides/errors#scope_missing",
"request_id": "req_6d2b7e1490af4c338b0e51d9c7a2e4b6",
"service": "UNKNOWN",
"severity": "ERROR"
}
}Three things to take from that refusal, because every refusal on this API is shaped the same way:
- Branch on
message.code, never on the status.403covers six different reasons; the code says which. doc_urlis a real link and it lands on the section of the Errors page that explains what to do.request_idis your handle on the call. Paste it into the key's Usage drawer in Tightly and the call comes back: the operation, the status, when.
Widening a key is minting a new one. Open the key's row menu, choose Replace, tick the reach you want, and choose how long the old key keeps working.
4. Write something, safely
POST /sell-out/preview recognises a retailer's own report and tells you what would happen. It writes nothing, which makes it the right first write to try.
bash
curl -X POST "https://api.app.tightly.io/api/v1/sell-out/preview?account_id=tp_00417" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: text/csv" \
--data-binary @week.csvjs
const response = await fetch(
'https://api.app.tightly.io/api/v1/sell-out/preview?account_id=tp_00417',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.TIGHTLY_API_KEY}`,
'Content-Type': 'text/csv',
},
body: await readFile('week.csv'),
},
)
const body = await response.json()
if (!response.ok) throw new Error(`${body.code}: ${body.message.desc}`)The answer names the profile that recognised the file, the week it resolved to, how many rows would land and the confirmations still outstanding. confirmed is what this account has already answered about its own reports and we_read_it_as is what this file proposes, so a difference between the two is a change worth looking at rather than a question to answer again. In both, week_ends_on and week_anchor_dow name the day the retailer's reporting week ends, whichever day the dates inside the file happen to carry.
A retailer no profile recognises is not a dead end once account_id is given: the preview keeps the file's header and a sample of its rows, matches its columns against the fields sell-out is stored in, and answers with mapping, one proposal per column and the reason for it. confirmed travels with that proposal, so an account that has answered the four sees its own answers beside the columns. Confirm the mapping once in Tightly and every later file from that account reads itself, with recognised_as naming the confirmed version rather than a profile. Send file_name alongside account_id to record what the upload was called, so whoever reviews the proposal can tell which file it was matched from.
An account whose mapping is confirmed but cannot read the file in front of it is refused 400 with the reason under data.reason: not_declared, no_door, mixed_currency, unreadable_file, no_columns or nothing_to_read. Each names the step that is missing, and none of them means the retailer is one we do not recognise. Every field of the answer and every refusal is on Recognise a retailer's report and say what would happen.
The preview needs sell_out:write, because a preview of a write is still an operation only a writer should be able to run. Mint a second key for it, or replace the first one with the reach widened.
Where to go next
| Next | Why |
|---|---|
| Authentication | Expiry, replacement, allowed addresses, and what one key can reach |
| Scopes | Every resource, in the words the reach matrix uses |
| Errors | Every code, and what a client should do about each |
| Pagination | offset and limit, and the caps |
| Reference | Every operation, with Try it against your own organisation |