jonathan8312/plemsi-laravel

Community Laravel SDK for the public PLEMSI API

Maintainers

Package info

github.com/Jonathan8312/plemsi-laravel

pkg:composer/jonathan8312/plemsi-laravel

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

v0.1.0 2026-08-23 16:53 UTC

This package is auto-updated.

Last update: 2026-08-23 17:51:33 UTC


README

A community-maintained Laravel SDK for the public PLEMSI API — Colombian electronic invoicing (facturación electrónica) and related DIAN-regulated documents.

CI License: MIT

Not an official PLEMSI package. This is an independent, open-source integration built by reading PLEMSI's public Postman collection and verifying real behavior against their staging environment. It is not affiliated with, endorsed by, or supported by PLEMSI.

What this package does

jonathan8312/plemsi-laravel gives your Laravel application a clean, typed, testable way to talk to PLEMSI: issuing electronic invoices, credit and debit notes, support documents, and RADIAN events; managing customers, your company profile, and DIAN numbering resolutions; and looking up DIAN reference catalogs (municipalities, taxes, payment methods, and more).

It is deliberately just an SDK — it does not calculate totals or taxes, does not model your invoices/orders/customers in your database, and does not perform DIAN's own validation on your behalf. Your application owns its domain logic; this package owns talking to PLEMSI correctly and safely.

It is also free, open source, and has no phone-home behavior — no license checks, no telemetry, no tracking, no remote activation. It only ever talks to the PLEMSI base URL you configure.

Requirements

PHP 8.2 or newer
Laravel 12.x or 13.x

This is the exact matrix verified on every change by CI (PHP 8.2/8.4 against Laravel 12, PHP 8.3/8.5 against Laravel 13). No other combination is tested or supported.

Installation

composer require jonathan8312/plemsi-laravel

The package registers itself automatically via Laravel package discovery — no manual service provider registration needed.

Optionally publish the config file:

php artisan vendor:publish --tag=plemsi-config

This creates config/plemsi.php. Publishing it is optional — the package ships with sensible defaults for everything except your PLEMSI token and base URL, which you must set yourself.

Configuration

Set these in your application's .env:

PLEMSI_TOKEN=your-plemsi-bearer-token
PLEMSI_BASE_URL=https://pruebas.plemsi.com
Variable Required Default Description
PLEMSI_TOKEN No (none) Default Bearer token, used by every call that doesn't explicitly select a company via withToken(). Leave unset for multi-tenant apps.
PLEMSI_BASE_URL Yes (none) The PLEMSI API base URL. No default is provided on purpose — see Environments.
PLEMSI_CONNECT_TIMEOUT No 5 TCP connection timeout, in seconds.
PLEMSI_TIMEOUT No 15 Total request timeout, in seconds.
PLEMSI_RETRY_ENABLED No false Whether idempotent GET requests may be retried automatically.
PLEMSI_RETRY_MAX_ATTEMPTS No 2 Max attempts (including the first) for a retryable GET request.
PLEMSI_RETRY_BACKOFF_MS No 200 Milliseconds between retry attempts.

PLEMSI_BASE_URL has no built-in default — not even PLEMSI's own staging URL — so a misconfigured deployment fails loudly at boot instead of silently talking to the wrong environment. See Environments and Configuration for the full detail.

Quick start

use Jonathan8312\Plemsi\Plemsi;
use Jonathan8312\Plemsi\DataTransferObjects\Requests\Invoices\InvoiceData;
use Jonathan8312\Plemsi\DataTransferObjects\Requests\DocumentCustomerData;
use Jonathan8312\Plemsi\DataTransferObjects\Requests\DocumentItem;
use Jonathan8312\Plemsi\DataTransferObjects\Requests\AdValoremTax;
use Jonathan8312\Plemsi\DataTransferObjects\Requests\Payment;
use Jonathan8312\Plemsi\DataTransferObjects\Requests\DocumentLookupField;

$plemsi = app(Plemsi::class);

$response = $plemsi->invoices()->create(new InvoiceData(
    date: '2026-01-01',
    time: '10:00:00',
    prefix: 'SETT',
    number: 1,
    customer: new DocumentCustomerData(
        identificationNumber: '1067987456',
        name: 'Cliente de Ejemplo',
        typeDocumentIdentificationId: 3,
    ),
    items: [new DocumentItem(
        unitMeasureId: 70,
        lineExtensionAmount: 100000,
        freeOfChargeIndicator: false,
        description: 'Servicio de ejemplo',
        code: 'ITEM-1',
        typeItemIdentificationId: 4,
        priceAmount: 100000,
        baseQuantity: 1,
        invoicedQuantity: 1,
        taxTotals: [new AdValoremTax(taxId: 1, percent: 19, taxAmount: 19000, taxableAmount: 100000)],
    )],
    resolution: '18760000001',
    invoiceBaseTotal: 100000,
    invoiceTaxExclusiveTotal: 100000,
    invoiceTaxInclusiveTotal: 119000,
    totalToPay: 119000,
    payment: new Payment(paymentFormId: 1, paymentMethodId: 10, paymentDueDate: '2026-01-31', durationMeasure: '30'),
    allTaxTotals: [new AdValoremTax(taxId: 1, taxAmount: 19000, percent: 19, taxableAmount: 100000)],
));

$response->json();

// Later, look it up again:
$plemsi->invoices()->find(DocumentLookupField::Cude, $cude);

Every resource is reached through $plemsi->{resource}(), never through a hand-built PLEMSI URL — see each resource's own doc page below for its full request shape, since PLEMSI's payloads are too specific to summarize generically here.

Available resources

Resource Accessor Docs
Catalogs $plemsi->catalogs() docs/catalogs.md
Customers $plemsi->customers() docs/customers.md
Company $plemsi->company() docs/company.md
Invoices $plemsi->invoices() docs/invoices.md
Credit Notes $plemsi->creditNotes() docs/credit-notes.md
Debit Notes $plemsi->debitNotes() docs/debit-notes.md
Resolutions $plemsi->resolutions() docs/resolutions.md
General $plemsi->general() docs/general.md
Support Documents $plemsi->supportDocuments() docs/support-documents.md
RADIAN Events $plemsi->radianEvents() docs/radian-events.md

Two resources not implemented yet: Nota Crédito a Documento Soporte and Docs. Equivalentes / POS. See Known Issues for status.

Methods, resource by resource

Catalogs$plemsi->catalogs() — read-only DIAN reference lists:

documentIdentificationTypes(), municipalities(), municipalitiesComplete(), countries(), departments(), regimeTypes(), liabilityTypes(), operationTypes(), organizationTypes(), taxes(), unitMeasures(), paymentMethods(), paymentForms(), itemIdentificationTypes(), eventRejectionTypes(), documentTypes(), healthOperationTypes(), healthContractingPaymentMethods(), healthCoverageTypes(), healthPaymentCollectionTypes(). See docs/catalogs.md.

Customers$plemsi->customers():

Method What it does
create(CustomerData $customer) Register a new customer.
all() List every customer registered for the current company.
find(string $id) Look up a customer by PLEMSI's internal identifier.
dianBasicInfo(int $typeDocumentId, string $identificationNumber) Look up a person/company's basic info directly from DIAN.
filtered(CustomerFilters $filters) Search customers by DNI, name, email, and/or phone.
update(string $id, CustomerData $customer) Partially update an existing customer.

Company$plemsi->company():

Method What it does
get() Get the current company's profile.
update(CompanyData $company) Partially update the current company's profile.

Invoices$plemsi->invoices():

Method What it does
create(InvoiceData $invoice) Issue a new electronic sales invoice.
all(?int $limit = null) List invoices for the current company.
find(DocumentLookupField $by, string $value) Find a single invoice by cude, number, or id.
pdf(string $cude) Get the invoice's graphic representation (PDF), base64-encoded.
xml(string $cude) Get the invoice's XML (AttachedDocument), base64-encoded.

Credit Notes$plemsi->creditNotes():

Method What it does
create(CreditNoteData $creditNote) Issue a new credit note, referencing an invoice or standalone.
all(NoteListOptions $options = new NoteListOptions) List credit notes for the current company.
find(DocumentLookupField $by, string $value) Find a single credit note by cude, number, or id.
xml(string $cude) Get the credit note's XML (AttachedDocument), base64-encoded.

Debit Notes$plemsi->debitNotes():

Method What it does
create(DebitNoteData $debitNote) Issue a new debit note.
all(NoteListOptions $options = new NoteListOptions) List debit notes for the current company.
find(DocumentLookupField $by, string $value) Find a single debit note by cude, number, or id.
xml(string $cude) Get the debit note's XML (AttachedDocument), base64-encoded.

Resolutions$plemsi->resolutions():

Method What it does
create(ResolutionData $resolution) Register a new DIAN numbering resolution.
all(ResolutionListOptions $options = new ResolutionListOptions) List the resolutions registered for the current company.
remainingNumbers() Get the remaining folio/consecutive numbers for every registered resolution.
remainingNumbersFor(string $resolution) Get the remaining folio/consecutive numbers for one resolution, by resolution number.

General$plemsi->general():

Method What it does
resendEmail(string $document, string $targetEmail) Resend the notification email for a previously issued sales document.
some(string $id) Deprecated — confirmed disabled by PLEMSI itself.

Support Documents$plemsi->supportDocuments():

Method What it does
create(SupportDocumentData $supportDocument) Issue a new support document.
all(SupportDocumentListOptions $options = new SupportDocumentListOptions) List support documents for the current company.
find(DocumentLookupField $by, string $value) Find a single support document by cude, number, or id.

RADIAN Events$plemsi->radianEvents():

Method What it does
create(RadianEventType $type, RadianEventData $event) Issue a new RADIAN event against a referenced invoice.
all(RadianEventListOptions $options = new RadianEventListOptions) List RADIAN events for the current company.
find(string $id) Find a single RADIAN event by PLEMSI's internal _id.

Every table above is a summary — request shapes, required/optional fields, and real behavior confirmed against staging are documented in full on each resource's own doc page linked above. This README intentionally does not duplicate that detail.

Multi-company / SaaS usage

PLEMSI issues one token per company. For applications serving more than one company, resolve a scoped instance per request instead of relying on the configured default:

use Jonathan8312\Plemsi\Plemsi;

$plemsi = app(Plemsi::class);

$acme = $plemsi->withToken($acmeCompanyToken);
$acme->invoices()->all();

withToken() never mutates the instance it's called on — it always returns a new, independent one, which is safe under Octane and other long-running workers. See Authentication.

Error handling

Every failure is surfaced as one of a small set of typed exceptions, all extending Jonathan8312\Plemsi\Exceptions\PlemsiException:

Exception When
AuthenticationException HTTP 401; PLEMSI's specific invalid-token HTTP 400 body; or no token configured at all.
ValidationException HTTP 422.
ApiException Any other unsuccessful response (403, 404, 429, other 400s, 5xx).
ConnectionException No HTTP response received (DNS/TLS failure, refused connection, timeout).
use Jonathan8312\Plemsi\Exceptions\PlemsiException;

try {
    $plemsi->invoices()->create($invoice);
} catch (PlemsiException $exception) {
    $exception->statusCode(); // ?int
    $exception->endpoint();   // ?string, e.g. "POST /api/billing/invoice"
    $exception->response();   // ?PlemsiResponse — raw status/body/json/headers
}

POST/PUT requests (every document-issuing operation) are never retried automatically, under any configuration — an automatic retry of a non-idempotent call risks creating a duplicate electronic document. See Errors for the full model, including PLEMSI's inconsistent response envelope shapes across endpoints.

Testing your own application

This SDK talks to PLEMSI through Laravel's own HTTP client, so Http::fake() works exactly as it does for any other Laravel HTTP call:

use Illuminate\Support\Facades\Http;

Http::fake(['*' => Http::response(['ok' => true], 200)]);

$plemsi = app(Plemsi::class);
// every call this SDK makes, including via withToken(), is now faked

See Testing for asserting on requests and for running this package's own opt-in staging verification suite.

Security

  • No licensing checks, telemetry, tracking, or remote activation — this package only ever talks to the PLEMSI_BASE_URL you configure.
  • Tokens are never logged, never included in exception messages or debug output, and never persisted by this package.
  • withToken() returns new, immutable instances rather than mutating shared state — safe under Octane and other long-running workers.
  • Explicit connection/request timeouts by default; non-idempotent requests are never retried automatically.

See docs/errors.md and docs/authentication.md for the full detail. Please report suspected security issues privately to jt@jonathant.dev rather than opening a public issue.

Known limitations

Some PLEMSI/DIAN behavior is documented in docs/known-issues.md as an environment or upstream limitation rather than an SDK defect — for example, RADIAN event issuance requiring a second, acquirer-side company account to fully verify, and DIAN offering no test environment at all for Documento Soporte. Worth a read before you file an issue against unexpected staging behavior.

Scope

This package is intentionally just an SDK. It does not:

  • calculate or correct invoice totals, taxes, or any monetary value;
  • replicate DIAN's own validation engine;
  • model your application's customers, orders, or products;
  • retry non-idempotent operations automatically.

Your application remains responsible for its own domain logic; this package is responsible for communicating correctly and safely with PLEMSI.

Contributing

This package is built module by module, strictly against PLEMSI's documented Postman collection and behavior verified on their staging environment — never invented endpoints, fields, or business rules. If you'd like to contribute, please open an issue first to discuss scope.

Author

Maintained by Jonathan Torres — trebolcolombia.com.

License

MIT — see LICENSE.