Skip to content

Releases: stackin-io/stackin-php-sdk

v0.10.1

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 10 Sep 23:41
085e68f

What's Changed

  • Publishing works again — v0.10.0 never reached Packagist, so this is the first tag since v0.7.0 that does

  • FiscalReference — the published classification tables, with an accessor per kind

  • Taxpayer — one lookup, by exact tax id

  • Path segments are escaped — a value the caller types can no longer rewrite the request's URL

Why

An issuer filling a document needs CFOP, NCM, CEST and the rest, and until now the SDK had no way to ask. Both clients are read-only and take the same api key.

$ref = new FiscalReference(apiKey: '...');
$ref->ncm->get('84716052');
$ref->ncm->search('teclado', limit: 5);
$ref->kinds();

(new Taxpayer(apiKey: '...'))->get('00000000000191');

The eight named accessors are ergonomics. kind(\$name) reaches anything else, including a classification published after this release — ask kinds() rather than trusting the list, because the two answers diverge the moment the source data grows one.

metadata is passed through as the API sends it and differs per kind: utrib on an NCM, ncm_code on a CEST, tax_type on a CST, empty on an ISS service.

Three things worth knowing

These share the invoice read allowance — 600 calls a minute per key, the same bucket consult(), history(), received() and pdf() draw from. One page of search() beats N single lookups.

Ordering is fixed — kind, then code, ascending. Unlike history(), the search takes no sort or order argument: the route accepts them and discards them.

A 404 from Taxpayer does not mean the company does not exist. That registry reloads monthly from the RFB dump, so a recently registered CNPJ is simply not in it yet. It is not a validation rule.

Fixed

A tax id or code containing a / — which is how a CNPJ is normally written — used to land in the URL raw and address a different path, surfacing as a 404 that read like a registry miss. Segments are now escaped, and the ones that would leave the path are refused before the call.

v0.10.0

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 10 Sep 23:34
999f0eb

What's Changed

  • FiscalReference — the published classification tables, with an accessor per kind
  • Taxpayer — one lookup, by exact tax id
  • Path segments are escaped — a value the caller types can no longer rewrite the request's URL

Why

An issuer filling a document needs CFOP, NCM, CEST and the rest, and until now the SDK had no way to ask. Both clients are read-only and take the same api key.

$ref = new FiscalReference(apiKey: '...');
$ref->ncm->get('84716052');
$ref->ncm->search('teclado', limit: 5);
$ref->kinds();

(new Taxpayer(apiKey: '...'))->get('00000000000191');

The eight named accessors are ergonomics. kind(\$name) reaches anything else, including a classification published after this release — ask kinds() rather than trusting the list, because the two answers diverge the moment the source data grows one.

metadata is passed through as the API sends it and differs per kind: utrib on an NCM, ncm_code on a CEST, tax_type on a CST, empty on an ISS service.

Three things worth knowing

These share the invoice read allowance — 600 calls a minute per key, the same bucket consult(), history(), received() and pdf() draw from. One page of search() beats N single lookups.

Ordering is fixed — kind, then code, ascending. Unlike history(), the search takes no sort or order argument: the route accepts them and discards them.

A 404 from Taxpayer does not mean the company does not exist. That registry reloads monthly from the RFB dump, so a recently registered CNPJ is simply not in it yet. It is not a validation rule.

Fixed

A tax id or code containing a / — which is how a CNPJ is normally written — used to land in the URL raw and address a different path, surfacing as a 404 that read like a registry miss. Segments are now escaped, and the ones that would leave the path are refused before the call.

0.9.0

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 08 Sep 01:26

What's Changed

  • ibsCbs on a product — the Reforma Tributária group, per item

Why

The API has taken ibs_cbs since the tax-reform groups landed, but no SDK exposed it: a caller who needed IBS/CBS had to fall back to extraGroups and hand-build the payload. It is required from 2026 for the regime normal, so that gap had a deadline attached.

new Product(
    description: 'Teclado',
    quantity: 2.0,
    unitPrice: 120.00,
    ncm: '84716052',
    cfop: '5102',
    ibsCbs: [
        'cst' => '000',
        'classification' => '000001',
        'rate_state' => 0.1,
        'rate_city' => 0.0,
        'rate_federal' => 0.9,
    ],
);

cst (3 digits) and classification (cClassTrib, 6 digits) are your fiscal decision; the API derives the amounts from the three rates. base is optional — without it the base is the item amount.

An item without the group emits nothing, which is what an NF-e looked like before the reform. ibsCbs was added last in the constructor, so no positional or named argument moved.

0.8.0

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 07 Sep 23:39

What's Changed

  • unitPrice on a product — the price of one unit, beside amount, the line's gross total

Why

amount has always been the line's gross total, but a reader sees quantity: 2.0, amount: 120.00 and expects R$ 240,00. The API answered R$ 120,00, and the mistake only surfaced on an authorized document. There was no field that meant "what one unit costs", so the API derived it by dividing — in float, at the tenth decimal place the SEFAZ reads.

new Product(
    description: 'Teclado',
    quantity: 2.0,
    unitPrice: 120.00,
    unit: 'UN',
    ncm: '84716052',
    cfop: '5102',
);

Send unitPrice or amount. Sending both asserts they agree: the API compares quantity x unit_price against amount and refuses the document before transmission if they differ, naming the line and both numbers.

Both now travel as strings on the wire, so a ten-place unit price reaches the API exactly as typed. amount became optional and unitPrice was added last in the constructor, so no positional or named argument moved.

amount keeps the meaning it always had and is not deprecated — nothing has to migrate.

v0.7.0

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 06 Sep 16:06

What's Changed

  • cancel() takes an $idempotencyKey — a retried cancellation replays the first answer instead of cancelling twice

Why

issue() and reissue() already accepted the key; cancel() — the one irreversible call, inside a legal window — did not. A dropped connection or a retry sent a second cancellation.

$invoice->cancel(
    $accessKey,
    DocumentType::NFSE,
    'Pedido cancelado pelo comprador',
    idempotencyKey: 'cancel-8f21',
);

The same key with the same body replays the stored answer. A different body under the same key is refused, not silently accepted. Nothing generates a key for you: two calls without one are two cancellations, deliberately.

The parameter is optional and last. Nothing was removed and no other signature changed.

v0.6.0

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 06 Sep 08:01

What's Changed

  • submissions — every attempt made for one invoice, and what the authorizer answered to each

submissions — why a document was rejected

consult gives the status. This gives the reason.

$rows = $invoice->submissions('e4a1b2c3-0000-4000-8000-000000000001');

Takes the invoice id, like reissue and unlike everything else: a rejected document has no access key to look it up by.

Each row carries status, status_code (the authorizer's own code — 209, 539, and so on), detail (its message), protocol when one was issued, environment, endpoint, http_status, duration_ms, and the raw_request/raw_response exactly as they went over the wire.

The rows are attempts, not documents: a reissued invoice has more than one, oldest first, and only the last one describes the current status. raw_response keeps the authorizer's own shape and is deliberately not normalized — normalizing would mean guessing, and a wrong guess about a rejection is worse than the raw payload.

Nothing was removed and no signature changed.

Full Changelog: v0.5.0...v0.6.0

v0.5.0

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 06 Sep 07:24

What's Changed

  • history — lists what this company issued, the counterpart of received
  • The contract grew from nine methods to ten, in every language at once

history — the company's own issuance list

received lists what other companies issued against this one. This is the other side: what this company issued, newest first.

$result = $invoice->history(documentType: DocumentType::NFE, status: 'rejected', limit: 10);

Returns the usual paginated envelope. Each row carries both identifiers the other methods need: id, which reissue takes, and access_key, which consult, cancel, correct and pdf take. A rejected row has an id and no access key — the authorizer never assigned one.

Filters: document type, status (pending, authorized, rejected, cancelled), plus limit, offset and ordering.

It is named history rather than list because list is a builtin in Python and a language construct in PHP, and the API itself names the operation after the issuance history.

Nothing was removed and no signature changed.

Full Changelog: v0.4.0...v0.5.0

v0.4.0

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 06 Sep 02:48

Every tax group the official NF-e leiaute defines: 21 ICMS groups (was 5), PIS/COFINS by quantity, PISST, COFINSST, and the IPI stamp fields.

The tax payload now goes out in snake_case, matching the rest of the API. The XSD spelling is still accepted server-side, so existing code keeps working.

Generated from leiauteNFe_v4.00.xsd — see specs/tax/ in stackin-sdk-template.

This SDK had no tax module before.

v0.3.1

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 06 Sep 00:09

Standalone examples, every one of them reading STACKIN_API_KEY. received(), manifest() and pdf() documented in the README. LAST_VERSION is now verified against the tag, and the release issues a real NFS-e before publishing.

v0.3.0

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 05 Sep 17:49
7aba594

Adds pdf(), the seventh method on Invoice and the only one returning raw bytes.

The authorizer's own rendering of an authorized document. The XML remains the legally valid document; this is a convenience, and the authorizer's endpoint for it is unstable by their own documentation — an APIError with status 502 means the authorizer is unavailable, not that the invoice is wrong. NFS-e only; NF-e answers 501.

Internal: the HTTP layer was split so the raw response is available, because the previous path parsed JSON off every response and would have corrupted a PDF. Behaviour-neutral for the six existing methods.