Sending pieces
One way in: a file. Every rule below applies to every piece inside it.
POST /api/v1/integration/pieces/import
Send a .csv, .xlsx or .zip as multipart form data, in a field named file:
curl -H "X-API-Key: mgi_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-F "file=@example.zip" \
https://your-server/api/v1/integration/pieces/import
The field name matters: anything other than file arrives as nothing and the import fails. So does the file extension — the server decides what to do by the name you send, so a ZIP called data.txt is rejected.
{
"fileFormat": "ZIP (CSV)",
"totals": { "read": 3, "created": 1, "ignored": 1, "rejected": 1 },
"items": [
{ "barcode": "ABC-001", "outcome": "CREATED" },
{ "barcode": "ABC-002", "outcome": "IGNORED", "reason": "Barcode already exists" },
{ "barcode": "ABC-003", "outcome": "REJECTED", "reason": "Enter at least one service in ServicosAplicaveis (e.g. [CORTE_MONO],[TEMPERA]) — without a service there is no production routing." }
],
"warnings": [],
"createdGlassTypes": [],
"createdCustomers": []
}
The fields you may not expect
fileFormat tells you what the server actually read — ZIP (CSV) means it found a CSV inside your ZIP. Useful when a package is built wrong.
warnings are non-fatal. An image or shape referenced in the spreadsheet but missing from the package, for example: the piece went in, the attachment did not.
createdGlassTypes and createdCustomers list what the system created on its own from your file. A glass type or a customer that does not exist yet is created during import — and getting the list back is what stops the master data from growing behind your back. If GLASS A and GLASS A both show up here across two weeks, someone is typing it differently upstream.
The columns the file may carry are on the file format page. A column we do not know is ignored, not rejected.
The three outcomes
| Outcome | What it means | What to do |
|---|---|---|
CREATED | The piece was created, with its production route | Nothing |
IGNORED | That barcode already existed | Nothing. This is the expected result of a retry |
REJECTED | It did not go in, and reason says why | Fix the data and resend only the refused rows |
outcome is always sent as text, never as a number. That is guaranteed and tested — an enum serialized as a number would force you to memorize an order that could silently change.
reason is filled on IGNORED and REJECTED, and null on CREATED, where there is nothing to explain.
reason comes back in your language. Send an Accept-Language header — en, it, es-ES or pt-BR — and the refusal text arrives translated. Without the header you get Portuguese. The text is for a human to read: never branch your code on it, because the wording can be reworded and the translation can be corrected. Branch on outcome, which never changes.
The file is not atomic
Each piece runs in its own transaction. A file with 100 pieces and 3 bad rows saves 97 and refuses 3. There is no rollback of the batch.
This is why the response is item by item. Two consequences worth designing for:
- Resending the whole file after fixing it is safe — the 97 come back as
IGNORED - But knowing which 3 failed saves you from doing that at all
Retrying is safe
The barcode is yours, and we guarantee we will not duplicate it.
If a request times out and you have no idea whether it arrived, send it again. The worst case is IGNORED. There is no reconciliation step, no idempotency key to manage, and no window where a retry creates a twin.
What a refusal is not
A refusal is a business answer, not a server failure. It arrives with 200, not 500.
That distinction matters for your retry logic: 5xx means try again later, the content was never the problem. REJECTED means the content is the problem, and retrying it unchanged will fail identically forever.