Connecting your own systems
Four ways your prices, your capacity and the things you make can reach this marketplace β typed in by hand, pushed by your system, asked for by ours, or read off the shop you already run.
Four ways to be listed
These are peers, not tiers. Which one fits depends on whether you already run a system worth connecting, and on how often what you sell actually changes. Nobody is ranked lower for typing their rate card in by hand.
| Route | What it costs you to set up | Who keeps it true | When it updates | Who it suits |
|---|---|---|---|---|
| π₯οΈ You type it here β | Nothing. It is a form. | You, by hand | When you remember to edit it | A workshop whose rate card moves twice a year. It is also the only one of the four that works today. |
| π€ You push to us π§ | One scheduled job in your system that posts a file | You. Your system stays the source of truth. | As often as you send it | You already run stock or ERP software, and your prices move faster than you would want to retype them. |
| π₯ We ask you π§ | You host one endpoint and we call it | You, as part of your own service staying up | Per order, at the moment somebody asks | Your price genuinely depends on the job β the quantity, the material, how full next week already is. |
| π We read your shop π§ | You send us a link and answer a few questions | Us β and we will get things wrong | Whenever we next re-read the page | You have a shop already and no appetite whatsoever for any of the above. |
Things every API route would share
π Authentication
Proposed: a bearer token issued from your supplier dashboard and scoped to the one account it belongs to β the same account you already log in with, since becoming a supplier here never created a second one. Requests we make to you would be signed the other way round, as an HMAC of the raw body in an X-Personali-Signature header, so you can prove a request is ours without us holding a secret of yours.
Whether a plain token, OAuth or mutual TLS is right here is genuinely undecided, and a supplier who has already had to integrate with several marketplaces has a more useful opinion about it than we do.
β»οΈ Sending the same thing twice
Catalogue writes are an upsert keyed on sku β your own identifier for the item, which we store and never rewrite. Sending the same sku again edits that item instead of creating a second one, so re-running a sync that failed halfway is safe and you never have to keep a table of our ids alongside yours.
An item you stop sending is not deleted. Silence is not a withdrawal here, the same rule the rest of this marketplace already follows for a supplier who leaves a size field blank. Retiring an item is an explicit status of withdrawn, so a truncated file can never quietly empty your catalogue.
β οΈ When something does not validate
Per item, not per request. Forty items with two bad rows would store thirty-eight and report the two, because rejecting a whole file over one typo leaves your catalogue stale until somebody happens to notice. The response names the item by your own sku and the field by its path.
A value we do not recognise β a material with no id here, a production step we do not run β is reported as unrecognised rather than dropped. A field quietly ignored is exactly how a supplier ends up believing they published something they did not.
π€ Route 2 β pushing your catalogue to us
Two calls. One says who you are and what you charge; the other says what you make. Both mirror shapes this app already uses internally, which is why the fields read like the application form β that is precisely what they are.
Step 1 β your supplier profile
role and category decide which requests you are shown at all. price and turnaroundDays are your headline rate. priceBreaks is your volume ladder as an explicit list of thresholds you chose, rather than a curve fitted on your behalf β a producer's breaks are a business decision they can explain, and we would rather publish yours than invent one. city is what makes collect-in-person and printers-near-you mean anything.
sizesOffered, madeToMeasure and maxDimensionsCm are your capability statement: which sizes you stock or cut, whether you will build to a customer's own measurements at all, and the largest thing that fits through your workshop. Leave any of them out and we treat it as unstated, never as a refusal β a supplier who fills in less of the form should not be quietly penalised for it.
Every amount is PLN per unit, because every price in this app is. See the open questions β that is a real limitation, not a convention we are happy with.
role artist | vendor | warehouse | service | seller
category print | sew | assemble | engrave | transport (role: vendor only)
sizesOffered xs | s | m | l | xl | xxl | xxxl | one_size | s_m | l_xl
city warszawa | krakow | lodz | wroclaw | poznan | gdansk | szczecin |
katowice | lublin | rzeszow | kyiv | lviv | odesa | berlin | praha |
vilnius | bratislava | amsterdam | paris | madrid POST /v1/supplier/profile
Authorization: Bearer <your-token>
Content-Type: application/json
{
"role": "vendor",
"category": "print",
"displayName": "PrintHouse KrakΓ³w",
"avatarEmoji": "π¨οΈ",
"city": "krakow",
"price": 24,
"turnaroundDays": 3,
"priceBreaks": [
{ "minQty": 25, "unitPrice": 20 },
{ "minQty": 100, "unitPrice": 17 }
],
"description": "DTG and 4-colour screen print. Under 25 pieces we run DTG; above that screen becomes cheaper and we will say so rather than quietly charge the DTG rate.",
"sizesOffered": ["s", "m", "l", "xl", "xxl"],
"madeToMeasure": false,
"maxDimensionsCm": { "length": 200, "width": 120, "height": 60 }
} Step 2 β the items you supply
An item here is one concrete, orderable thing inside one of our product types, not a product type of its own. Our Mug is the card; your 450 ml double-walled ceramic mug is an item on it. So every item names the productId it belongs to.
priceModifier is what your item costs over the cheapest item on that same card, in PLN per unit. A modifier rather than a full price on purpose: the printing or the sewing is the same work whichever item of that product it is done to, so a second full price would have to restate the chain and could then disagree with it. extraLeadDays is what your item adds to the schedule β a hand-thrown mug is not pulled off a shelf.
material and personalizations come from closed vocabularies rather than free text, because customers filter on them: Organic cotton and organic cotton would become two different filters the first time two people typed them. nameLocalized is optional and worth sending β the invented brand part of a name does not translate, but Heavyweight and Hand-Finished are product facts, and leaving those in English tells a Polish buyer nothing.
stock is the one field on this page with no counterpart anywhere in the app today. It appears in the example because a real integration would carry it; see the open questions for why nobody can yet say what it would do.
productId hoodie | tshirt | cap | tote_bag | mug | water_bottle | tumbler |
poster | canvas | stickers | cushion | wall_clock | blanket |
phone_case | keychain | pen | chair | bookshelf
material cotton | organic_cotton | cotton_blend | fleece | wool | velvet |
canvas | recycled_polyester | recycled_plastic | silicone | leather |
ceramic | stainless_steel | brass | bamboo | oak | pine | paper |
fine_art_paper | vinyl
personalizations print | embroidery | engraving | foil | handpaint
status listed | withdrawn POST /v1/supplier/items
Authorization: Bearer <your-token>
Content-Type: application/json
{
"items": [
{
"sku": "PH-MUG-450-DW",
"productId": "mug",
"name": "BrewLine Thermal 450",
"nameLocalized": { "pl": "BrewLine Termiczny 450", "uk": "BrewLine Π’Π΅ΡΠΌΠΎ 450" },
"material": "ceramic",
"personalizations": ["print", "engraving"],
"priceModifier": 14,
"extraLeadDays": 2,
"stock": 340,
"status": "listed"
},
{
"sku": "PH-TEE-ORG-XXXL",
"productId": "tee",
"name": "Organic Heavyweight Tee",
"material": "organic-cotton",
"personalizations": ["print"],
"priceModifier": 9,
"status": "listed"
}
]
} What comes back
A 207, listing each sku you sent and what happened to it. The rejected row below fails twice on purpose: one field is a near-miss on a real vocabulary, the other is a product id that does not exist. Both name the alternatives, because an error that only says no costs somebody an afternoon.
HTTP/1.1 207 Multi-Status
Content-Type: application/json
{
"accepted": 1,
"rejected": 1,
"results": [
{
"sku": "PH-MUG-450-DW",
"status": "stored",
"listedAs": "mug / BrewLine Thermal 450"
},
{
"sku": "PH-TEE-ORG-XXXL",
"status": "rejected",
"errors": [
{
"field": "productId",
"code": "unknown_product",
"message": "No product 'tee'. Apparel ids are hoodie, tshirt, cap, tote_bag.",
"didYouMean": "tshirt"
},
{
"field": "material",
"code": "unknown_value",
"message": "'organic-cotton' is not a material id.",
"didYouMean": "organic_cotton"
}
]
}
]
}π₯ Route 3 β us sending requests to you
The mirror image, and the more interesting one. Instead of publishing a rate card and letting our arithmetic apply it, you register a URL and we ask what one specific job costs at the moment somebody wants it. What we send is the estimate request this marketplace already runs on; what we expect back is a proposal, which is exactly what a supplier answering by hand in the dashboard types into a form.
The quote request
POST https://your-system.example/personali/quote
X-Personali-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015
Content-Type: application/json
{
"requestId": "req_8f2c41",
"cartId": "cart_31a9d0",
"target": { "kind": "step", "itemId": "item_7d1", "stepId": "print" },
"stepLabelKey": "step_label_print",
"quantity": 120,
"bulkTargetQuantity": 2500,
"item": {
"productId": "tshirt",
"specificProductId": "tshirt-std",
"supplierSku": "PH-TEE-ORG-L",
"size": "l",
"material": "organic_cotton",
"personalizations": ["print"]
},
"deliverTo": { "city": "warszawa" },
"respondBy": "2026-08-07T09:41:12Z"
} Honest note on this payload: requestId, target, stepLabelKey, quantity and bulkTargetQuantity are fields the app's own estimate request already carries. item, deliverTo and respondBy are not. A supplier reading the dashboard can see the item on the screen in front of them and a machine cannot, so a webhook has to say out loud what a page merely shows. Flagged rather than blended in, so that nothing here reads as documentation of something that exists.
Your answer
One gotcha worth stating loudly: price is for the whole request β all 120 units β and not per unit. That is the convention the rest of this marketplace already uses for a step's price, and a machine quoting per unit by mistake would undercut itself by two orders of magnitude. bulkUnitPrice is the per-unit figure at the bulk target, and is the number a corporate buyer is really deciding on when a sample run is in play.
HTTP/1.1 200 OK
Content-Type: application/json
{
"proposals": [
{
"price": 1980,
"turnaroundDays": 4,
"bulkUnitPrice": 12.4,
"note": "Screen print, 3 colours. The 2 500 rate assumes one artwork, no colour change."
}
]
} Declining is a real answer
An empty proposals array means not this one. It is better for both sides than a price you would rather not honour, and it is what a supplier does in the dashboard every day by simply not bidding. This is a quote and not an order: the customer still compares it against everybody else's and may well pick another workshop.
HTTP/1.1 200 OK
Content-Type: application/json
{ "proposals": [] } If your endpoint does not answer
A quote we cannot get inside a few seconds would fall back to your published rate card if you have one, and to nothing if you do not β a customer cannot be left waiting on a server that is not answering. What the timeout should be, and whether an endpoint that keeps timing out should mute itself until you fix it, is unresolved.
Or let us pull the catalogue as well
If a scheduled push is the awkward half for you, the same items array from route 2 can be something we fetch rather than something you send. One GET, the same body, the same upsert rules. Which of the two is less work is entirely a fact about your systems, so both are offered instead of one being named the right way.
GET https://your-system.example/personali/catalogue
X-Personali-Signature: sha256=1b4f0e9851971998e732078544c96b36
Accept: application/json
β 200 OK, with a body identical to the "items" payload in route 2 above.π We read your shop
What reading your website really means
There is no reliable way to read an arbitrary shop, and we are not going to describe this as a sync. Whatever gets built here will be part parser and part person: a first pass over your pages, then somebody at this end matching what was found onto our own categories, production steps, materials and sizes, then you approving the result before any of it goes live.
A listing made this way is still yours. You can correct it in the ordinary form, and a correction always outranks anything we read later β a supplier who fixes a wrong price should not watch it revert overnight.
What we would need from you
- The address of the shop, and whether we may read it.
- Which of our production steps you actually perform, since a shop page shows finished goods and says nothing about which part of the chain is yours.
- Whether the prices shown include VAT, because a page rarely says and guessing wrong is a 23% error.
- A person to email when the match is wrong, which it sometimes will be.
Deliberately unresolved
Naming these is the point of publishing early. Each one is a decision that should be made with suppliers rather than presented to them.
- Rate limits and payload size. Nothing is decided. A supplier with forty thousand items and a supplier with twelve are both plausible here, and picking a page size before either exists would only be a number to walk back.
- Stock. Made-to-order production has no stock at all, and the one place this marketplace sells finished goods tracks availability as a claim against a listing rather than a count anyone pushes. Whether pushed stock decrements on order, expires on its own, or is purely advisory has no answer yet.
- Currency. Every price in this app is PLN and there is no currency field anywhere in it. A Ukrainian or German supplier integrating by API is exactly who breaks that, and it cannot be fixed by adding a field without also deciding who converts, at whose rate, and who carries the movement between quote and invoice.
- The authentication model itself, and how a token gets rotated or revoked when somebody leaves your company.
- Retries and replay on requests we make to you: how many, how far apart, and whether answering the same quote request twice has to be safe.
- Whose write wins. If you push a price and then correct it in the form, one of the two has to lose. Our instinct is that the most recent write wins whichever way it arrived β but a supplier whose ERP silently overwrites a hand-typed fix every night would reasonably hate that, so it is not settled.
- There is no sandbox, no test credential and no machine-readable schema, because there is no server. When there is one, the sandbox comes before the documentation rather than after it.
- Route 4's parser does not exist even in outline. How much of it can be automatic and how much is always going to be a person reading your pages is the question that decides whether it gets offered at all.
What you can actually do today
Apply through the form like everybody else. Use the description field to say which of these four routes you would want and what your system is β it is free text precisely because no structured field could hold that answer. It reaches a person, and it is the only thing on this page that reaches anything at all.