jonathan8312 / plemsi-laravel
Community Laravel SDK for the public PLEMSI API
Requires
- php: ^8.2
- ext-json: *
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0|^11.0
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^11.5.50|^12.5.8|^13.0.0
- vlucas/phpdotenv: ^5.6
README
A community-maintained Laravel SDK for the public PLEMSI API — Colombian electronic invoicing (facturación electrónica) and related DIAN-regulated documents.
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_URLyou 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.